Răsfoiți Sursa

fix: 不再使用脆弱的反射进行备份与恢复

HapeLee 5 luni în urmă
părinte
comite
e592fcdebf

+ 19 - 20
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -25,7 +25,6 @@ import io.legado.app.utils.createFolderIfNotExist
 import io.legado.app.utils.defaultSharedPreferences
 import io.legado.app.utils.externalFiles
 import io.legado.app.utils.getFile
-import io.legado.app.utils.getSharedPreferences
 import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.normalizeFileName
 import io.legado.app.utils.openOutputStream
@@ -178,29 +177,29 @@ object Backup {
                 .writeText(GSON.toJson(it))
         }
         currentCoroutineContext().ensureActive()
-        appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
-            sp.edit(commit = true) {
-                appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
-                    if (BackupConfig.keyIsNotIgnore(key)) {
-                        when (key) {
-                            PreferKey.webDavPassword -> {
-                                putString(key, aes.runCatching {
-                                    encryptBase64(value.toString())
-                                }.getOrDefault(value.toString()))
-                            }
+        val configMap = appCtx.defaultSharedPreferences.all
+        val xmlBuilder = StringBuilder()
+        xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
+        xmlBuilder.append("<map>\n")
+        configMap.forEach { (key, value) ->
+            if (BackupConfig.keyIsNotIgnore(key, true)) {
+                val finalValue = if (key == PreferKey.webDavPassword) {
+                    aes.runCatching { encryptBase64(value.toString()) }.getOrDefault(value.toString())
+                } else value
 
-                            else -> when (value) {
-                                is Int -> putInt(key, value)
-                                is Boolean -> putBoolean(key, value)
-                                is Long -> putLong(key, value)
-                                is Float -> putFloat(key, value)
-                                is String -> putString(key, value)
-                            }
-                        }
-                    }
+                when (finalValue) {
+                    is String -> xmlBuilder.append("    <string name=\"$key\">${finalValue.replace("&", "&amp;").replace("<", "&lt;")}</string>\n")
+                    is Int -> xmlBuilder.append("    <int name=\"$key\" value=\"$finalValue\" />\n")
+                    is Long -> xmlBuilder.append("    <long name=\"$key\" value=\"$finalValue\" />\n")
+                    is Float -> xmlBuilder.append("    <float name=\"$key\" value=\"$finalValue\" />\n")
+                    is Boolean -> xmlBuilder.append("    <boolean name=\"$key\" value=\"$finalValue\" />\n")
                 }
             }
         }
+        xmlBuilder.append("</map>")
+        FileUtils.createFileIfNotExist(backupPath + File.separator + "config.xml")
+            .writeText(xmlBuilder.toString())
+
         currentCoroutineContext().ensureActive()
         val zipFileName = getNowZipFileName()
         val paths = arrayListOf(*backupFileNames)

+ 38 - 9
app/src/main/java/io/legado/app/help/storage/BackupConfig.kt

@@ -88,7 +88,35 @@ object BackupConfig {
         PreferKey.bgImage,
         PreferKey.bgImageBlurring,
         PreferKey.bgImageN,
-        PreferKey.bgImageNBlurring
+        PreferKey.bgImageNBlurring,
+        PreferKey.themeColor,
+        PreferKey.secondaryThemeColor,
+        PreferKey.paletteStyle,
+        PreferKey.materialVersion,
+        PreferKey.composeEngine,
+        PreferKey.customContrast,
+        PreferKey.customMode,
+        PreferKey.useMiuixMonet,
+        PreferKey.containerOpacity,
+        PreferKey.topBarOpacity,
+        PreferKey.bottomBarOpacity,
+        PreferKey.enableBlur,
+        PreferKey.enableProgressiveBlur,
+        PreferKey.topBarBlurRadius,
+        PreferKey.bottomBarBlurRadius,
+        PreferKey.topBarBlurAlpha,
+        PreferKey.bottomBarBlurAlpha,
+        PreferKey.bottomBarLensRadius,
+        PreferKey.useFlexibleTopAppBar,
+        PreferKey.cBackground,
+        PreferKey.cBBackground,
+        PreferKey.cNBackground,
+        PreferKey.cNBBackground,
+        PreferKey.enableDeepPersonalization,
+        PreferKey.primaryTextColor,
+        PreferKey.secondaryTextColor,
+        PreferKey.themeBackgroundColor,
+        PreferKey.labelContainerColor
     )
 
     private val coverPrefKeys = arrayOf(
@@ -100,9 +128,10 @@ object BackupConfig {
         PreferKey.coverShowAuthorN
     )
 
-    fun keyIsNotIgnore(key: String): Boolean {
+    fun keyIsNotIgnore(key: String, isBackup: Boolean = false): Boolean {
+        if (ignorePrefKeys.contains(key)) return false
+        if (isBackup) return true
         return when {
-            ignorePrefKeys.contains(key) -> false
             ignoreReadConfig && readPrefKeys.contains(key) -> false
             ignoreThemeConfig && themePrefKeys.contains(key) -> false
             ignoreCoverConfig && coverPrefKeys.contains(key) -> false
@@ -116,17 +145,17 @@ object BackupConfig {
 
     val ignoreReadConfig: Boolean
         get() = ignoreConfig[readConfigKey] == true
-    private val ignoreThemeMode: Boolean
+    val ignoreThemeMode: Boolean
         get() = ignoreConfig[PreferKey.themeMode] == true
-    private val ignoreThemeConfig: Boolean
+    val ignoreThemeConfig: Boolean
         get() = ignoreConfig[themeConfigKey] == true
-    private val ignoreCoverConfig: Boolean
+    val ignoreCoverConfig: Boolean
         get() = ignoreConfig[coverConfigKey] == true
-    private val ignoreBookshelfLayout: Boolean
+    val ignoreBookshelfLayout: Boolean
         get() = ignoreConfig[PreferKey.bookshelfLayout] == true
-    private val ignoreShowRss: Boolean
+    val ignoreShowRss: Boolean
         get() = ignoreConfig[PreferKey.showRss] == true
-    private val ignoreThreadCount: Boolean
+    val ignoreThreadCount: Boolean
         get() = ignoreConfig[PreferKey.threadCount] == true
     val ignoreLocalBook: Boolean
         get() = ignoreConfig[localBookKey] == true

+ 98 - 44
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -48,7 +48,6 @@ import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.getPrefInt
 import io.legado.app.utils.getPrefString
-import io.legado.app.utils.getSharedPreferences
 import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.isJsonArray
 import io.legado.app.utils.openInputStream
@@ -58,6 +57,8 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserFactory
 import splitties.init.appCtx
 import java.io.File
 import java.io.FileInputStream
@@ -273,17 +274,19 @@ object Restore : KoinComponent {
             AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it)
         }
         //恢复主题配置
-        File(path, OldThemeConfig.configFileName).takeIf {
-            it.exists()
-        }?.runCatching {
-            FileUtils.delete(OldThemeConfig.configFilePath)
-            copyTo(File(OldThemeConfig.configFilePath))
-            OldThemeConfig.upConfig()
-        }?.onFailure {
-            AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
+        if (!BackupConfig.ignoreThemeConfig) {
+            File(path, OldThemeConfig.configFileName).takeIf {
+                it.exists()
+            }?.runCatching {
+                FileUtils.delete(OldThemeConfig.configFilePath)
+                copyTo(File(OldThemeConfig.configFilePath))
+                OldThemeConfig.upConfig()
+            }?.onFailure {
+                AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
+            }
         }
         File(path, BookCover.configFileName).takeIf {
-            it.exists()
+            it.exists() && !BackupConfig.ignoreCoverConfig
         }?.runCatching {
             val json = readText()
             BookCover.saveCoverRule(json)
@@ -311,43 +314,19 @@ object Restore : KoinComponent {
                 AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
             }
         }
-        //AppWebDav.downBgs()
-        appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
-            val finalMap = mutableMapOf<String, Any>()
-            appCtx.defaultSharedPreferences.edit {
-                map.forEach { (key, value) ->
-                    if (BackupConfig.keyIsNotIgnore(key)) {
-                        when (key) {
-                            PreferKey.webDavPassword -> {
-                                val password = kotlin.runCatching {
-                                    aes.decryptStr(value.toString())
-                                }.getOrNull() ?: let {
-                                    if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
-                                        value.toString()
-                                    } else null
-                                }
-                                password?.let {
-                                    putString(key, it)
-                                    finalMap[key] = it
-                                }
-                            }
-
-                            else -> {
-                                when (value) {
-                                    is Int -> { putInt(key, value); finalMap[key] = value }
-                                    is Boolean -> { putBoolean(key, value); finalMap[key] = value }
-                                    is Long -> { putLong(key, value); finalMap[key] = value }
-                                    is Float -> { putFloat(key, value); finalMap[key] = value }
-                                    is String -> { putString(key, value); finalMap[key] = value }
-                                }
-                            }
-                        }
-                    }
+        // 恢复配置文件 (手动解析 XML,替代反射逻辑)
+        val configFile = File(path, "config.xml")
+        if (configFile.exists()) {
+            try {
+                val map = readXmlToMap(configFile)
+                if (map.isNotEmpty()) {
+                    applyConfigMap(map, aes)
                 }
+            } catch (e: Exception) {
+                AppLog.put("恢复配置 XML 出错\n${e.localizedMessage}", e)
             }
-            // 同步恢复到 DataStore
-            settingsRepository.batchPutFromMap(finalMap)
         }
+
         ReadBookConfig.apply {
             comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
             readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
@@ -366,6 +345,81 @@ object Restore : KoinComponent {
         }
     }
 
+    private suspend fun applyConfigMap(map: Map<String, Any?>, aes: BackupAES) {
+        val finalMap = mutableMapOf<String, Any>()
+        appCtx.defaultSharedPreferences.edit {
+            map.forEach { (key, value) ->
+                if (BackupConfig.keyIsNotIgnore(key)) {
+                    when (key) {
+                        PreferKey.webDavPassword -> {
+                            val password = kotlin.runCatching {
+                                aes.decryptStr(value.toString())
+                            }.getOrNull() ?: let {
+                                if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
+                                    value.toString()
+                                } else null
+                            }
+                            password?.let {
+                                putString(key, it)
+                                finalMap[key] = it
+                            }
+                        }
+
+                        else -> {
+                            if (value != null) {
+                                when (value) {
+                                    is Int -> { putInt(key, value); finalMap[key] = value }
+                                    is Boolean -> { putBoolean(key, value); finalMap[key] = value }
+                                    is Long -> { putLong(key, value); finalMap[key] = value }
+                                    is Double -> { // JSON 数字会被解析为 Double
+                                        val floatValue = value.toFloat()
+                                        putFloat(key, floatValue)
+                                        finalMap[key] = floatValue
+                                    }
+                                    is Float -> { putFloat(key, value); finalMap[key] = value }
+                                    is String -> { putString(key, value); finalMap[key] = value }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        // 同步恢复到 DataStore
+        settingsRepository.batchPutFromMap(finalMap)
+    }
+
+    private fun readXmlToMap(file: File): Map<String, Any?> {
+        val map = mutableMapOf<String, Any?>()
+        try {
+            val factory = XmlPullParserFactory.newInstance()
+            val parser = factory.newPullParser()
+            FileInputStream(file).use { fis ->
+                parser.setInput(fis, "UTF-8")
+                var eventType = parser.eventType
+                while (eventType != XmlPullParser.END_DOCUMENT) {
+                    if (eventType == XmlPullParser.START_TAG) {
+                        val tagName = parser.name
+                        val name = parser.getAttributeValue(null, "name")
+                        if (name != null) {
+                            when (tagName) {
+                                "string" -> map[name] = parser.nextText()
+                                "int" -> map[name] = parser.getAttributeValue(null, "value").toInt()
+                                "long" -> map[name] = parser.getAttributeValue(null, "value").toLong()
+                                "float" -> map[name] = parser.getAttributeValue(null, "value").toFloat()
+                                "boolean" -> map[name] = parser.getAttributeValue(null, "value").toBoolean()
+                            }
+                        }
+                    }
+                    eventType = parser.next()
+                }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+        return map
+    }
+
     private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
         try {
             val file = File(path, fileName)

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt

@@ -374,7 +374,7 @@ fun BackupConfigScreen(
                 AppTextField(
                     value = tempPassword,
                     onValueChange = { tempPassword = it },
-                    backgroundColor = MiuixTheme.colorScheme.surface,
+                    backgroundColor = LegadoTheme.colorScheme.surface,
                     label = stringResource(R.string.web_dav_pw),
                     visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
                     keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),