0
0
mirror of https://github.com/florisboard/florisboard.git synced 2024-09-20 12:02:19 +02:00

Add restore data functionality

This commit is contained in:
Patrick Goldinger 2022-01-10 23:00:00 +01:00
parent 23dddfd16e
commit 96e7f2eeac
9 changed files with 174 additions and 95 deletions

View File

@ -156,9 +156,9 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.2")
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta02")
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta02")
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta02")
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta03")
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta03")
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta03")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
implementation("androidx.room:room-runtime:2.4.0")

View File

@ -44,7 +44,7 @@ import dev.patrickgold.florisboard.ime.nlp.NlpManager
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.res.cache.CacheManager
import dev.patrickgold.florisboard.res.io.deleteContentsRecursively
import dev.patrickgold.jetpref.datastore.JetPrefManager
import dev.patrickgold.jetpref.datastore.JetPref
import java.io.File
import kotlin.Exception
@ -80,7 +80,7 @@ class FlorisApplication : Application() {
override fun onCreate() {
super.onCreate()
try {
JetPrefManager.init(saveIntervalMs = 1_000)
JetPref.configure(saveIntervalMs = 500)
Flog.install(
context = this,
isFloggingEnabled = BuildConfig.DEBUG,
@ -93,12 +93,12 @@ class FlorisApplication : Application() {
if (AndroidVersion.ATLEAST_API24_N && !UserManagerCompat.isUserUnlocked(this)) {
val context = createDeviceProtectedStorageContext()
initICU(context)
prefs.initializeForContext(context)
prefs.initializeBlocking(context)
registerReceiver(BootComplete(), IntentFilter(Intent.ACTION_USER_UNLOCKED))
} else {
initICU(this)
cacheDir?.deleteContentsRecursively()
prefs.initializeForContext(this)
prefs.initializeBlocking(this)
clipboardManager.value.initializeForContext(this)
}
@ -141,7 +141,7 @@ class FlorisApplication : Application() {
flogError { e.toString() }
}
cacheDir?.deleteContentsRecursively()
prefs.initializeForContext(this@FlorisApplication)
prefs.initializeBlocking(this@FlorisApplication)
clipboardManager.value.initializeForContext(this@FlorisApplication)
}
}

View File

@ -144,12 +144,6 @@ class FlorisAppActivity : ComponentActivity() {
}
}
override fun onDestroy() {
super.onDestroy()
prefs.forceSyncToDisk()
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AppContent() {

View File

@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.app.prefs
import android.os.Build
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
@ -33,11 +31,10 @@ import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
import dev.patrickgold.florisboard.util.VersionName
import dev.patrickgold.jetpref.datastore.JetPref
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
import dev.patrickgold.jetpref.datastore.preferenceModel
import java.time.LocalTime
fun florisPreferenceModel() = preferenceModel(AppPrefs::class, ::AppPrefs)
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
class AppPrefs : PreferenceModel("florisboard-app-prefs") {
val advanced = Advanced()
@ -555,15 +552,13 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
default = extCoreTheme("floris_night"),
serializer = ExtensionComponentName.Serializer,
)
@RequiresApi(Build.VERSION_CODES.O)
val sunriseTime = localTime(
key = "theme__sunrise_time",
default = LocalTime.of(6, 0),
)
@RequiresApi(Build.VERSION_CODES.O)
val sunsetTime = localTime(
key = "theme__sunset_time",
default = LocalTime.of(18, 0),
)
//val sunriseTime = localTime(
// key = "theme__sunrise_time",
// default = LocalTime.of(6, 0),
//)
//val sunsetTime = localTime(
// key = "theme__sunset_time",
// default = LocalTime.of(18, 0),
//)
}
}

View File

@ -46,15 +46,14 @@ import dev.patrickgold.florisboard.res.FileRegistry
import dev.patrickgold.florisboard.res.ZipUtils
import dev.patrickgold.florisboard.res.cache.CacheManager
import dev.patrickgold.florisboard.res.ext.ExtensionManager
import dev.patrickgold.florisboard.res.io.parentDir
import dev.patrickgold.florisboard.res.io.subDir
import dev.patrickgold.florisboard.res.io.subFile
import dev.patrickgold.florisboard.res.io.writeJson
import dev.patrickgold.jetpref.datastore.jetprefDatastoreDir
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object Backup {
const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.file"
const val METADATA_JSON_NAME = "backup_metadata.json"
@ -128,8 +127,8 @@ fun BackupScreen() = FlorisScreen {
fun prepareBackupWorkspace() {
val workspace = cacheManager.backupAndRestore.new()
if (backupFilesSelector.jetprefDatastore) {
context.filesDir.parentDir!!.subDir("jetpref_datastore").let { dir ->
dir.copyRecursively(workspace.inputDir.subDir("jetpref_datastore"))
context.jetprefDatastoreDir.let { dir ->
dir.copyRecursively(workspace.inputDir.subDir(dir.name))
}
}
val workspaceFilesDir = workspace.inputDir.subDir("files")
@ -225,31 +224,43 @@ fun BackupScreen() = FlorisScreen {
text = stringRes(R.string.backup_and_restore__back_up__destination_share_intent),
)
}
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
BackupFilesSelector(
filesSelector = backupFilesSelector,
title = stringRes(R.string.backup_and_restore__back_up__files),
) {
CheckboxListItem(
onClick = { backupFilesSelector.jetprefDatastore = !backupFilesSelector.jetprefDatastore },
checked = backupFilesSelector.jetprefDatastore,
text = stringRes(R.string.backup_and_restore__back_up__files_jetpref_datastore),
)
CheckboxListItem(
onClick = { backupFilesSelector.imeKeyboard = !backupFilesSelector.imeKeyboard },
checked = backupFilesSelector.imeKeyboard,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_keyboard),
)
CheckboxListItem(
onClick = { backupFilesSelector.imeSpelling = !backupFilesSelector.imeSpelling },
checked = backupFilesSelector.imeSpelling,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_spelling),
)
CheckboxListItem(
onClick = { backupFilesSelector.imeTheme = !backupFilesSelector.imeTheme },
checked = backupFilesSelector.imeTheme,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_theme),
)
}
)
}
}
@Composable
internal fun BackupFilesSelector(
modifier: Modifier = Modifier,
filesSelector: Backup.FilesSelector,
title: String,
) {
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
title = title,
) {
CheckboxListItem(
onClick = { filesSelector.jetprefDatastore = !filesSelector.jetprefDatastore },
checked = filesSelector.jetprefDatastore,
text = stringRes(R.string.backup_and_restore__back_up__files_jetpref_datastore),
)
CheckboxListItem(
onClick = { filesSelector.imeKeyboard = !filesSelector.imeKeyboard },
checked = filesSelector.imeKeyboard,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_keyboard),
)
CheckboxListItem(
onClick = { filesSelector.imeSpelling = !filesSelector.imeSpelling },
checked = filesSelector.imeSpelling,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_spelling),
)
CheckboxListItem(
onClick = { filesSelector.imeTheme = !filesSelector.imeTheme },
checked = filesSelector.imeTheme,
text = stringRes(R.string.backup_and_restore__back_up__files_ime_theme),
)
}
}

View File

@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -40,6 +41,7 @@ import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.components.CardDefaults
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
@ -53,8 +55,15 @@ import dev.patrickgold.florisboard.common.android.showLongToast
import dev.patrickgold.florisboard.res.FileRegistry
import dev.patrickgold.florisboard.res.ZipUtils
import dev.patrickgold.florisboard.res.cache.CacheManager
import dev.patrickgold.florisboard.res.ext.ExtensionManager
import dev.patrickgold.florisboard.res.io.deleteContentsRecursively
import dev.patrickgold.florisboard.res.io.readJson
import dev.patrickgold.florisboard.res.io.subDir
import dev.patrickgold.florisboard.res.io.subFile
import dev.patrickgold.jetpref.datastore.JetPref
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object Restore {
const val MIN_VERSION_CODE = 64
@ -76,7 +85,11 @@ fun RestoreScreen() = FlorisScreen {
val context = LocalContext.current
val cacheManager by context.cacheManager()
val restoreFilesSelector = remember { Backup.FilesSelector() }
var restoreMode by remember { mutableStateOf(Restore.Mode.MERGE) }
// TODO: rememberCoroutineScope() is unusable because it provides the scope in a cancelled state, which does
// not make sense at all. I suspect that this is a bug and once it is resolved we can use it here again.
val restoreScope = remember { CoroutineScope(Dispatchers.Main) }
var restoreWorkspace by remember {
mutableStateOf<CacheManager.BackupAndRestoreWorkspace?>(null)
}
@ -115,6 +128,52 @@ fun RestoreScreen() = FlorisScreen {
},
)
suspend fun performRestore() {
val prefs by florisPreferenceModel()
val workspace = restoreWorkspace!!
val shouldReset = restoreMode == Restore.Mode.ERASE_AND_OVERWRITE
if (restoreFilesSelector.jetprefDatastore) {
val datastoreFile = workspace.outputDir
.subDir(JetPref.JETPREF_DIR_NAME)
.subFile("${prefs.name}.${JetPref.JETPREF_FILE_EXT}")
if (datastoreFile.exists()) {
prefs.datastorePersistenceHandler?.loadPrefs(datastoreFile, shouldReset)
prefs.datastorePersistenceHandler?.persistPrefs()
}
}
val workspaceFilesDir = workspace.outputDir.subDir("files")
if (restoreFilesSelector.imeKeyboard) {
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH)
val dstDir = context.filesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH)
if (shouldReset) {
dstDir.deleteContentsRecursively()
}
if (srcDir.exists()) {
srcDir.copyRecursively(dstDir, overwrite = true)
}
}
if (restoreFilesSelector.imeSpelling) {
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
val dstDir = context.filesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
if (shouldReset) {
dstDir.deleteContentsRecursively()
}
if (srcDir.exists()) {
srcDir.copyRecursively(dstDir, overwrite = true)
}
}
if (restoreFilesSelector.imeTheme) {
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_THEME_PATH)
val dstDir = context.filesDir.subDir(ExtensionManager.IME_THEME_PATH)
if (shouldReset) {
dstDir.deleteContentsRecursively()
}
if (srcDir.exists()) {
srcDir.copyRecursively(dstDir, overwrite = true)
}
}
}
bottomBar {
FlorisButtonBar {
ButtonBarSpacer()
@ -127,7 +186,15 @@ fun RestoreScreen() = FlorisScreen {
)
ButtonBarButton(
onClick = {
//
restoreScope.launch(Dispatchers.Main) {
try {
performRestore()
context.showLongToast(R.string.backup_and_restore__restore__success)
navController.popBackStack()
} catch (e: Throwable) {
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to e.localizedMessage)
}
}
},
text = stringRes(R.string.action__restore),
enabled = restoreWorkspace != null && restoreWorkspace?.restoreErrorId == null,
@ -155,23 +222,23 @@ fun RestoreScreen() = FlorisScreen {
text = stringRes(R.string.backup_and_restore__restore__mode_erase_and_overwrite),
)
}
FlorisOutlinedButton(
onClick = {
runCatching {
restoreDataFromFileSystemLauncher.launch(
FileRegistry.BackupArchive.mediaType
)
}.onFailure { error ->
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
}
},
modifier = Modifier
.padding(vertical = 16.dp)
.align(Alignment.CenterHorizontally),
text = stringRes(R.string.action__select_file),
)
val workspace = restoreWorkspace
if (workspace == null) {
FlorisOutlinedButton(
onClick = {
runCatching {
restoreDataFromFileSystemLauncher.launch(
FileRegistry.BackupArchive.mediaType
)
}.onFailure { error ->
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
}
},
modifier = Modifier
.padding(vertical = 16.dp)
.align(Alignment.CenterHorizontally),
text = stringRes(R.string.action__select_file),
)
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
@ -224,6 +291,12 @@ fun RestoreScreen() = FlorisScreen {
)
}
}
if (workspace.restoreErrorId == null) {
BackupFilesSelector(
filesSelector = restoreFilesSelector,
title = stringRes(R.string.backup_and_restore__restore__files),
)
}
}
}
}

View File

@ -30,11 +30,25 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.prefs.AppPrefs
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
import dev.patrickgold.florisboard.debug.*
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
private class SafePreferenceInstanceWrapper : ReadOnlyProperty<Any?, AppPrefs?> {
val cachedPreferenceModel = try {
florisPreferenceModel()
} catch (_: Throwable) {
null
}
override fun getValue(thisRef: Any?, property: KProperty<*>): AppPrefs? {
return cachedPreferenceModel?.getValue(thisRef, property)
}
}
class CrashDialogActivity : ComponentActivity() {
private var stacktraces: List<CrashUtility.Stacktrace> = listOf()
private var errorReport: StringBuilder = StringBuilder()
private var prefs: AppPrefs? = null
private val prefs by SafePreferenceInstanceWrapper()
private val stacktrace by lazy { findViewById<TextView>(R.id.stacktrace) }
private val reportInstructions by lazy { findViewById<TextView>(R.id.report_instructions) }
@ -48,13 +62,6 @@ class CrashDialogActivity : ComponentActivity() {
val layout = layoutInflater.inflate(R.layout.crash_dialog, null)
setContentView(layout)
// We secure the PrefHelper usage here because the PrefHelper could potentially be the
// source of the crash, thus making the crash dialog unusable if not wrapped.
try {
prefs = florisPreferenceModel().preferenceModel
} catch (_: Exception) {
}
stacktraces = CrashUtility.getUnhandledStacktraces(this)
errorReport.apply {
appendLine("#### Environment information")

View File

@ -38,7 +38,6 @@ import androidx.lifecycle.MutableLiveData
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.common.android.AndroidVersion
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.res.ZipUtils
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
@ -53,7 +52,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import java.time.LocalTime
import kotlin.properties.Delegates
/**
@ -235,18 +233,18 @@ class ThemeManager(context: Context) {
prefs.theme.dayThemeId.get()
}
ThemeMode.FOLLOW_TIME -> {
if (AndroidVersion.ATLEAST_API26_O) {
val current = LocalTime.now()
val sunrise = prefs.theme.sunriseTime.get()
val sunset = prefs.theme.sunsetTime.get()
if (current in sunrise..sunset) {
prefs.theme.dayThemeId.get()
} else {
prefs.theme.nightThemeId.get()
}
} else {
//if (AndroidVersion.ATLEAST_API26_O) {
// val current = LocalTime.now()
// val sunrise = prefs.theme.sunriseTime.get()
// val sunset = prefs.theme.sunsetTime.get()
// if (current in sunrise..sunset) {
// prefs.theme.dayThemeId.get()
// } else {
// prefs.theme.nightThemeId.get()
// }
//} else {
prefs.theme.nightThemeId.get()
}
//}
}
}
}

View File

@ -430,7 +430,8 @@
<string name="backup_and_restore__back_up__failure">Failed to export backup archive: {error_message}</string>
<string name="backup_and_restore__restore__title">Restore data</string>
<string name="backup_and_restore__restore__summary">Restore preferences and customizations from a backup archive</string>
<string name="backup_and_restore__restore__metadata">Backup archive metadata</string>
<string name="backup_and_restore__restore__files">Select what to restore</string>
<string name="backup_and_restore__restore__metadata">Selected backup archive</string>
<string name="backup_and_restore__restore__metadata_warn_different_version">This backup archive was generated in another version than current, which is generally supported. Be aware though that minor issues can occur or some preferences may not get transferred properly due to feature differences.</string>
<string name="backup_and_restore__restore__metadata_warn_different_vendor">This backup archive was generated in a third-party app, which is generally not supported. Data losses may occur, restore at your own risk!</string>
<string name="backup_and_restore__restore__metadata_error_invalid_metadata">This backup archive contains invalid metadata. Either it has been corrupted or poorly modified. Restoring from this archive is not possible, please select another one.</string>