Selaa lähdekoodia

feat: Restore 配置恢复删除 SP 写入分支,提取 normalizeConfigMap 纯函数

config.xml 恢复改为 normalizeConfigMap(AES 解密回退/Double 转 Float/
bookInfoBackgroundBlur 拆键,纯函数附 JVM 单测)规整后经 AppConfigStore.putAll
单次原子 edit 落盘,恢复完成提示前 awaitPendingWrites 确保已持久化
5151561 3 kuukautta sitten
vanhempi
commit
91725cb84b

+ 11 - 63
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -4,7 +4,6 @@ import android.content.Context
 import android.database.sqlite.SQLiteConstraintException
 import android.net.Uri
 import androidx.appcompat.app.AppCompatDelegate
-import androidx.core.content.edit
 import androidx.documentfile.provider.DocumentFile
 import io.legado.app.BuildConfig
 import io.legado.app.R
@@ -39,6 +38,7 @@ import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.upType
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.LocalConfig
+import io.legado.app.help.config.DsSync
 import io.legado.app.help.config.ThemeConfigStore
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.config.otherConfig.OtherConfig
@@ -50,7 +50,6 @@ import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.compress.ZipUtils
-import io.legado.app.utils.defaultSharedPreferences
 import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.isContentScheme
@@ -397,68 +396,17 @@ object Restore : KoinComponent {
     }
 
     private suspend fun applyConfigMap(map: Map<String, Any?>, aes: BackupAES) {
-        val finalMap = mutableMapOf<String, Any>()
-        val legacyBookInfoBackground = map[PreferKey.bookInfoBackgroundBlur] as? String
-        val restoreNetworkCoverBackground =
-            legacyBookInfoBackground != null &&
-                    PreferKey.bookInfoNetworkCoverBackground !in map &&
-                    BackupConfig.keyIsNotIgnore(PreferKey.bookInfoBackgroundBlur) &&
-                    BackupConfig.keyIsNotIgnore(PreferKey.bookInfoNetworkCoverBackground)
-        val restoreDefaultCoverBackground =
-            legacyBookInfoBackground != null &&
-                    PreferKey.bookInfoDefaultCoverBackground !in map &&
-                    BackupConfig.keyIsNotIgnore(PreferKey.bookInfoBackgroundBlur) &&
-                    BackupConfig.keyIsNotIgnore(PreferKey.bookInfoDefaultCoverBackground)
-        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 }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            legacyBookInfoBackground?.let { backgroundMode ->
-                if (restoreNetworkCoverBackground) {
-                    putString(PreferKey.bookInfoNetworkCoverBackground, backgroundMode)
-                    finalMap[PreferKey.bookInfoNetworkCoverBackground] = backgroundMode
-                }
-                if (restoreDefaultCoverBackground) {
-                    putString(PreferKey.bookInfoDefaultCoverBackground, backgroundMode)
-                    finalMap[PreferKey.bookInfoDefaultCoverBackground] = backgroundMode
-                }
-            }
-        }
-        // 经快照层批量恢复:立即对读侧生效(onRestoreFinish 的读取不再依赖回灌时机),异步单次 edit 落盘
+        val finalMap = normalizeConfigMap(
+            map = map,
+            keyIsNotIgnore = { BackupConfig.keyIsNotIgnore(it) },
+            decryptWebDavPassword = { runCatching { aes.decryptStr(it) }.getOrNull() },
+            hasLocalWebDavPassword = !appCtx.getPrefString(PreferKey.webDavPassword)
+                .isNullOrBlank(),
+        )
+        // 经快照层批量恢复:立即对读侧生效(onRestoreFinish 的读取不再依赖回灌时机),单次原子 edit 落盘
         AppConfigStore.putAll(finalMap)
+        // 恢复完成提示前等待落盘,dataStore.edit 返回即持久化完成
+        DsSync.awaitPendingWrites()
     }
 
     private fun readXmlToMap(file: File): Map<String, Any?> {

+ 46 - 0
app/src/main/java/io/legado/app/help/storage/RestoreConfigNormalizer.kt

@@ -0,0 +1,46 @@
+package io.legado.app.help.storage
+
+import io.legado.app.constant.PreferKey
+
+/**
+ * 把 config.xml 解析出的原始键值规整为可直接批量写入设置存储的 map(纯函数,便于 JVM 单测):
+ * - [PreferKey.webDavPassword]:AES 解密;解密失败时本地无密码则保留原文,否则丢弃;
+ * - Double 转 Float(JSON 数字会被解析为 Double);
+ * - 旧版 [PreferKey.bookInfoBackgroundBlur] 拆分补写两个新背景键(备份中缺新键且未被忽略时);
+ * - 过滤忽略键与 null 值。
+ */
+internal fun normalizeConfigMap(
+    map: Map<String, Any?>,
+    keyIsNotIgnore: (String) -> Boolean,
+    decryptWebDavPassword: (String) -> String?,
+    hasLocalWebDavPassword: Boolean,
+): Map<String, Any> {
+    val finalMap = mutableMapOf<String, Any>()
+    map.forEach { (key, value) ->
+        if (!keyIsNotIgnore(key)) return@forEach
+        when (key) {
+            PreferKey.webDavPassword -> {
+                val password = decryptWebDavPassword(value.toString())
+                    ?: value.toString().takeIf { !hasLocalWebDavPassword }
+                password?.let { finalMap[key] = it }
+            }
+
+            else -> when (value) {
+                is Double -> finalMap[key] = value.toFloat()
+                is Int, is Boolean, is Long, is Float, is String -> finalMap[key] = value
+            }
+        }
+    }
+    val legacyBookInfoBackground = map[PreferKey.bookInfoBackgroundBlur] as? String
+    if (legacyBookInfoBackground != null && keyIsNotIgnore(PreferKey.bookInfoBackgroundBlur)) {
+        listOf(
+            PreferKey.bookInfoNetworkCoverBackground,
+            PreferKey.bookInfoDefaultCoverBackground,
+        ).forEach { newKey ->
+            if (newKey !in map && keyIsNotIgnore(newKey)) {
+                finalMap[newKey] = legacyBookInfoBackground
+            }
+        }
+    }
+    return finalMap
+}

+ 119 - 0
app/src/test/java/io/legado/app/help/storage/NormalizeConfigMapTest.kt

@@ -0,0 +1,119 @@
+package io.legado.app.help.storage
+
+import io.legado.app.constant.PreferKey
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class NormalizeConfigMapTest {
+
+    private fun normalize(
+        map: Map<String, Any?>,
+        keyIsNotIgnore: (String) -> Boolean = { true },
+        decryptWebDavPassword: (String) -> String? = { null },
+        hasLocalWebDavPassword: Boolean = false,
+    ) = normalizeConfigMap(map, keyIsNotIgnore, decryptWebDavPassword, hasLocalWebDavPassword)
+
+    @Test
+    fun `基础类型原样保留且跳过 null`() {
+        val result = normalize(
+            mapOf(
+                "int" to 1,
+                "boolean" to true,
+                "long" to 2L,
+                "float" to 3f,
+                "string" to "s",
+                "nullKey" to null,
+            )
+        )
+        assertEquals(
+            mapOf("int" to 1, "boolean" to true, "long" to 2L, "float" to 3f, "string" to "s"),
+            result
+        )
+    }
+
+    @Test
+    fun `Double 转为 Float`() {
+        val result = normalize(mapOf("fontScale" to 1.5))
+        assertEquals(1.5f, result["fontScale"])
+    }
+
+    @Test
+    fun `忽略键被过滤`() {
+        val result = normalize(
+            mapOf("keep" to 1, "drop" to 2),
+            keyIsNotIgnore = { it != "drop" },
+        )
+        assertEquals(mapOf("keep" to 1), result)
+    }
+
+    @Test
+    fun `webDavPassword 解密成功用解密值`() {
+        val result = normalize(
+            mapOf(PreferKey.webDavPassword to "encrypted"),
+            decryptWebDavPassword = { if (it == "encrypted") "plain" else null },
+            hasLocalWebDavPassword = true,
+        )
+        assertEquals("plain", result[PreferKey.webDavPassword])
+    }
+
+    @Test
+    fun `webDavPassword 解密失败且本地无密码时保留原文`() {
+        val result = normalize(
+            mapOf(PreferKey.webDavPassword to "rawPassword"),
+            decryptWebDavPassword = { null },
+            hasLocalWebDavPassword = false,
+        )
+        assertEquals("rawPassword", result[PreferKey.webDavPassword])
+    }
+
+    @Test
+    fun `webDavPassword 解密失败且本地已有密码时丢弃`() {
+        val result = normalize(
+            mapOf(PreferKey.webDavPassword to "rawPassword"),
+            decryptWebDavPassword = { null },
+            hasLocalWebDavPassword = true,
+        )
+        assertFalse(result.containsKey(PreferKey.webDavPassword))
+    }
+
+    @Test
+    fun `旧版 bookInfoBackgroundBlur 补写两个新背景键`() {
+        val result = normalize(mapOf(PreferKey.bookInfoBackgroundBlur to "blur"))
+        assertEquals("blur", result[PreferKey.bookInfoBackgroundBlur])
+        assertEquals("blur", result[PreferKey.bookInfoNetworkCoverBackground])
+        assertEquals("blur", result[PreferKey.bookInfoDefaultCoverBackground])
+    }
+
+    @Test
+    fun `备份已含新背景键时不覆盖`() {
+        val result = normalize(
+            mapOf(
+                PreferKey.bookInfoBackgroundBlur to "blur",
+                PreferKey.bookInfoNetworkCoverBackground to "network",
+            )
+        )
+        assertEquals("network", result[PreferKey.bookInfoNetworkCoverBackground])
+        assertEquals("blur", result[PreferKey.bookInfoDefaultCoverBackground])
+    }
+
+    @Test
+    fun `新背景键被忽略时不补写`() {
+        val result = normalize(
+            mapOf(PreferKey.bookInfoBackgroundBlur to "blur"),
+            keyIsNotIgnore = { it != PreferKey.bookInfoDefaultCoverBackground },
+        )
+        assertEquals("blur", result[PreferKey.bookInfoNetworkCoverBackground])
+        assertFalse(result.containsKey(PreferKey.bookInfoDefaultCoverBackground))
+    }
+
+    @Test
+    fun `bookInfoBackgroundBlur 本身被忽略时不拆键`() {
+        val result = normalize(
+            mapOf(PreferKey.bookInfoBackgroundBlur to "blur"),
+            keyIsNotIgnore = { it != PreferKey.bookInfoBackgroundBlur },
+        )
+        assertFalse(result.containsKey(PreferKey.bookInfoNetworkCoverBackground))
+        assertFalse(result.containsKey(PreferKey.bookInfoDefaultCoverBackground))
+    }
+}