Add app settings to backups

This should be compatible with Aniyomi's implementation.
Related to #1857

Co-authored-by: jmir1 <jmir1@users.noreply.github.com>
This commit is contained in:
arkon 2023-10-08 10:40:58 -04:00
parent 9c688b08c0
commit 72024aa44a
11 changed files with 141 additions and 9 deletions

View file

@ -147,6 +147,7 @@ object SettingsBackupScreen : SearchableSettings {
BackupConst.BACKUP_CHAPTER to R.string.chapters, BackupConst.BACKUP_CHAPTER to R.string.chapters,
BackupConst.BACKUP_TRACK to R.string.track, BackupConst.BACKUP_TRACK to R.string.track,
BackupConst.BACKUP_HISTORY to R.string.history, BackupConst.BACKUP_HISTORY to R.string.history,
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
) )
} }
val flags = remember { choices.keys.toMutableStateList() } val flags = remember { choices.keys.toMutableStateList() }

View file

@ -4,11 +4,18 @@ package eu.kanade.tachiyomi.data.backup
internal object BackupConst { internal object BackupConst {
const val BACKUP_CATEGORY = 0x1 const val BACKUP_CATEGORY = 0x1
const val BACKUP_CATEGORY_MASK = 0x1 const val BACKUP_CATEGORY_MASK = 0x1
const val BACKUP_CHAPTER = 0x2 const val BACKUP_CHAPTER = 0x2
const val BACKUP_CHAPTER_MASK = 0x2 const val BACKUP_CHAPTER_MASK = 0x2
const val BACKUP_HISTORY = 0x4 const val BACKUP_HISTORY = 0x4
const val BACKUP_HISTORY_MASK = 0x4 const val BACKUP_HISTORY_MASK = 0x4
const val BACKUP_TRACK = 0x8 const val BACKUP_TRACK = 0x8
const val BACKUP_TRACK_MASK = 0x8 const val BACKUP_TRACK_MASK = 0x8
const val BACKUP_ALL = 0xF
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_APP_PREFS_MASK = 0x10
const val BACKUP_ALL = 0x1F
} }

View file

@ -6,6 +6,8 @@ import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.chapter.model.copyFrom import eu.kanade.domain.chapter.model.copyFrom
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
@ -18,8 +20,15 @@ import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
@ -30,6 +39,7 @@ import logcat.LogPriority
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.sink import okio.sink
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Manga_sync import tachiyomi.data.Manga_sync
@ -61,6 +71,7 @@ class BackupManager(
private val getCategories: GetCategories = Injekt.get() private val getCategories: GetCategories = Injekt.get()
private val getFavorites: GetFavorites = Injekt.get() private val getFavorites: GetFavorites = Injekt.get()
private val getHistory: GetHistory = Injekt.get() private val getHistory: GetHistory = Injekt.get()
private val preferenceStore: PreferenceStore = Injekt.get()
internal val parser = ProtoBuf internal val parser = ProtoBuf
@ -81,6 +92,7 @@ class BackupManager(
backupCategories(flags), backupCategories(flags),
emptyList(), emptyList(),
prepExtensionInfoForSync(databaseManga), prepExtensionInfoForSync(databaseManga),
backupAppPreferences(flags),
) )
var file: UniFile? = null var file: UniFile? = null
@ -133,7 +145,7 @@ class BackupManager(
} }
} }
fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> { private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
return mangas return mangas
.asSequence() .asSequence()
.map(Manga::source) .map(Manga::source)
@ -148,7 +160,7 @@ class BackupManager(
* *
* @return list of [BackupCategory] to be backed up * @return list of [BackupCategory] to be backed up
*/ */
suspend fun backupCategories(options: Int): List<BackupCategory> { private suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup // Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
getCategories.await() getCategories.await()
@ -159,7 +171,7 @@ class BackupManager(
} }
} }
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> { private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
return mangas.map { return mangas.map {
backupManga(it, flags) backupManga(it, flags)
} }
@ -219,6 +231,25 @@ class BackupManager(
return mangaObject return mangaObject
} }
@Suppress("UNCHECKED_CAST")
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
return preferenceStore.getAll().mapNotNull { (key, value) ->
when (value) {
is Int -> BackupPreference(key, IntPreferenceValue(value))
is Long -> BackupPreference(key, LongPreferenceValue(value))
is Float -> BackupPreference(key, FloatPreferenceValue(value))
is String -> BackupPreference(key, StringPreferenceValue(value))
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
is Set<*> -> (value as? Set<String>)?.let {
BackupPreference(key, StringSetPreferenceValue(it))
}
else -> null
}
}
}
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
var updatedManga = manga.copy(id = dbManga._id) var updatedManga = manga.copy(id = dbManga._id)
updatedManga = updatedManga.copyFrom(dbManga) updatedManga = updatedManga.copyFrom(dbManga)

View file

@ -7,13 +7,20 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.BackupUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
@ -30,8 +37,8 @@ class BackupRestorer(
private val notifier: BackupNotifier, private val notifier: BackupNotifier,
) { ) {
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val chapterRepository: ChapterRepository = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get()
private val preferenceStore: PreferenceStore = Injekt.get()
private var now = ZonedDateTime.now() private var now = ZonedDateTime.now()
private var currentFetchWindow = fetchInterval.getWindow(now) private var currentFetchWindow = fetchInterval.getWindow(now)
@ -106,6 +113,8 @@ class BackupRestorer(
currentFetchWindow = fetchInterval.getWindow(now) currentFetchWindow = fetchInterval.getWindow(now)
return coroutineScope { return coroutineScope {
restoreAppPreferences(backup.backupPreferences)
// Restore individual manga // Restore individual manga
backup.backupManga.forEach { backup.backupManga.forEach {
if (!isActive) { if (!isActive) {
@ -115,6 +124,7 @@ class BackupRestorer(
restoreManga(it, backup.backupCategories, sync) restoreManga(it, backup.backupCategories, sync)
} }
// TODO: optionally trigger online library + tracker update // TODO: optionally trigger online library + tracker update
true true
} }
} }
@ -200,6 +210,45 @@ class BackupRestorer(
backupManager.restoreTracking(manga, tracks) backupManager.restoreTracking(manga, tracks)
} }
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
val prefs = preferenceStore.getAll()
preferences.forEach { (key, value) ->
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
preferenceStore.getInt(key).set(value.value)
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
preferenceStore.getStringSet(key).set(value.value)
}
}
}
}
}
/** /**
* Called to update dialog in [BackupConst] * Called to update dialog in [BackupConst]
* *

View file

@ -11,9 +11,9 @@ import java.util.Locale
data class Backup( data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>, @ProtoNumber(1) val backupManga: List<BackupManga>,
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(), @ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(), @ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(), @ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
) { ) {
companion object { companion object {

View file

@ -9,7 +9,6 @@ class BackupCategory(
@ProtoNumber(1) var name: String, @ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0, @ProtoNumber(2) var order: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Long = 0, @ProtoNumber(100) var flags: Long = 0,
) { ) {
fun getCategory(): Category { fun getCategory(): Category {

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupPreference(
@ProtoNumber(1) val key: String,
@ProtoNumber(2) val value: PreferenceValue,
)
@Serializable
sealed class PreferenceValue
@Serializable
data class IntPreferenceValue(val value: Int) : PreferenceValue()
@Serializable
data class LongPreferenceValue(val value: Long) : PreferenceValue()
@Serializable
data class FloatPreferenceValue(val value: Float) : PreferenceValue()
@Serializable
data class StringPreferenceValue(val value: String) : PreferenceValue()
@Serializable
data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue()
@Serializable
data class StringSetPreferenceValue(val value: Set<String>) : PreferenceValue()

View file

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.util.system.logcat
sealed class AndroidPreference<T>( sealed class AndroidPreference<T>(
private val preferences: SharedPreferences, private val preferences: SharedPreferences,
@ -29,7 +30,13 @@ sealed class AndroidPreference<T>(
} }
override fun get(): T { override fun get(): T {
return read(preferences, key, defaultValue) return try {
read(preferences, key, defaultValue)
} catch (e: ClassCastException) {
logcat { "Invalid value for $key; deleting" }
delete()
defaultValue
}
} }
override fun set(value: T) { override fun set(value: T) {

View file

@ -60,6 +60,10 @@ class AndroidPreferenceStore(
deserializer = deserializer, deserializer = deserializer,
) )
} }
override fun getAll(): Map<String, *> {
return sharedPreferences.all ?: emptyMap<String, Any>()
}
} }
private val SharedPreferences.keyFlow private val SharedPreferences.keyFlow

View file

@ -20,6 +20,8 @@ interface PreferenceStore {
serializer: (T) -> String, serializer: (T) -> String,
deserializer: (String) -> T, deserializer: (String) -> T,
): Preference<T> ): Preference<T>
fun getAll(): Map<String, *>
} }
inline fun <reified T : Enum<T>> PreferenceStore.getEnum( inline fun <reified T : Enum<T>> PreferenceStore.getEnum(

View file

@ -493,6 +493,7 @@
</plurals> </plurals>
<string name="backup_in_progress">Backup is already in progress</string> <string name="backup_in_progress">Backup is already in progress</string>
<string name="backup_choice">What do you want to backup?</string> <string name="backup_choice">What do you want to backup?</string>
<string name="app_settings">App settings</string>
<string name="creating_backup">Creating backup</string> <string name="creating_backup">Creating backup</string>
<string name="creating_backup_error">Backup failed</string> <string name="creating_backup_error">Backup failed</string>
<string name="missing_storage_permission">Storage permissions not granted</string> <string name="missing_storage_permission">Storage permissions not granted</string>