diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/io/sharedprefs/JavaSharedPreferences.kt b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/io/sharedprefs/JavaSharedPreferences.kt index b0cac7a2c..ea480af85 100644 --- a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/io/sharedprefs/JavaSharedPreferences.kt +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/io/sharedprefs/JavaSharedPreferences.kt @@ -9,7 +9,8 @@ package xyz.nulldev.androidcompat.io.sharedprefs import android.content.SharedPreferences 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.decodeValueOrNull import com.russhwolf.settings.serialization.encodeValue @@ -17,14 +18,52 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer -import java.util.prefs.PreferenceChangeListener -import java.util.prefs.Preferences +import mu.KotlinLogging +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) class JavaSharedPreferences(key: String) : SharedPreferences { - private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key") - private val preferences = PreferencesSettings(javaPreferences) - private val listeners = mutableMapOf() + companion object { + private val logger = KotlinLogging.logger {} + } + + 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 Unit>() // TODO: 2021-05-29 Need to find a way to get this working with all pref types override fun getAll(): MutableMap { @@ -90,17 +129,21 @@ class JavaSharedPreferences(key: String) : SharedPreferences { } 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() private sealed class Action { data class Add(val key: String, val value: Any) : Action() data class Remove(val key: String) : Action() - object Clear : Action() + data object Clear : Action() } override fun putString( @@ -182,7 +225,7 @@ class JavaSharedPreferences(key: String) : SharedPreferences { actions.forEach { @Suppress("UNCHECKED_CAST") when (it) { - is Action.Add -> + is Action.Add -> { when (val value = it.value) { is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set) is String -> preferences.putString(it.key, value) @@ -192,6 +235,8 @@ class JavaSharedPreferences(key: String) : SharedPreferences { is Double -> preferences.putDouble(it.key, value) is Boolean -> preferences.putBoolean(it.key, value) } + notify(it.key) + } is Action.Remove -> { preferences.remove(it.key) /** @@ -205,6 +250,8 @@ class JavaSharedPreferences(key: String) : SharedPreferences { preferences.remove(key) } } + + notify(it.key) } Action.Clear -> preferences.clear() } @@ -213,23 +260,18 @@ class JavaSharedPreferences(key: String) : SharedPreferences { } override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - val javaListener = - PreferenceChangeListener { - listener.onSharedPreferenceChanged(this, it.key) - } + val javaListener: (String) -> Unit = { + listener.onSharedPreferenceChanged(this, it) + } listeners[listener] = javaListener - javaPreferences.addPreferenceChangeListener(javaListener) } override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { - val registeredListener = listeners.remove(listener) - if (registeredListener != null) { - javaPreferences.removePreferenceChangeListener(registeredListener) - } + listeners.remove(listener) } fun deleteAll(): Boolean { - javaPreferences.removeNode() + preferences.clear() return true } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt index 14a7d9569..37c000473 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -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 * 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 kotlinx.coroutines.flow.combine import mu.KotlinLogging @@ -38,13 +40,14 @@ import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.util.HAScheduler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date import java.util.concurrent.TimeUnit -import java.util.prefs.Preferences import kotlin.time.Duration.Companion.days object ProtoBackupExport : ProtoBackupBase() { @@ -52,7 +55,7 @@ object ProtoBackupExport : ProtoBackupBase() { private val applicationDirs by DI.global.instance() private var backupSchedulerJobId: String = "" private const val LAST_AUTOMATED_BACKUP_KEY = "lastAutomatedBackupKey" - private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java) + private val preferences = Injekt.get().getSharedPreferences("manga/impl/backup/proto", Context.MODE_PRIVATE) init { serverConfig.subscribeTo( @@ -77,7 +80,7 @@ object ProtoBackupExport : ProtoBackupBase() { val task = { cleanupAutomatedBackups() 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() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt index 4487ced5c..b20dc67ff 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt @@ -64,7 +64,7 @@ object DownloadManager { Injekt.get().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE) private fun loadDownloadQueue(): List { - 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() { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 7cd820fd8..32ec00a5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -1,5 +1,7 @@ package suwayomi.tachidesk.manga.impl.update +import android.app.Application +import android.content.Context import eu.kanade.tachiyomi.source.model.UpdateStrategy import kotlinx.coroutines.CancellationException 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.server.serverConfig import suwayomi.tachidesk.util.HAScheduler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.Date import java.util.concurrent.ConcurrentHashMap -import java.util.prefs.Preferences import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.hours @@ -48,7 +51,7 @@ class Updater : IUpdater { private val lastUpdateKey = "lastUpdateKey" private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey" - private val preferences = Preferences.userNodeForPackage(Updater::class.java) + private val preferences = Injekt.get().getSharedPreferences("manga/impl/update", Context.MODE_PRIVATE) private var currentUpdateTaskId = "" @@ -80,7 +83,7 @@ class Updater : IUpdater { private fun autoUpdateTask() { val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) - preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis()) + preferences.edit().putLong(lastAutomatedUpdateKey, System.currentTimeMillis()).apply() if (status.value.running) { logger.debug { "Global update is already in progress" } @@ -178,7 +181,7 @@ class Updater : IUpdater { clear: Boolean?, forceAll: Boolean, ) { - preferences.putLong(lastUpdateKey, System.currentTimeMillis()) + preferences.edit().putLong(lastUpdateKey, System.currentTimeMillis()).apply() if (clear == true) { reset() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index 25776083a..350887d98 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -7,6 +7,8 @@ package suwayomi.tachidesk.server * 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/. */ +import android.app.Application +import android.content.Context import ch.qos.logback.classic.Level import com.typesafe.config.ConfigRenderOptions 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.util.AppMutex.handleAppMutex 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.AndroidCompatInitializer import xyz.nulldev.ts.config.ApplicationRootDir @@ -42,6 +46,9 @@ import xyz.nulldev.ts.config.setLogLevelFor import java.io.File import java.security.Security import java.util.Locale +import java.util.prefs.Preferences +import kotlin.io.path.exists +import kotlin.io.path.outputStream private val logger = KotlinLogging.logger {} @@ -211,6 +218,10 @@ fun applicationSetup() { } }, ignoreInitialValue = false) + val preferences = Preferences.userRoot().node("suwayomi/tachidesk") + migratePreferences(null, preferences) + preferences.removeNode() + // Disable jetty's logging System.setProperty("org.eclipse.jetty.util.log.announce", "false") 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 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().getSharedPreferences(key, Context.MODE_PRIVATE) + + val items: Map = + 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 + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index 619e24933..3ca1e8668 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -7,6 +7,8 @@ package suwayomi.tachidesk.server.util * 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/. */ +import android.app.Application +import android.content.Context import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper 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.serverConfig import suwayomi.tachidesk.util.HAScheduler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream @@ -53,7 +57,6 @@ import java.net.URL import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.Date -import java.util.prefs.Preferences import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds @@ -124,7 +127,7 @@ object WebInterfaceManager { private const val LAST_WEBUI_UPDATE_CHECK_KEY = "lastWebUIUpdateCheckKey" - private val preferences = Preferences.userNodeForPackage(WebInterfaceManager::class.java) + private val preferences = Injekt.get().getSharedPreferences("server/util", Context.MODE_PRIVATE) private var currentUpdateTaskId: String = "" private val json: Json by injectLazy() @@ -326,7 +329,7 @@ object WebInterfaceManager { } 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() if (!isUpdateAvailable(localVersion).second) {