Explorar o código

feat: 增强数据存储兼容性,优化类型转换和错误处理

5151561 hai 3 meses
pai
achega
aa39f00c6f

+ 13 - 1
app/src/main/java/io/legado/app/help/config/AppConfigStore.kt

@@ -19,6 +19,9 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import java.io.IOException
 
+import io.legado.app.utils.LogUtils
+import io.legado.app.utils.stackTraceStr
+
 /**
  * "settings" DataStore 的进程内内存快照层,设置读取的唯一同步入口。
  *
@@ -134,7 +137,16 @@ internal class PendingOverlayCore(
             launchWrite {
                 val result = runCatching { persist(key, value) }
                 synchronized(lock) {
-                    result.onSuccess { snapshot = it }
+                    result.onSuccess { 
+                        snapshot = it 
+                    }.onFailure { e ->
+                        val isOverridden = pending[key] !== write
+                        if (isOverridden) {
+                            LogUtils.e("AppConfigStore", "保存设置失败且已被新写入覆盖: key=$key, value=$value\n${e.stackTraceStr}")
+                        } else {
+                            LogUtils.e("AppConfigStore", "保存设置失败: key=$key, value=$value\n${e.stackTraceStr}")
+                        }
+                    }
                     // 同 key 已有更新的写入时不移除,继续由新条目覆盖
                     if (pending[key] === write) pending.remove(key)
                     rebuild()

+ 3 - 5
app/src/main/java/io/legado/app/help/config/DsSync.kt

@@ -63,12 +63,10 @@ object DsSync {
      * 用于 restart 等必须确保"写入已持久化"的场景。
      */
     suspend fun awaitPendingWrites(timeoutMs: Long = 3000) {
-        val startTime = System.currentTimeMillis()
-        while (pendingCount.get() > 0) {
-            if (System.currentTimeMillis() - startTime > timeoutMs) {
-                break
+        withTimeoutOrNull(timeoutMs) {
+            while (pendingCount.get() > 0) {
+                delay(50)
             }
-            delay(50)
         }
     }
 

+ 18 - 7
app/src/main/java/io/legado/app/help/config/PreferencesDsCompat.kt

@@ -25,30 +25,41 @@ import androidx.datastore.preferences.core.stringSetPreferencesKey
  */
 fun Preferences.rawPrefValue(key: String): Any? = asMap()[stringPreferencesKey(key)]
 
-fun Preferences.compatDsString(key: String): String? = rawPrefValue(key) as? String
+fun Preferences.compatDsString(key: String): String? = when (val raw = rawPrefValue(key)) {
+    is String -> raw
+    is Number, is Boolean -> raw.toString()
+    else -> null
+}
 
 fun Preferences.compatDsInt(key: String): Int? = when (val raw = rawPrefValue(key)) {
-    is Int -> raw
+    is Number -> raw.toInt()
     is String -> raw.toIntOrNull()
     else -> null
 }
 
 fun Preferences.compatDsBoolean(key: String): Boolean? = when (val raw = rawPrefValue(key)) {
     is Boolean -> raw
-    is String -> raw.toBooleanStrictOrNull()
+    is String -> raw.toBooleanStrictOrNull() ?: when (raw) {
+        "1" -> true
+        "0" -> false
+        else -> null
+    }
+    is Number -> when (raw.toInt()) {
+        1 -> true
+        0 -> false
+        else -> null
+    }
     else -> null
 }
 
 fun Preferences.compatDsLong(key: String): Long? = when (val raw = rawPrefValue(key)) {
-    is Long -> raw
-    is Int -> raw.toLong()
+    is Number -> raw.toLong()
     is String -> raw.toLongOrNull()
     else -> null
 }
 
 fun Preferences.compatDsFloat(key: String): Float? = when (val raw = rawPrefValue(key)) {
-    is Float -> raw
-    is Int -> raw.toFloat()
+    is Number -> raw.toFloat()
     is String -> raw.toFloatOrNull()
     else -> null
 }

+ 7 - 22
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt

@@ -77,7 +77,6 @@ fun ThemeManageScreen(
     var applyTarget by remember { mutableStateOf<SavedTheme?>(null) }
     var exportTarget by remember { mutableStateOf<SavedTheme?>(null) }
     var editTarget by remember { mutableStateOf<SavedTheme?>(null) }
-    var showRestartDialog by remember { mutableStateOf(false) }
     val savedThemes = state.savedThemes
 
     val exportLauncher = rememberLauncherForActivityResult(
@@ -117,7 +116,9 @@ fun ThemeManageScreen(
         viewModel.effects.collectLatest { effect ->
             when (effect) {
                 ThemeManageEffect.RestartRequired -> {
-                    showRestartDialog = true
+                    Handler(Looper.getMainLooper()).postDelayed({
+                        postEvent(EventBus.RECREATE, "")
+                    }, 100)
                 }
 
                 is ThemeManageEffect.LegacyMigrationFinished -> {
@@ -146,7 +147,9 @@ fun ThemeManageScreen(
                     }
                     context.toastOnUi(message)
                     if (effect.restartRequired) {
-                        showRestartDialog = true
+                        Handler(Looper.getMainLooper()).postDelayed({
+                            postEvent(EventBus.RECREATE, "")
+                        }, 100)
                     }
                 }
             }
@@ -254,25 +257,7 @@ fun ThemeManageScreen(
         }
     }
 
-    // Restart dialog
-    // TODO: 后续降级为纯重组,参考 ThemeConfigStore.applyDayNightLive
-    AppAlertDialog(
-        show = showRestartDialog,
-        onDismissRequest = { showRestartDialog = false },
-        title = stringResource(R.string.restart_required_message),
-        onConfirm = {
-            showRestartDialog = false
-            Handler(Looper.getMainLooper()).postDelayed({
-                postEvent(EventBus.RECREATE, "")
-            }, 100)
-        },
-        confirmText = stringResource(R.string.ok),
-        onDismiss = {
-            showRestartDialog = false
-            context.toastOnUi(R.string.restart_later_message)
-        },
-        dismissText = stringResource(R.string.cancel)
-    )
+
 
     // Save theme dialog
     AppAlertDialog(

+ 31 - 4
app/src/test/java/io/legado/app/help/config/PreferencesDsCompatTest.kt

@@ -56,16 +56,43 @@ class PreferencesDsCompatTest {
     fun `缺失或无法解析的值返回 null 而不抛异常`() {
         val prefs = mutablePreferencesOf(
             stringPreferencesKey("garbage") to "not a number",
-            intPreferencesKey("wrongType") to 1,
         )
         assertNull(prefs.compatDsInt("absent"))
         assertNull(prefs.compatDsInt("garbage"))
         assertNull(prefs.compatDsBoolean("garbage"))
-        assertNull(prefs.compatDsBoolean("wrongType"))
-        assertNull(prefs.compatDsString("wrongType"))
         assertNull(prefs.compatDsStringSet("garbage"))
     }
 
+    @Test
+    fun `增强的类型转换测试`() {
+        val prefs = mutablePreferencesOf(
+            intPreferencesKey("i1") to 1,
+            intPreferencesKey("i0") to 0,
+            intPreferencesKey("i9") to 9,
+            stringPreferencesKey("s1") to "1",
+            stringPreferencesKey("s0") to "0",
+            booleanPreferencesKey("b") to true,
+            floatPreferencesKey("f") to 3.14f
+        )
+        // Number/Boolean to String
+        assertEquals("1", prefs.compatDsString("i1"))
+        assertEquals("true", prefs.compatDsString("b"))
+        assertEquals("3.14", prefs.compatDsString("f"))
+
+        // Float/Double/Long/Int to Boolean
+        assertEquals(true, prefs.compatDsBoolean("i1"))
+        assertEquals(false, prefs.compatDsBoolean("i0"))
+        assertNull(prefs.compatDsBoolean("i9"))
+
+        // String "1"/"0" to Boolean
+        assertEquals(true, prefs.compatDsBoolean("s1"))
+        assertEquals(false, prefs.compatDsBoolean("s0"))
+
+        // Float to Long/Int
+        assertEquals(3, prefs.compatDsInt("f"))
+        assertEquals(3L, prefs.compatDsLong("f"))
+    }
+
     @Test
     fun `boolean 解析大小写严格`() {
         val prefs = mutablePreferencesOf(stringPreferencesKey("b") to "True")
@@ -77,7 +104,7 @@ class PreferencesDsCompatTest {
         val prefs = mutablePreferencesOf(stringPreferencesKey("k") to "1")
         prefs.setPrefValue("k", 2)
         assertEquals(2, prefs.compatDsInt("k"))
-        assertNull(prefs.compatDsString("k"))
+        assertEquals("2", prefs.compatDsString("k"))
     }
 
     @Test