[优化] 优化听书相关代码

This commit is contained in:
HapeLee authored and Kudomaga committed 2026-01-17 06:39:29 +08:00
1 parent 65acfd9f51
commit a5a88fb7b2
12 files changed
+123 -87

No files matched your search

@@ -69,6 +69,7 @@ object PreferKey {
const val webDavAccount = "web_dav_account" const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password" const val webDavPassword = "web_dav_password"
const val webDavDir = "webDavDir" const val webDavDir = "webDavDir"
const val webServiceAutoStart = "webServiceAutoStart"
const val enableCustomExport = "enableCustomExport" const val enableCustomExport = "enableCustomExport"
const val exportToWebDav = "webDavCacheBackup" const val exportToWebDav = "webDavCacheBackup"
const val exportNoChapterName = "exportNoChapterName" const val exportNoChapterName = "exportNoChapterName"
@@ -243,4 +244,6 @@ object PreferKey {
const val showTip = "showTip" const val showTip = "showTip"
const val sliderVibrator = "sliderVibrator" const val sliderVibrator = "sliderVibrator"
const val audioCacheCleanTime = "audioCacheCleanTime"
const val audioPreDownloadNum = "audioPreDownloadNum"
} }
@@ -20,9 +20,6 @@ import io.legado.app.utils.removePref
import io.legado.app.utils.sysConfiguration import io.legado.app.utils.sysConfiguration
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import splitties.init.appCtx import splitties.init.appCtx
// 【新增引用】为了支持文件清理操作
import java.io.File
import io.legado.app.utils.FileUtils
@Suppress("MemberVisibilityCanBePrivate", "ConstPropertyName") @Suppress("MemberVisibilityCanBePrivate", "ConstPropertyName")
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
@@ -60,7 +57,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
var optimizeRender = CanvasRecorderFactory.isSupport var optimizeRender = CanvasRecorderFactory.isSupport
&& appCtx.getPrefBoolean(PreferKey.optimizeRender, false) && appCtx.getPrefBoolean(PreferKey.optimizeRender, false)
var recordLog = appCtx.getPrefBoolean(PreferKey.recordLog) var recordLog = appCtx.getPrefBoolean(PreferKey.recordLog)
var webServiceAutoStart = appCtx.getPrefBoolean(PreferKey.webServiceAutoStart)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
@@ -976,39 +973,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefBoolean(PreferKey.sliderVibrator, value) appCtx.putPrefBoolean(PreferKey.sliderVibrator, value)
} }
// ================= 自定义功能区域 Start =================
// 1. 听书预加载数量
// 逻辑:从设置里读取字符串,转成数字。如果读不到,默认返回 10。
val audioPreDownloadNum: Int val audioPreDownloadNum: Int
get() { get() = appCtx.getPrefInt(PreferKey.audioPreDownloadNum, 10)
// 注意:EditTextPreference 保存的是 String
val str = appCtx.getPrefString("audioPreDownloadNum") val audioCacheCleanTimeOrgin: Int
return str?.toIntOrNull() ?: 10 get() = appCtx.getPrefInt(PreferKey.audioCacheCleanTime, 10)
}
// 2. 音频缓存保留时间 (返回毫秒)
// 逻辑:用户输入的是“分钟”,我们在这里把它乘以 60000 变成“毫秒”。
val audioCacheCleanTime: Long val audioCacheCleanTime: Long
get() { get() {
val str = appCtx.getPrefString("audioCacheCleanTime") val str = appCtx.getPrefInt(PreferKey.audioCacheCleanTime, 10)
val minutes = str?.toLongOrNull() ?: 10L // 默认 10 分钟 return str * 60 * 1000L
return minutes * 60 * 1000L
} }
/**
* 【新增】手动清理 TTS 缓存
* 这个方法会自动寻找 externalCacheDir(外部存储),如果找不到再找内部。
* 确保和 Service 里的逻辑保持一致。
*/
fun clearTtsCache() {
val baseDir = appCtx.externalCacheDir ?: appCtx.cacheDir
// 清理音频文件
FileUtils.delete(baseDir.absolutePath + File.separator + "httpTTS")
// 清理数据库索引 (可选,建议一起清,防止索引还在但文件没了)
FileUtils.delete(baseDir.absolutePath + File.separator + "httpTTS_cache")
}
// ================= 自定义功能区域 End =================
} }
@@ -65,11 +65,9 @@ import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.coroutines.coroutineContext
/** /**
* 在线朗读 * 在线朗读
* (完美匹配版:修复文件名MD5不一致 + 修复缓存不可见 + 修复0分钟不彻底清理)
*/ */
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class HttpReadAloudService : BaseReadAloudService(), class HttpReadAloudService : BaseReadAloudService(),
@@ -78,7 +76,7 @@ class HttpReadAloudService : BaseReadAloudService(),
ExoPlayer.Builder(this).build() ExoPlayer.Builder(this).build()
} }
// 【修改点1】改为外部存储,方便你查看文件,确认是否生成 // 改为外部存储
private val ttsFolderPath: String by lazy { private val ttsFolderPath: String by lazy {
val baseDir = externalCacheDir ?: cacheDir val baseDir = externalCacheDir ?: cacheDir
baseDir.absolutePath + File.separator + "httpTTS" + File.separator baseDir.absolutePath + File.separator + "httpTTS" + File.separator
@@ -454,13 +452,10 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
/** /**
* 【核心修复点】生成音频文件名 * 生成音频文件名
* 1. 之前的问题:textChapter.title 可能是 "第一章 内容" (带格式),而数据库里是 "第一章 内容" (不带格式)。
* 2. 现在的做法:强制使用 textChapter.chapter.title (数据库原始标题)。
* 3. 结果:前台 (播放) 和后台 (下载) 用的标题来源完全一致,MD5 绝对匹配,不会再重复下载了!
*/ */
private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String { private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String {
val titleToUse = textChapter?.chapter?.title ?: "" // 强制取“核芯”标题 val titleToUse = textChapter?.chapter?.title ?: ""
return MD5Utils.md5Encode16(titleToUse) + "_" + return MD5Utils.md5Encode16(titleToUse) + "_" +
MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content") MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
} }
@@ -492,7 +487,7 @@ class HttpReadAloudService : BaseReadAloudService(),
/** /**
* 移除缓存文件 * 移除缓存文件
* 修复逻辑:如果时间设置为0,则不再保护当前章节,退出即全删。 * 如果时间设置为0,则不再保护当前章节,退出即全删。
*/ */
private fun removeCacheFile() { private fun removeCacheFile() {
val keepTime = AppConfig.audioCacheCleanTime val keepTime = AppConfig.audioCacheCleanTime
@@ -1,5 +1,6 @@
package io.legado.app.service package io.legado.app.service
// ——————【新增引用】——————
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -21,6 +22,7 @@ import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.servicePendingIntent import io.legado.app.utils.servicePendingIntent
import io.legado.app.utils.startForegroundServiceCompat import io.legado.app.utils.startForegroundServiceCompat
@@ -33,8 +35,6 @@ import splitties.init.appCtx
import splitties.systemservices.powerManager import splitties.systemservices.powerManager
import splitties.systemservices.wifiManager import splitties.systemservices.wifiManager
import java.io.IOException import java.io.IOException
// ——————【新增引用】——————
import io.legado.app.utils.putPrefBoolean
class WebService : BaseService() { class WebService : BaseService() {
@@ -43,9 +43,6 @@ class WebService : BaseService() {
var hostAddress = "" var hostAddress = ""
fun start(context: Context) { fun start(context: Context) {
// ——————【修改开始】记录开启状态——————
appCtx.putPrefBoolean("web_service_auto", true)
// ——————【修改结束】——————
context.startService<WebService>() context.startService<WebService>()
} }
@@ -55,9 +52,6 @@ class WebService : BaseService() {
} }
fun stop(context: Context) { fun stop(context: Context) {
// ——————【修改开始】记录关闭状态——————
appCtx.putPrefBoolean("web_service_auto", false)
// ——————【修改结束】——————
context.stopService<WebService>() context.stopService<WebService>()
} }
@@ -1,5 +1,8 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
//import io.legado.app.lib.theme.backgroundColor
//import io.legado.app.lib.theme.primaryColor
// 【新增引用】为了显示清理成功的提示
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -18,16 +21,16 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.lib.prefs.SwitchPreference import io.legado.app.lib.prefs.SwitchPreference
import io.legado.app.lib.prefs.fragment.PreferenceFragment import io.legado.app.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.backgroundColor
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.model.ReadAloud import io.legado.app.model.ReadAloud
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.StringUtils import io.legado.app.utils.StringUtils
import io.legado.app.utils.TTSCacheUtils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
// 【新增引用】为了显示清理成功的提示
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
class ReadAloudConfigDialog : BasePrefDialogFragment() { class ReadAloudConfigDialog : BasePrefDialogFragment() {
@@ -73,17 +76,16 @@ class ReadAloudConfigDialog : BasePrefDialogFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_aloud) addPreferencesFromResource(R.xml.pref_config_aloud)
upSpeakEngineSummary() upSpeakEngineSummary()
upPreferenceSummary(PreferKey.audioPreDownloadNum)
upPreferenceSummary(PreferKey.audioCacheCleanTime)
findPreference<SwitchPreference>(PreferKey.pauseReadAloudWhilePhoneCalls)?.let { findPreference<SwitchPreference>(PreferKey.pauseReadAloudWhilePhoneCalls)?.let {
it.isEnabled = AppConfig.ignoreAudioFocus it.isEnabled = AppConfig.ignoreAudioFocus
} }
// 【新增代码】绑定清理缓存按钮事件
findPreference<Preference>("clear_cache")?.let { findPreference<Preference>("clear_cache")?.let {
it.summary = getString(R.string.clear_cache) it.summary = getString(R.string.clear_cache)
it.setOnPreferenceClickListener { it.setOnPreferenceClickListener {
// 调用 AppConfig 中的万能清理函数 (自动处理外部存储) TTSCacheUtils.clearTtsCache()
AppConfig.clearTtsCache()
// 弹出提示
toastOnUi("音频缓存已清理") toastOnUi("音频缓存已清理")
true true
} }
@@ -107,6 +109,38 @@ class ReadAloudConfigDialog : BasePrefDialogFragment() {
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) { when (preference.key) {
PreferKey.audioPreDownloadNum -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.read_aloud_preload))
.setMaxValue(50)
.setMinValue(0)
.setValue(10)
.setCustomButton((R.string.btn_default_s)) {
putPrefInt(PreferKey.audioPreDownloadNum, 10)
upPreferenceSummary(PreferKey.audioPreDownloadNum)
}
.show {
putPrefInt(PreferKey.audioPreDownloadNum, it)
upPreferenceSummary(PreferKey.audioPreDownloadNum)
}
}
PreferKey.audioCacheCleanTime -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.audio_cache_clean_time))
.setMaxValue(50)
.setMinValue(0)
.setValue(1)
.setCustomButton((R.string.btn_default_s)) {
putPrefInt(PreferKey.audioCacheCleanTime, 10)
upPreferenceSummary(PreferKey.audioCacheCleanTime)
}
.show {
putPrefInt(PreferKey.audioCacheCleanTime, it)
upPreferenceSummary(PreferKey.audioCacheCleanTime)
}
}
PreferKey.ttsEngine -> showDialogFragment(SpeakEngineDialog()) PreferKey.ttsEngine -> showDialogFragment(SpeakEngineDialog())
"sysTtsConfig" -> IntentHelp.openTTSSetting() "sysTtsConfig" -> IntentHelp.openTTSSetting()
} }
@@ -139,12 +173,34 @@ class ReadAloudConfigDialog : BasePrefDialogFragment() {
preference.summary = if (index >= 0) preference.entries[index] else null preference.summary = if (index >= 0) preference.entries[index] else null
} }
else -> { else -> {
preference?.summary = value preference?.summary = value
} }
} }
} }
private fun upPreferenceSummary(preferenceKey: String, value: String? = null) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.audioPreDownloadNum -> {
preference.summary = getString(
R.string.read_aloud_preload_summary,
AppConfig.audioPreDownloadNum
)
}
PreferKey.audioCacheCleanTime -> {
preference.summary = getString(
R.string.audio_cache_clean_time_summary,
AppConfig.audioCacheCleanTimeOrgin
)
}
else -> preference.summary = value
}
}
override fun upSpeakEngineSummary() { override fun upSpeakEngineSummary() {
upPreferenceSummary( upPreferenceSummary(
findPreference(PreferKey.ttsEngine), findPreference(PreferKey.ttsEngine),
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
//import io.legado.app.lib.theme.primaryColor
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
@@ -24,15 +25,14 @@ import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.model.ReadAloud import io.legado.app.model.ReadAloud
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.association.ImportHttpTtsDialog import io.legado.app.ui.association.ImportHttpTtsDialog
import io.legado.app.ui.file.HandleFileContract import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.login.SourceLoginActivity import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.TTSCacheUtils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
@@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
/** /**
* tts引擎管理 * tts引擎管理
@@ -236,19 +235,10 @@ class SpeakEngineDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
adapter.notifyItemRangeChanged(adapter.getHeaderCount(), adapter.itemCount) adapter.notifyItemRangeChanged(adapter.getHeaderCount(), adapter.itemCount)
} }
/**
* 【核心修改点】清理缓存方法
* 原来:只删除 cacheDir (内部存储)
* 现在:调用 AppConfig.clearTtsCache(),自动处理 externalCacheDir (外部存储)
*/
fun clearCache() { fun clearCache() {
execute { execute {
ReadAloud.upReadAloudClass() ReadAloud.upReadAloudClass()
TTSCacheUtils.clearTtsCache()
// 调用万能清理函数
AppConfig.clearTtsCache()
// 显示成功提示
toastOnUi(R.string.clear_cache_success) toastOnUi(R.string.clear_cache_success)
} }
} }
@@ -42,6 +42,7 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.help.update.AppUpdateGitHub import io.legado.app.help.update.AppUpdateGitHub
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.service.WebService
import io.legado.app.ui.about.CrashLogsDialog import io.legado.app.ui.about.CrashLogsDialog
import io.legado.app.ui.about.UpdateDialog import io.legado.app.ui.about.UpdateDialog
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
@@ -71,7 +72,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import io.legado.app.service.WebService
/** /**
* 主界面 * 主界面
@@ -135,13 +135,11 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
binding.viewPagerMain.fitsSystemWindows = true binding.viewPagerMain.fitsSystemWindows = true
// 其他初始化逻辑 // 其他初始化逻辑
setupBackCallback() setupBackCallback()
// ——————【修改开始】——————
// 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启 // 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启
if (getPrefBoolean("web_service_auto", false)) { if (AppConfig.webServiceAutoStart) {
WebService.startForeground(this) WebService.startForeground(this)
} }
// ——————【修改结束】——————
upBottomMenu() upBottomMenu()
initView() initView()
@@ -0,0 +1,16 @@
package io.legado.app.utils
import splitties.init.appCtx
import java.io.File
object TTSCacheUtils {
fun clearTtsCache() {
val baseDir = appCtx.externalCacheDir ?: appCtx.cacheDir
// 清理音频文件
FileUtils.delete(baseDir.absolutePath + File.separator + "httpTTS")
// 清理数据库索引 (可选,建议一起清,防止索引还在但文件没了)
FileUtils.delete(baseDir.absolutePath + File.separator + "httpTTS_cache")
}
}
@@ -1370,4 +1370,9 @@
<string name="palette_style">调色板样式</string> <string name="palette_style">调色板样式</string>
<string name="tip_compose_set">这里的选项仅可以控制使用Compose重写的界面。</string> <string name="tip_compose_set">这里的选项仅可以控制使用Compose重写的界面。</string>
<string name="is_blur_enable">启用控件模糊</string> <string name="is_blur_enable">启用控件模糊</string>
<string name="web_service_auto_start">启动应用时打开Web服务</string>
<string name="read_aloud_preload">听书预加载数量</string>
<string name="read_aloud_preload_summary">%d</string>
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
</resources> </resources>
+5
View File
@@ -1375,4 +1375,9 @@
<string name="palette_style">调色板样式</string> <string name="palette_style">调色板样式</string>
<string name="tip_compose_set">这里的选项仅可以控制使用Compose重写的界面。</string> <string name="tip_compose_set">这里的选项仅可以控制使用Compose重写的界面。</string>
<string name="is_blur_enable">启用控件模糊</string> <string name="is_blur_enable">启用控件模糊</string>
<string name="web_service_auto_start">启动应用时打开Web服务</string>
<string name="read_aloud_preload">听书预加载数量</string>
<string name="read_aloud_preload_summary">%d</string>
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
</resources> </resources>
+6 -12
View File
@@ -67,22 +67,16 @@
android:key="sysTtsConfig" android:key="sysTtsConfig"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.EditTextPreference <io.legado.app.lib.prefs.Preference
android:key="audioPreDownloadNum" android:key="audioPreDownloadNum"
android:title="听书预加载数量(章节)" android:summary="@string/read_aloud_preload_summary"
android:summary="设置听书时预先下载的章节数量" android:title="@string/read_aloud_preload"
android:defaultValue="10"
android:inputType="number"
android:persistent="true"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.EditTextPreference <io.legado.app.lib.prefs.Preference
android:key="audioCacheCleanTime" android:key="audioCacheCleanTime"
android:title="音频缓存保留时间(分钟)" android:summary="@string/audio_cache_clean_time_summary"
android:summary="听书缓存文件保留多久后自动删除(单位:分钟)。输入 0 代表退出即刻清理。" android:title="@string/audio_cache_clean_time"
android:defaultValue="10"
android:inputType="number"
android:persistent="true"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.lib.prefs.PreferenceCategory> </io.legado.app.lib.prefs.PreferenceCategory>
@@ -19,6 +19,12 @@
app:entries="@array/default_app_variant" app:entries="@array/default_app_variant"
app:entryValues="@array/default_app_variant_value" /> app:entryValues="@array/default_app_variant_value" />
<io.legado.app.lib.prefs.SwitchPreference
android:defaultValue="false"
android:key="webServiceAutoStart"
android:title="@string/web_service_auto_start"
app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.PreferenceCategory <io.legado.app.lib.prefs.PreferenceCategory
android:title="@string/main_activity" android:title="@string/main_activity"
app:allowDividerAbove="false" app:allowDividerAbove="false"