Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Backup & Restore #2302

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ org.gradle.parallel=true
org.gradle.unsafe.configuration-cache=true
kotlin.daemon.useFallbackStrategy=false
org.gradle.kotlin.dsl.allWarningsAsErrors=true
org.gradle.jvmargs=-Xmx2g
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was required for me to be able to build the project the first time, but I can back it out if you want

3 changes: 3 additions & 0 deletions settings/src/main/kotlin/voice/settings/SettingsListener.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package voice.settings
import android.net.Uri

interface SettingsListener {
fun close()
Expand All @@ -11,6 +12,8 @@ interface SettingsListener {
fun dismissDialog()
fun getSupport()
fun suggestIdea()
fun backup(saveFile: (handle: (uri: Uri) -> Unit) -> Unit)
fun restore(openFile: (handle: (uri: Uri) -> Unit) -> Unit)
fun openBugReport()
fun openTranslations()
}
71 changes: 71 additions & 0 deletions settings/src/main/kotlin/voice/settings/SettingsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import android.content.Context
import android.content.Intent
import java.io.File
import androidx.core.net.toUri
import de.paulwoitaschek.flowpref.Pref
import voice.common.AppInfoProvider
Expand All @@ -15,8 +18,18 @@ import voice.common.grid.GridMode
import voice.common.navigation.Destination
import voice.common.navigation.Navigator
import voice.common.pref.PrefKeys
import voice.data.repo.internals.AppDb
import javax.inject.Inject
import javax.inject.Named
import android.net.Uri
import java.nio.file.Files
import kotlin.io.path.Path
import java.io.OutputStream
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import java.util.zip.ZipInputStream


class SettingsViewModel
@Inject constructor(
Expand All @@ -31,6 +44,8 @@ class SettingsViewModel
@Named(PrefKeys.GRID_MODE)
private val gridModePref: Pref<GridMode>,
private val gridCount: GridCount,
private val appDb: AppDb,
private val context: Context,
) : SettingsListener {

private val dialog = mutableStateOf<SettingsViewState.Dialog?>(null)
Expand Down Expand Up @@ -104,6 +119,62 @@ class SettingsViewModel
navigator.goTo(Destination.Website("https://github.com/PaulWoitaschek/Voice/discussions/categories/ideas"))
}

override fun backup(saveFile: (handle: (uri: Uri) -> Unit) -> Unit) {
val db = appDb.openHelper.readableDatabase
saveFile({ uri ->
val outp: OutputStream = context.contentResolver.openOutputStream(uri)!!
val zip = ZipOutputStream(outp)

val files = listOf(File(db.path!!), File(db.path!! + "-shm"), File(db.path!! + "-wal"))
for (file in files) {
if (!file.exists()) {
continue
}
zip.putNextEntry(ZipEntry(file.name))

val fileInputStream = file.inputStream()
val buffer = ByteArray(1024)
var length: Int

while (fileInputStream.read(buffer).also { length = it } > 0) {
zip.write(buffer, 0, length)
}

fileInputStream.close()
zip.closeEntry()
}

zip.close()
})
}

override fun restore(openFile: (handle: (uri: Uri) -> Unit) -> Unit) {
openFile({ uri ->
val db = appDb.openHelper.readableDatabase
val inp = context.contentResolver.openInputStream(uri)!!
val zip = ZipInputStream(inp)

val dbFile = File(db.path!!)

var entry = zip.getNextEntry()
while (entry != null) {
if (!entry.name.startsWith(dbFile.name) || entry.name.contains("/")) {
// invalid
continue
}
val outp = Path(dbFile.parent!!, entry.name)
Files.copy(zip, outp, REPLACE_EXISTING)

entry = zip.getNextEntry()
}

val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
System.exit(0)
})
}

override fun openBugReport() {
val url = "https://github.com/PaulWoitaschek/Voice/issues/new".toUri()
.buildUpon()
Expand Down
47 changes: 46 additions & 1 deletion settings/src/main/kotlin/voice/settings/views/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.GridView
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.Lightbulb
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Upload
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
Expand All @@ -23,6 +25,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
Expand All @@ -37,6 +41,10 @@ import voice.settings.SettingsListener
import voice.settings.SettingsViewModel
import voice.settings.SettingsViewState
import voice.strings.R as StringsR
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
import android.net.Uri

@Composable
@Preview
Expand Down Expand Up @@ -64,9 +72,13 @@ private fun SettingsPreview() {
override fun openTranslations() {}
override fun getSupport() {}
override fun suggestIdea() {}
override fun backup(saveFile: (handle: (uri: Uri) -> Unit) -> Unit) {}
override fun restore(openFile: (handle: (uri: Uri) -> Unit) -> Unit) {}
override fun openBugReport() {}
override fun toggleGrid() {}
},
{ _ -> },
{ _ -> },
)
}
}
Expand All @@ -75,6 +87,8 @@ private fun SettingsPreview() {
private fun Settings(
viewState: SettingsViewState,
listener: SettingsListener,
saveFile: (handle: (uri: Uri) -> Unit) -> Unit,
openFile: (handle: (uri: Uri) -> Unit) -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
Expand Down Expand Up @@ -141,6 +155,16 @@ private fun Settings(
leadingContent = { Icon(Icons.Outlined.Lightbulb, stringResource(StringsR.string.pref_suggest_idea)) },
headlineContent = { Text(stringResource(StringsR.string.pref_suggest_idea)) },
)
ListItem(
modifier = Modifier.clickable { listener.backup(saveFile) },
leadingContent = { Icon(Icons.Outlined.Download, stringResource(StringsR.string.pref_backup_database)) },
headlineContent = { Text(stringResource(StringsR.string.pref_backup_database)) },
)
ListItem(
modifier = Modifier.clickable { listener.restore(openFile) },
leadingContent = { Icon(Icons.Outlined.Upload, stringResource(StringsR.string.pref_restore_database)) },
headlineContent = { Text(stringResource(StringsR.string.pref_restore_database)) },
)
ListItem(
modifier = Modifier.clickable { listener.getSupport() },
leadingContent = { Icon(Icons.AutoMirrored.Outlined.HelpOutline, stringResource(StringsR.string.pref_get_support)) },
Expand Down Expand Up @@ -172,7 +196,28 @@ interface SettingsComponent {
fun Settings() {
val viewModel = rememberScoped { rootComponentAs<SettingsComponent>().settingsViewModel }
val viewState = viewModel.viewState()
Settings(viewState, viewModel)

val saveResult = remember { mutableStateOf<(uri: Uri) -> Unit>({ _ -> }) }
val saveLauncher = rememberLauncherForActivityResult(CreateDocument("application/binary")) { uri: Uri? ->
uri?.let { inner ->
saveResult.value(inner)
}
}

val openResult = remember { mutableStateOf<(uri: Uri) -> Unit>({ _ -> }) }
val openLauncher = rememberLauncherForActivityResult(OpenDocument()) { uri: Uri? ->
uri?.let { inner ->
openResult.value(inner)
}
}

Settings(viewState, viewModel, { cb ->
saveResult.value = cb
saveLauncher.launch("voice.zip")
}, { cb ->
openResult.value = cb
openLauncher.launch(arrayOf("*/*"))
})
}

@Composable
Expand Down
2 changes: 2 additions & 0 deletions strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
<string name="pref_theme_dark">Dark Theme</string>
<string name="pref_auto_rewind_title">Auto rewind</string>
<string name="pref_suggest_idea">Suggest an idea</string>
<string name="pref_backup_database">Backup database</string>
<string name="pref_restore_database">Restore database</string>
<string name="pref_use_grid">Use grid layout</string>
<string name="pref_report_issue">Report a problem</string>
<string name="pref_help_translating">Help translating Voice</string>
Expand Down