Migrate to XML Settings from Preferences (#722)

* Migrate to XML Settings from Preferences

* Lint
This commit is contained in:
Mitchell Syer
2023-10-29 11:01:46 -04:00
committed by GitHub
parent 60015bc041
commit 583a2f0fad
6 changed files with 126 additions and 31 deletions

View File

@@ -9,7 +9,8 @@ package xyz.nulldev.androidcompat.io.sharedprefs
import android.content.SharedPreferences import android.content.SharedPreferences
import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.PreferencesSettings import com.russhwolf.settings.PropertiesSettings
import com.russhwolf.settings.Settings
import com.russhwolf.settings.serialization.decodeValue import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue import com.russhwolf.settings.serialization.encodeValue
@@ -17,14 +18,52 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import java.util.prefs.PreferenceChangeListener import mu.KotlinLogging
import java.util.prefs.Preferences import xyz.nulldev.ts.config.ApplicationRootDir
import java.util.Properties
import kotlin.io.path.Path
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteIfExists
import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.outputStream
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) @OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences { class JavaSharedPreferences(key: String) : SharedPreferences {
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key") companion object {
private val preferences = PreferencesSettings(javaPreferences) private val logger = KotlinLogging.logger {}
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>() }
private val file = Path(ApplicationRootDir, "settings", "$key.xml")
private val properties =
Properties().also { properties ->
try {
if (file.exists()) {
file.inputStream().use { properties.loadFromXML(it) }
}
} catch (e: Exception) {
logger.error(e) { "Error loading settings from $key" }
}
}
private val preferences =
PropertiesSettings(
properties,
onModify = { properties ->
try {
if (properties.isEmpty) {
file.deleteIfExists()
} else {
file.createParentDirectories()
file.outputStream().use {
properties.storeToXML(it, null)
}
}
} catch (e: Exception) {
logger.error(e) { "Error saving settings in $key" }
}
},
)
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, (String) -> Unit>()
// TODO: 2021-05-29 Need to find a way to get this working with all pref types // TODO: 2021-05-29 Need to find a way to get this working with all pref types
override fun getAll(): MutableMap<String, *> { override fun getAll(): MutableMap<String, *> {
@@ -90,17 +129,21 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
override fun edit(): SharedPreferences.Editor { override fun edit(): SharedPreferences.Editor {
return Editor(preferences) return Editor(preferences) { key ->
listeners.forEach { (_, listener) ->
listener(key)
}
}
} }
class Editor(private val preferences: PreferencesSettings) : SharedPreferences.Editor { class Editor(private val preferences: Settings, private val notify: (String) -> Unit) : SharedPreferences.Editor {
private val actions = mutableListOf<Action>() private val actions = mutableListOf<Action>()
private sealed class Action { private sealed class Action {
data class Add(val key: String, val value: Any) : Action() data class Add(val key: String, val value: Any) : Action()
data class Remove(val key: String) : Action() data class Remove(val key: String) : Action()
object Clear : Action() data object Clear : Action()
} }
override fun putString( override fun putString(
@@ -182,7 +225,7 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
actions.forEach { actions.forEach {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
when (it) { when (it) {
is Action.Add -> is Action.Add -> {
when (val value = it.value) { when (val value = it.value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>) is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>)
is String -> preferences.putString(it.key, value) is String -> preferences.putString(it.key, value)
@@ -192,6 +235,8 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
is Double -> preferences.putDouble(it.key, value) is Double -> preferences.putDouble(it.key, value)
is Boolean -> preferences.putBoolean(it.key, value) is Boolean -> preferences.putBoolean(it.key, value)
} }
notify(it.key)
}
is Action.Remove -> { is Action.Remove -> {
preferences.remove(it.key) preferences.remove(it.key)
/** /**
@@ -205,6 +250,8 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
preferences.remove(key) preferences.remove(key)
} }
} }
notify(it.key)
} }
Action.Clear -> preferences.clear() Action.Clear -> preferences.clear()
} }
@@ -213,23 +260,18 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
val javaListener = val javaListener: (String) -> Unit = {
PreferenceChangeListener { listener.onSharedPreferenceChanged(this, it)
listener.onSharedPreferenceChanged(this, it.key)
} }
listeners[listener] = javaListener listeners[listener] = javaListener
javaPreferences.addPreferenceChangeListener(javaListener)
} }
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
val registeredListener = listeners.remove(listener) listeners.remove(listener)
if (registeredListener != null) {
javaPreferences.removePreferenceChangeListener(registeredListener)
}
} }
fun deleteAll(): Boolean { fun deleteAll(): Boolean {
javaPreferences.removeNode() preferences.clear()
return true return true
} }
} }

View File

@@ -7,6 +7,8 @@ package suwayomi.tachidesk.manga.impl.backup.proto
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import mu.KotlinLogging import mu.KotlinLogging
@@ -38,13 +40,14 @@ import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler import suwayomi.tachidesk.util.HAScheduler
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.prefs.Preferences
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
object ProtoBackupExport : ProtoBackupBase() { object ProtoBackupExport : ProtoBackupBase() {
@@ -52,7 +55,7 @@ object ProtoBackupExport : ProtoBackupBase() {
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private var backupSchedulerJobId: String = "" private var backupSchedulerJobId: String = ""
private const val LAST_AUTOMATED_BACKUP_KEY = "lastAutomatedBackupKey" private const val LAST_AUTOMATED_BACKUP_KEY = "lastAutomatedBackupKey"
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java) private val preferences = Injekt.get<Application>().getSharedPreferences("manga/impl/backup/proto", Context.MODE_PRIVATE)
init { init {
serverConfig.subscribeTo( serverConfig.subscribeTo(
@@ -77,7 +80,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val task = { val task = {
cleanupAutomatedBackups() cleanupAutomatedBackups()
createAutomatedBackup() createAutomatedBackup()
preferences.putLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis()) preferences.edit().putLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis()).apply()
} }
val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() } val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() }

View File

@@ -64,7 +64,7 @@ object DownloadManager {
Injekt.get<Application>().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE) Injekt.get<Application>().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE)
private fun loadDownloadQueue(): List<Int> { private fun loadDownloadQueue(): List<Int> {
return sharedPreferences.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())?.mapNotNull { it.toInt() } ?: emptyList() return sharedPreferences.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())?.mapNotNull { it.toInt() }.orEmpty()
} }
private fun saveDownloadQueue() { private fun saveDownloadQueue() {

View File

@@ -1,5 +1,7 @@
package suwayomi.tachidesk.manga.impl.update package suwayomi.tachidesk.manga.impl.update
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -27,9 +29,10 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler import suwayomi.tachidesk.util.HAScheduler
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.prefs.Preferences
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
@@ -48,7 +51,7 @@ class Updater : IUpdater {
private val lastUpdateKey = "lastUpdateKey" private val lastUpdateKey = "lastUpdateKey"
private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey" private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey"
private val preferences = Preferences.userNodeForPackage(Updater::class.java) private val preferences = Injekt.get<Application>().getSharedPreferences("manga/impl/update", Context.MODE_PRIVATE)
private var currentUpdateTaskId = "" private var currentUpdateTaskId = ""
@@ -80,7 +83,7 @@ class Updater : IUpdater {
private fun autoUpdateTask() { private fun autoUpdateTask() {
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis()) preferences.edit().putLong(lastAutomatedUpdateKey, System.currentTimeMillis()).apply()
if (status.value.running) { if (status.value.running) {
logger.debug { "Global update is already in progress" } logger.debug { "Global update is already in progress" }
@@ -178,7 +181,7 @@ class Updater : IUpdater {
clear: Boolean?, clear: Boolean?,
forceAll: Boolean, forceAll: Boolean,
) { ) {
preferences.putLong(lastUpdateKey, System.currentTimeMillis()) preferences.edit().putLong(lastUpdateKey, System.currentTimeMillis()).apply()
if (clear == true) { if (clear == true) {
reset() reset()

View File

@@ -7,6 +7,8 @@ package suwayomi.tachidesk.server
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
import ch.qos.logback.classic.Level import ch.qos.logback.classic.Level
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
@@ -31,6 +33,8 @@ import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.SystemTray import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ApplicationRootDir
@@ -42,6 +46,9 @@ import xyz.nulldev.ts.config.setLogLevelFor
import java.io.File import java.io.File
import java.security.Security import java.security.Security
import java.util.Locale import java.util.Locale
import java.util.prefs.Preferences
import kotlin.io.path.exists
import kotlin.io.path.outputStream
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -211,6 +218,10 @@ fun applicationSetup() {
} }
}, ignoreInitialValue = false) }, ignoreInitialValue = false)
val preferences = Preferences.userRoot().node("suwayomi/tachidesk")
migratePreferences(null, preferences)
preferences.removeNode()
// Disable jetty's logging // Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false") System.setProperty("org.eclipse.jetty.util.log.announce", "false")
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog") System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
@@ -250,3 +261,36 @@ fun applicationSetup() {
// start DownloadManager and restore + resume downloads // start DownloadManager and restore + resume downloads
DownloadManager.restoreAndResumeDownloads() DownloadManager.restoreAndResumeDownloads()
} }
fun migratePreferences(
parent: String?,
rootNode: Preferences,
) {
val subNodes = rootNode.childrenNames()
for (subNodeName in subNodes) {
val subNode = rootNode.node(subNodeName)
val key =
if (parent != null) {
"$parent/$subNodeName"
} else {
subNodeName
}
val preferences = Injekt.get<Application>().getSharedPreferences(key, Context.MODE_PRIVATE)
val items: Map<String, String?> =
subNode.keys().associateWith {
subNode[it, null]?.ifBlank { null }
}
preferences.edit().apply {
items.forEach { (key, value) ->
if (value != null) {
putString(key, value)
}
}
}.apply()
migratePreferences(key, subNode) // Recursively migrate sub-level nodes
}
}

View File

@@ -7,6 +7,8 @@ package suwayomi.tachidesk.server.util
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
@@ -45,6 +47,8 @@ import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler import suwayomi.tachidesk.util.HAScheduler
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@@ -53,7 +57,6 @@ import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Date import java.util.Date
import java.util.prefs.Preferences
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -124,7 +127,7 @@ object WebInterfaceManager {
private const val LAST_WEBUI_UPDATE_CHECK_KEY = "lastWebUIUpdateCheckKey" private const val LAST_WEBUI_UPDATE_CHECK_KEY = "lastWebUIUpdateCheckKey"
private val preferences = Preferences.userNodeForPackage(WebInterfaceManager::class.java) private val preferences = Injekt.get<Application>().getSharedPreferences("server/util", Context.MODE_PRIVATE)
private var currentUpdateTaskId: String = "" private var currentUpdateTaskId: String = ""
private val json: Json by injectLazy() private val json: Json by injectLazy()
@@ -326,7 +329,7 @@ object WebInterfaceManager {
} }
private suspend fun checkForUpdate() { private suspend fun checkForUpdate() {
preferences.putLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis()) preferences.edit().putLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis()).apply()
val localVersion = getLocalVersion() val localVersion = getLocalVersion()
if (!isUpdateAvailable(localVersion).second) { if (!isUpdateAvailable(localVersion).second) {