From 72024aa44af1622fd148b859c8cea0c3404dc370 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 8 Oct 2023 10:40:58 -0400 Subject: [PATCH] Add app settings to backups This should be compatible with Aniyomi's implementation. Related to #1857 Co-authored-by: jmir1 --- .../settings/screen/SettingsBackupScreen.kt | 1 + .../tachiyomi/data/backup/BackupConst.kt | 9 +++- .../tachiyomi/data/backup/BackupManager.kt | 37 +++++++++++-- .../tachiyomi/data/backup/BackupRestorer.kt | 53 ++++++++++++++++++- .../tachiyomi/data/backup/models/Backup.kt | 2 +- .../data/backup/models/BackupCategory.kt | 1 - .../data/backup/models/BackupPreference.kt | 31 +++++++++++ .../core/preference/AndroidPreference.kt | 9 +++- .../core/preference/AndroidPreferenceStore.kt | 4 ++ .../core/preference/PreferenceStore.kt | 2 + i18n/src/main/res/values/strings.xml | 1 + 11 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt index ad2a003802..a5adfb75da 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt @@ -147,6 +147,7 @@ object SettingsBackupScreen : SearchableSettings { BackupConst.BACKUP_CHAPTER to R.string.chapters, BackupConst.BACKUP_TRACK to R.string.track, BackupConst.BACKUP_HISTORY to R.string.history, + BackupConst.BACKUP_APP_PREFS to R.string.app_settings, ) } val flags = remember { choices.keys.toMutableStateList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt index add7b3813d..a137046e4d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt @@ -4,11 +4,18 @@ package eu.kanade.tachiyomi.data.backup internal object BackupConst { const val BACKUP_CATEGORY = 0x1 const val BACKUP_CATEGORY_MASK = 0x1 + const val BACKUP_CHAPTER = 0x2 const val BACKUP_CHAPTER_MASK = 0x2 + const val BACKUP_HISTORY = 0x4 const val BACKUP_HISTORY_MASK = 0x4 + const val BACKUP_TRACK = 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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 67bb74e97a..9418781090 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -6,6 +6,8 @@ import android.net.Uri import com.hippo.unifile.UniFile import eu.kanade.domain.chapter.model.copyFrom 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_MASK 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.BackupHistory 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.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.backupChapterMapper import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper @@ -30,6 +39,7 @@ import logcat.LogPriority import okio.buffer import okio.gzip import okio.sink +import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.util.system.logcat import tachiyomi.data.DatabaseHandler import tachiyomi.data.Manga_sync @@ -61,6 +71,7 @@ class BackupManager( private val getCategories: GetCategories = Injekt.get() private val getFavorites: GetFavorites = Injekt.get() private val getHistory: GetHistory = Injekt.get() + private val preferenceStore: PreferenceStore = Injekt.get() internal val parser = ProtoBuf @@ -81,6 +92,7 @@ class BackupManager( backupCategories(flags), emptyList(), prepExtensionInfoForSync(databaseManga), + backupAppPreferences(flags), ) var file: UniFile? = null @@ -133,7 +145,7 @@ class BackupManager( } } - fun prepExtensionInfoForSync(mangas: List): List { + private fun prepExtensionInfoForSync(mangas: List): List { return mangas .asSequence() .map(Manga::source) @@ -148,7 +160,7 @@ class BackupManager( * * @return list of [BackupCategory] to be backed up */ - suspend fun backupCategories(options: Int): List { + private suspend fun backupCategories(options: Int): List { // Check if user wants category information in backup return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { getCategories.await() @@ -159,7 +171,7 @@ class BackupManager( } } - suspend fun backupMangas(mangas: List, flags: Int): List { + private suspend fun backupMangas(mangas: List, flags: Int): List { return mangas.map { backupManga(it, flags) } @@ -219,6 +231,25 @@ class BackupManager( return mangaObject } + @Suppress("UNCHECKED_CAST") + private fun backupAppPreferences(flags: Int): List { + 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)?.let { + BackupPreference(key, StringSetPreferenceValue(it)) + } + else -> null + } + } + } + internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { var updatedManga = manga.copy(id = dbManga._id) updatedManga = updatedManga.copyFrom(dbManga) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 765a700c12..1c0558b4d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -7,13 +7,20 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupHistory 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.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.system.createFileInCacheDir import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive +import tachiyomi.core.preference.PreferenceStore import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track @@ -30,8 +37,8 @@ class BackupRestorer( private val notifier: BackupNotifier, ) { private val updateManga: UpdateManga = Injekt.get() - private val chapterRepository: ChapterRepository = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get() + private val preferenceStore: PreferenceStore = Injekt.get() private var now = ZonedDateTime.now() private var currentFetchWindow = fetchInterval.getWindow(now) @@ -106,6 +113,8 @@ class BackupRestorer( currentFetchWindow = fetchInterval.getWindow(now) return coroutineScope { + restoreAppPreferences(backup.backupPreferences) + // Restore individual manga backup.backupManga.forEach { if (!isActive) { @@ -115,6 +124,7 @@ class BackupRestorer( restoreManga(it, backup.backupCategories, sync) } // TODO: optionally trigger online library + tracker update + true } } @@ -200,6 +210,45 @@ class BackupRestorer( backupManager.restoreTracking(manga, tracks) } + private fun restoreAppPreferences(preferences: List) { + 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] * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index 14d4dd0ae2..db9047f0da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -11,9 +11,9 @@ import java.util.Locale data class Backup( @ProtoNumber(1) val backupManga: List, @ProtoNumber(2) var backupCategories: List = emptyList(), - // Bump by 100 to specify this is a 0.x value @ProtoNumber(100) var backupBrokenSources: List = emptyList(), @ProtoNumber(101) var backupSources: List = emptyList(), + @ProtoNumber(104) var backupPreferences: List = emptyList(), ) { companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt index 8d93a0b32d..c27c86feca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt @@ -9,7 +9,6 @@ class BackupCategory( @ProtoNumber(1) var name: String, @ProtoNumber(2) var order: Long = 0, // @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, ) { fun getCategory(): Category { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt new file mode 100644 index 0000000000..791c9706f1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt @@ -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) : PreferenceValue() diff --git a/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt b/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt index a0f2f335c6..21f5b1d309 100644 --- a/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt +++ b/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import tachiyomi.core.util.system.logcat sealed class AndroidPreference( private val preferences: SharedPreferences, @@ -29,7 +30,13 @@ sealed class AndroidPreference( } 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) { diff --git a/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt b/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt index c59652da5f..67e4db7fcd 100644 --- a/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt +++ b/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt @@ -60,6 +60,10 @@ class AndroidPreferenceStore( deserializer = deserializer, ) } + + override fun getAll(): Map { + return sharedPreferences.all ?: emptyMap() + } } private val SharedPreferences.keyFlow diff --git a/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt b/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt index f8cc9f8905..5b0e9da9ba 100644 --- a/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt +++ b/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt @@ -20,6 +20,8 @@ interface PreferenceStore { serializer: (T) -> String, deserializer: (String) -> T, ): Preference + + fun getAll(): Map } inline fun > PreferenceStore.getEnum( diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 8821087b2b..410ec9ec2a 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -493,6 +493,7 @@ Backup is already in progress What do you want to backup? + App settings Creating backup Backup failed Storage permissions not granted