fix: 不再使用脆弱的反射进行备份与恢复
This commit is contained in:
@@ -25,7 +25,6 @@ import io.legado.app.utils.createFolderIfNotExist
|
|||||||
import io.legado.app.utils.defaultSharedPreferences
|
import io.legado.app.utils.defaultSharedPreferences
|
||||||
import io.legado.app.utils.externalFiles
|
import io.legado.app.utils.externalFiles
|
||||||
import io.legado.app.utils.getFile
|
import io.legado.app.utils.getFile
|
||||||
import io.legado.app.utils.getSharedPreferences
|
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.normalizeFileName
|
import io.legado.app.utils.normalizeFileName
|
||||||
import io.legado.app.utils.openOutputStream
|
import io.legado.app.utils.openOutputStream
|
||||||
@@ -178,29 +177,29 @@ object Backup {
|
|||||||
.writeText(GSON.toJson(it))
|
.writeText(GSON.toJson(it))
|
||||||
}
|
}
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
|
val configMap = appCtx.defaultSharedPreferences.all
|
||||||
sp.edit(commit = true) {
|
val xmlBuilder = StringBuilder()
|
||||||
appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
|
xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
|
||||||
if (BackupConfig.keyIsNotIgnore(key)) {
|
xmlBuilder.append("<map>\n")
|
||||||
when (key) {
|
configMap.forEach { (key, value) ->
|
||||||
PreferKey.webDavPassword -> {
|
if (BackupConfig.keyIsNotIgnore(key, true)) {
|
||||||
putString(key, aes.runCatching {
|
val finalValue = if (key == PreferKey.webDavPassword) {
|
||||||
encryptBase64(value.toString())
|
aes.runCatching { encryptBase64(value.toString()) }.getOrDefault(value.toString())
|
||||||
}.getOrDefault(value.toString()))
|
} else value
|
||||||
}
|
|
||||||
|
|
||||||
else -> when (value) {
|
when (finalValue) {
|
||||||
is Int -> putInt(key, value)
|
is String -> xmlBuilder.append(" <string name=\"$key\">${finalValue.replace("&", "&").replace("<", "<")}</string>\n")
|
||||||
is Boolean -> putBoolean(key, value)
|
is Int -> xmlBuilder.append(" <int name=\"$key\" value=\"$finalValue\" />\n")
|
||||||
is Long -> putLong(key, value)
|
is Long -> xmlBuilder.append(" <long name=\"$key\" value=\"$finalValue\" />\n")
|
||||||
is Float -> putFloat(key, value)
|
is Float -> xmlBuilder.append(" <float name=\"$key\" value=\"$finalValue\" />\n")
|
||||||
is String -> putString(key, value)
|
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()
|
currentCoroutineContext().ensureActive()
|
||||||
val zipFileName = getNowZipFileName()
|
val zipFileName = getNowZipFileName()
|
||||||
val paths = arrayListOf(*backupFileNames)
|
val paths = arrayListOf(*backupFileNames)
|
||||||
|
|||||||
@@ -88,7 +88,35 @@ object BackupConfig {
|
|||||||
PreferKey.bgImage,
|
PreferKey.bgImage,
|
||||||
PreferKey.bgImageBlurring,
|
PreferKey.bgImageBlurring,
|
||||||
PreferKey.bgImageN,
|
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(
|
private val coverPrefKeys = arrayOf(
|
||||||
@@ -100,9 +128,10 @@ object BackupConfig {
|
|||||||
PreferKey.coverShowAuthorN
|
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 {
|
return when {
|
||||||
ignorePrefKeys.contains(key) -> false
|
|
||||||
ignoreReadConfig && readPrefKeys.contains(key) -> false
|
ignoreReadConfig && readPrefKeys.contains(key) -> false
|
||||||
ignoreThemeConfig && themePrefKeys.contains(key) -> false
|
ignoreThemeConfig && themePrefKeys.contains(key) -> false
|
||||||
ignoreCoverConfig && coverPrefKeys.contains(key) -> false
|
ignoreCoverConfig && coverPrefKeys.contains(key) -> false
|
||||||
@@ -116,17 +145,17 @@ object BackupConfig {
|
|||||||
|
|
||||||
val ignoreReadConfig: Boolean
|
val ignoreReadConfig: Boolean
|
||||||
get() = ignoreConfig[readConfigKey] == true
|
get() = ignoreConfig[readConfigKey] == true
|
||||||
private val ignoreThemeMode: Boolean
|
val ignoreThemeMode: Boolean
|
||||||
get() = ignoreConfig[PreferKey.themeMode] == true
|
get() = ignoreConfig[PreferKey.themeMode] == true
|
||||||
private val ignoreThemeConfig: Boolean
|
val ignoreThemeConfig: Boolean
|
||||||
get() = ignoreConfig[themeConfigKey] == true
|
get() = ignoreConfig[themeConfigKey] == true
|
||||||
private val ignoreCoverConfig: Boolean
|
val ignoreCoverConfig: Boolean
|
||||||
get() = ignoreConfig[coverConfigKey] == true
|
get() = ignoreConfig[coverConfigKey] == true
|
||||||
private val ignoreBookshelfLayout: Boolean
|
val ignoreBookshelfLayout: Boolean
|
||||||
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
|
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
|
||||||
private val ignoreShowRss: Boolean
|
val ignoreShowRss: Boolean
|
||||||
get() = ignoreConfig[PreferKey.showRss] == true
|
get() = ignoreConfig[PreferKey.showRss] == true
|
||||||
private val ignoreThreadCount: Boolean
|
val ignoreThreadCount: Boolean
|
||||||
get() = ignoreConfig[PreferKey.threadCount] == true
|
get() = ignoreConfig[PreferKey.threadCount] == true
|
||||||
val ignoreLocalBook: Boolean
|
val ignoreLocalBook: Boolean
|
||||||
get() = ignoreConfig[localBookKey] == true
|
get() = ignoreConfig[localBookKey] == true
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import io.legado.app.utils.fromJsonArray
|
|||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.getPrefInt
|
import io.legado.app.utils.getPrefInt
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.getSharedPreferences
|
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.isJsonArray
|
import io.legado.app.utils.isJsonArray
|
||||||
import io.legado.app.utils.openInputStream
|
import io.legado.app.utils.openInputStream
|
||||||
@@ -58,6 +57,8 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@@ -273,17 +274,19 @@ object Restore : KoinComponent {
|
|||||||
AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it)
|
AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
//恢复主题配置
|
//恢复主题配置
|
||||||
File(path, OldThemeConfig.configFileName).takeIf {
|
if (!BackupConfig.ignoreThemeConfig) {
|
||||||
it.exists()
|
File(path, OldThemeConfig.configFileName).takeIf {
|
||||||
}?.runCatching {
|
it.exists()
|
||||||
FileUtils.delete(OldThemeConfig.configFilePath)
|
}?.runCatching {
|
||||||
copyTo(File(OldThemeConfig.configFilePath))
|
FileUtils.delete(OldThemeConfig.configFilePath)
|
||||||
OldThemeConfig.upConfig()
|
copyTo(File(OldThemeConfig.configFilePath))
|
||||||
}?.onFailure {
|
OldThemeConfig.upConfig()
|
||||||
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
|
}?.onFailure {
|
||||||
|
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File(path, BookCover.configFileName).takeIf {
|
File(path, BookCover.configFileName).takeIf {
|
||||||
it.exists()
|
it.exists() && !BackupConfig.ignoreCoverConfig
|
||||||
}?.runCatching {
|
}?.runCatching {
|
||||||
val json = readText()
|
val json = readText()
|
||||||
BookCover.saveCoverRule(json)
|
BookCover.saveCoverRule(json)
|
||||||
@@ -311,43 +314,19 @@ object Restore : KoinComponent {
|
|||||||
AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
|
AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//AppWebDav.downBgs()
|
// 恢复配置文件 (手动解析 XML,替代反射逻辑)
|
||||||
appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
|
val configFile = File(path, "config.xml")
|
||||||
val finalMap = mutableMapOf<String, Any>()
|
if (configFile.exists()) {
|
||||||
appCtx.defaultSharedPreferences.edit {
|
try {
|
||||||
map.forEach { (key, value) ->
|
val map = readXmlToMap(configFile)
|
||||||
if (BackupConfig.keyIsNotIgnore(key)) {
|
if (map.isNotEmpty()) {
|
||||||
when (key) {
|
applyConfigMap(map, aes)
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.put("恢复配置 XML 出错\n${e.localizedMessage}", e)
|
||||||
}
|
}
|
||||||
// 同步恢复到 DataStore
|
|
||||||
settingsRepository.batchPutFromMap(finalMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadBookConfig.apply {
|
ReadBookConfig.apply {
|
||||||
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
|
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
|
||||||
readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
|
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>? {
|
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
|
||||||
try {
|
try {
|
||||||
val file = File(path, fileName)
|
val file = File(path, fileName)
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ fun BackupConfigScreen(
|
|||||||
AppTextField(
|
AppTextField(
|
||||||
value = tempPassword,
|
value = tempPassword,
|
||||||
onValueChange = { tempPassword = it },
|
onValueChange = { tempPassword = it },
|
||||||
backgroundColor = MiuixTheme.colorScheme.surface,
|
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||||
label = stringResource(R.string.web_dav_pw),
|
label = stringResource(R.string.web_dav_pw),
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
|||||||
Reference in New Issue
Block a user