diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 825360885..5a4dde7bd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="io.legado.app">
+
diff --git a/app/src/main/assets/defaultData/directLinkUpload.json b/app/src/main/assets/defaultData/directLinkUpload.json
index 57dd4b0b6..b74237185 100644
--- a/app/src/main/assets/defaultData/directLinkUpload.json
+++ b/app/src/main/assets/defaultData/directLinkUpload.json
@@ -1,5 +1,5 @@
{
- "uploadUrl": "http://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}",
+ "uploadUrl": "https://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}",
"downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result",
"summary": "有效期2天"
}
\ No newline at end of file
diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md
index 5c0548e87..bd386dbc3 100644
--- a/app/src/main/assets/updateLog.md
+++ b/app/src/main/assets/updateLog.md
@@ -11,13 +11,21 @@
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
-**2022/10/13**
+**2022/10/16**
+
+* 添加更新失败分组
+* web服务和朗读服务添加唤醒锁
+* 修复搜索禁用书源的bug
+* 修复本地书籍很慢的问题
+
+**2022/10/14**
* 更新cronet: 106.0.5249.126
* 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容
* 书架管理添加筛选功能
* 搜索支持搜索范围多分组和单书源设置
-* 书源管理界面书源菜单添加单书源搜索数据功能
+* 书源管理界面书源菜单添加单书源搜索书籍功能
+* 复web阅读时app未退出阅读界面导致的进度bug by Xwite
**2022/10/09**
diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt
index 8a6339bdf..be8cd07e5 100644
--- a/app/src/main/java/io/legado/app/constant/AppConst.kt
+++ b/app/src/main/java/io/legado/app/constant/AppConst.kt
@@ -43,6 +43,7 @@ object AppConst {
const val bookGroupAudioId = -3L
const val bookGroupNetNoneId = -4L
const val bookGroupLocalNoneId = -5L
+ const val bookGroupErrorId = -11L
const val notificationIdRead = -1122391
const val notificationIdAudio = -1122392
diff --git a/app/src/main/java/io/legado/app/constant/BookType.kt b/app/src/main/java/io/legado/app/constant/BookType.kt
index 15515981c..9eb8bbbb1 100644
--- a/app/src/main/java/io/legado/app/constant/BookType.kt
+++ b/app/src/main/java/io/legado/app/constant/BookType.kt
@@ -12,6 +12,11 @@ object BookType {
*/
const val text = 0b1000
+ /**
+ * 16 更新失败
+ */
+ const val updateError = 0b10000
+
/**
* 32 音频
*/
@@ -35,9 +40,10 @@ object BookType {
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
- @IntDef(text, audio, image, webFile)
+ @IntDef(text, updateError, audio, image, webFile, local)
annotation class Type
+
/**
* 本地书籍书源标志
*/
diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt
index 01351d2eb..b826093b6 100644
--- a/app/src/main/java/io/legado/app/data/AppDatabase.kt
+++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt
@@ -105,9 +105,14 @@ abstract class AppDatabase : RoomDatabase() {
)
db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show)
- select ${AppConst.bookGroupLocalNoneId}, '本地未分组', -8, 0
+ select ${AppConst.bookGroupLocalNoneId}, '本地未分组', -6, 0
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalNoneId})"""
)
+ db.execSQL(
+ """insert into book_groups(groupId, groupName, 'order', show)
+ select ${AppConst.bookGroupErrorId}, '更新失败', -1, 1
+ where not exists (select * from book_groups where groupId = ${AppConst.bookGroupErrorId})"""
+ )
db.execSQL("update book_sources set loginUi = null where loginUi = 'null'")
db.execSQL("update rssSources set loginUi = null where loginUi = 'null'")
db.execSQL("update httpTTS set loginUi = null where loginUi = 'null'")
diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
index 4dd905c33..082bfba0a 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
@@ -53,6 +53,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun flowSearch(key: String): Flow>
+ @Query("SELECT * FROM books where type & ${BookType.updateError} > 0 order by durChapterTime desc")
+ fun flowUpdateError(): Flow>
+
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun getBooksByGroup(group: Long): List
diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
index af3982275..5e1fe7684 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
@@ -25,6 +25,7 @@ interface BookGroupDao {
or (groupId = -1 and show > 0)
or (groupId = -2 and show > 0 and (select count(*) from books where type & ${BookType.local} > 0) > 0)
or (groupId = -3 and show > 0 and (select count(*) from books where type & ${BookType.audio} > 0) > 0)
+ or (groupId = -11 and show > 0 and (select count(*) from books where type & ${BookType.updateError} > 0) > 0)
or (groupId = -4 and show > 0
and (
select count(*) from books
diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
index 9e0ec4dbc..f0a689843 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
@@ -134,6 +134,9 @@ interface BookSourceDao {
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
val allGroupsUnProcessed: List
+ @get:Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
+ val allEnabledGroupsUnProcessed: List
+
@Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource?
@@ -175,6 +178,11 @@ interface BookSourceDao {
return dealGroups(allGroupsUnProcessed)
}
+ val allEnabledGroups: List
+ get() {
+ return dealGroups(allEnabledGroupsUnProcessed)
+ }
+
fun flowGroups(): Flow> {
return flowGroupsUnProcessed().map { list ->
dealGroups(list)
diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt
index 910c82c85..91686a122 100644
--- a/app/src/main/java/io/legado/app/help/JsExtensions.kt
+++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt
@@ -166,7 +166,7 @@ interface JsExtensions {
fun importScript(path: String): String {
val result = when {
path.startsWith("http") -> cacheFile(path) ?: ""
- path.isContentScheme() -> DocumentUtils.readText(appCtx, Uri.parse(path))
+ path.isUri() -> Uri.parse(path).readText(appCtx)
path.startsWith("/storage") -> FileUtils.readText(path)
else -> readTxtFile(path)
}
diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
index 15b6c7912..3600881c7 100644
--- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
+++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
@@ -1,17 +1,16 @@
+@file:Suppress("unused")
+
package io.legado.app.help.book
import android.net.Uri
-import androidx.documentfile.provider.DocumentFile
import io.legado.app.constant.BookSourceType
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
-import io.legado.app.help.config.AppConfig.defaultBookTreeUri
import io.legado.app.exception.NoStackTraceException
-import io.legado.app.utils.isContentScheme
-import io.legado.app.utils.getFile
+import io.legado.app.help.config.AppConfig.defaultBookTreeUri
+import io.legado.app.utils.*
import java.io.File
-import splitties.init.appCtx
val Book.isAudio: Boolean
@@ -54,25 +53,16 @@ val Book.isOnLineTxt: Boolean
fun Book.getLocalUri(): Uri {
if (isLocal) {
- val originBookUri = if (bookUrl.isContentScheme()) {
- Uri.parse(bookUrl)
- } else {
- Uri.fromFile(File(bookUrl))
- }
+ val originBookUri = if (bookUrl.isContentScheme()) {
+ Uri.parse(bookUrl)
+ } else {
+ Uri.fromFile(File(bookUrl))
+ }
//不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件
defaultBookTreeUri ?: return originBookUri
val treeUri = Uri.parse(defaultBookTreeUri)
- return if (treeUri.isContentScheme()) {
- DocumentFile.fromTreeUri(appCtx, treeUri)?.run {
- findFile(originName)?.let {
- if (it.exists()) it.uri else originBookUri
- } ?: originBookUri
- } ?: originBookUri
- } else {
- val treeFile = File(treeUri.path!!)
- val file = treeFile.getFile(originName)
- if (file.exists()) Uri.fromFile(file) else originBookUri
- }
+ val treeFileDoc = FileDoc.fromUri(treeUri, true)
+ return treeFileDoc.find(originName, 5)?.uri ?: originBookUri
}
throw NoStackTraceException("不是本地书籍")
}
@@ -84,6 +74,27 @@ fun Book.getRemoteUrl(): String? {
return null
}
+fun Book.setType(@BookType.Type vararg types: Int) {
+ type = 0
+ addType(*types)
+}
+
+fun Book.addType(@BookType.Type vararg types: Int) {
+ types.forEach {
+ type = type or it
+ }
+}
+
+fun Book.removeType(@BookType.Type vararg types: Int) {
+ types.forEach {
+ type = type and it.inv()
+ }
+}
+
+fun Book.clearType() {
+ type = 0
+}
+
fun Book.upType() {
if (type < 8) {
type = when (type) {
diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
index 16c82fabd..72e87ed5f 100644
--- a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
+++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
@@ -145,7 +145,7 @@ class Coroutine(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): Job {
- return (scope + Dispatchers.Main).launch(start = startOption) {
+ return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
try {
start?.let { dispatchVoidCallback(this, it) }
ensureActive()
diff --git a/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt
index 30e32e364..946d6d42a 100644
--- a/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt
+++ b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt
@@ -75,4 +75,14 @@ suspend fun BookSource.clearExploreKindsCache() {
aCache.remove(exploreKindsKey)
exploreKindsMap.remove(getExploreKindsKey())
}
+}
+
+fun BookSource.contains(word: String?): Boolean {
+ if (word.isNullOrEmpty()) {
+ return true
+ }
+ return bookSourceName.contains(word)
+ || bookSourceUrl.contains(word)
+ || bookSourceGroup?.contains(word) == true
+ || bookSourceComment?.contains(word) == true
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
index 414c1db19..015118ccb 100644
--- a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
+++ b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
@@ -19,7 +19,7 @@ object ImportOldData {
when (doc.name) {
"myBookShelf.json" ->
kotlin.runCatching {
- DocumentUtils.readText(context, doc.uri).let { json ->
+ doc.uri.readText(context).let { json ->
val importCount = importOldBookshelf(json)
context.toastOnUi("成功导入书籍${importCount}")
}
@@ -28,7 +28,7 @@ object ImportOldData {
}
"myBookSource.json" ->
kotlin.runCatching {
- DocumentUtils.readText(context, doc.uri).let { json ->
+ doc.uri.readText(context).let { json ->
val importCount = importOldSource(json)
context.toastOnUi("成功导入书源${importCount}")
}
@@ -37,7 +37,7 @@ object ImportOldData {
}
"myBookReplaceRule.json" ->
kotlin.runCatching {
- DocumentUtils.readText(context, doc.uri).let { json ->
+ doc.uri.readText(context).let { json ->
val importCount = importOldReplaceRule(json)
context.toastOnUi("成功导入替换规则${importCount}")
}
diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
index d028d4953..8ceeafe93 100644
--- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
+++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
@@ -358,9 +358,9 @@ open class WebDav(val path: String, val authorization: Authorization) {
val exception = document.getElementsByTag("s:exception").firstOrNull()?.text()
val message = document.getElementsByTag("s:message").firstOrNull()?.text()
if (exception == "ObjectNotFound") {
- throw ObjectNotFoundException(message ?: "$path doesn't exist")
+ throw ObjectNotFoundException(message ?: "$path doesn't exist. code:${response.code}")
}
- throw WebDavException(message ?: "未知错误")
+ throw WebDavException(message ?: "未知错误 code:${response.code}")
}
}
diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt
index 8949774e9..a5354cabf 100644
--- a/app/src/main/java/io/legado/app/service/AudioPlayService.kt
+++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt
@@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
import android.media.AudioManager
import android.net.Uri
import android.os.Build
+import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
@@ -36,6 +37,7 @@ import io.legado.app.utils.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main
import splitties.systemservices.audioManager
+import splitties.systemservices.powerManager
/**
* 音频播放服务
@@ -61,6 +63,9 @@ class AudioPlayService : BaseService(),
private set
}
+ private val wakeLock by lazy {
+ powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
+ }
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
@@ -82,8 +87,10 @@ class AudioPlayService : BaseService(),
private var upPlayProgressJob: Job? = null
private var playSpeed: Float = 1f
+ @SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
+ wakeLock.acquire()
isRun = true
upNotification()
exoPlayer.addListener(this)
@@ -120,6 +127,7 @@ class AudioPlayService : BaseService(),
override fun onDestroy() {
super.onDestroy()
+ wakeLock.release()
isRun = false
abandonFocus()
exoPlayer.release()
diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
index 2a185fa1d..7547ad987 100644
--- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
+++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
@@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.media.AudioManager
+import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper
@@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import splitties.systemservices.audioManager
+import splitties.systemservices.powerManager
/**
* 朗读服务
@@ -55,6 +57,9 @@ abstract class BaseReadAloudService : BaseService(),
}
}
+ private val wakeLock by lazy {
+ powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:readAloud")
+ }
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
@@ -77,8 +82,10 @@ abstract class BaseReadAloudService : BaseService(),
}
}
+ @SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
+ wakeLock.acquire()
isRun = true
pause = false
initMediaSession()
@@ -90,6 +97,7 @@ abstract class BaseReadAloudService : BaseService(),
override fun onDestroy() {
super.onDestroy()
+ wakeLock.release()
isRun = false
pause = true
abandonFocus()
diff --git a/app/src/main/java/io/legado/app/service/WebService.kt b/app/src/main/java/io/legado/app/service/WebService.kt
index c18c95f82..1f408e143 100644
--- a/app/src/main/java/io/legado/app/service/WebService.kt
+++ b/app/src/main/java/io/legado/app/service/WebService.kt
@@ -1,8 +1,10 @@
package io.legado.app.service
+import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
+import android.os.PowerManager
import androidx.core.app.NotificationCompat
import io.legado.app.R
import io.legado.app.base.BaseService
@@ -14,6 +16,7 @@ import io.legado.app.receiver.NetworkChangedListener
import io.legado.app.utils.*
import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer
+import splitties.systemservices.powerManager
import java.io.IOException
@@ -33,6 +36,9 @@ class WebService : BaseService() {
}
+ private val wakeLock by lazy {
+ powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
+ }
private var httpServer: HttpServer? = null
private var webSocketServer: WebSocketServer? = null
private var notificationContent = ""
@@ -40,8 +46,10 @@ class WebService : BaseService() {
NetworkChangedListener(this)
}
+ @SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
+ wakeLock.acquire()
isRun = true
notificationContent = getString(R.string.service_starting)
upNotification()
@@ -73,6 +81,7 @@ class WebService : BaseService() {
override fun onDestroy() {
super.onDestroy()
+ wakeLock.release()
networkChangedListener.unRegister()
isRun = false
if (httpServer?.isAlive == true) {
diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
index 0f6a924e1..ca484936b 100644
--- a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
@@ -2,16 +2,11 @@ package io.legado.app.ui.association
import android.app.Application
import android.net.Uri
-import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern.bookFileRegex
-import io.legado.app.exception.NoStackTraceException
import io.legado.app.model.localBook.LocalBook
-import io.legado.app.utils.inputStream
-import io.legado.app.utils.isJson
-import io.legado.app.utils.printOnDebug
-import java.io.File
+import io.legado.app.utils.*
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
val importBookLiveData = MutableLiveData()
@@ -24,15 +19,9 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
execute {
lateinit var fileName: String
//如果是普通的url,需要根据返回的内容判断是什么
- if (uri.scheme == "file" || uri.scheme == "content") {
- fileName = if (uri.scheme == "file") {
- val file = File(uri.path.toString())
- file.name
- } else {
- val file = DocumentFile.fromSingleUri(context, uri)
- if (file?.exists() != true) throw NoStackTraceException("文件不存在")
- file.name ?: ""
- }
+ if (uri.isContentScheme() || uri.isFileScheme()) {
+ val fileDoc = FileDoc.fromUri(uri, false)
+ fileName = fileDoc.name
kotlin.runCatching {
if (uri.inputStream(context).isJson()) {
importJson(uri)
diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt
index 2eafcce82..63e6bbc22 100644
--- a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt
@@ -46,22 +46,19 @@ class ImportBookActivity : BaseImportBookActivity
- if (uri.isContentScheme()) {
- AppConfig.importBookPath = uri.toString()
- initRootDoc(true)
- } else {
- AppConfig.importBookPath = uri.path
- initRootDoc(true)
- }
+ AppConfig.importBookPath = uri.toString()
+ initRootDoc(true)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setTitle(R.string.book_local)
launch {
- setBookStorage()
initView()
initEvent()
+ if (setBookStorage() && AppConfig.importBookPath.isNullOrBlank()) {
+ AppConfig.importBookPath = AppConfig.defaultBookTreeUri
+ }
initData()
}
}
@@ -150,35 +147,43 @@ class ImportBookActivity : BaseImportBookActivity upPath()
- lastPath.isNullOrEmpty() -> {
+ if (viewModel.rootDoc != null && !changedFolder) {
+ upPath()
+ } else {
+ val lastPath = AppConfig.importBookPath
+ if (lastPath.isNullOrBlank()) {
binding.tvEmptyMsg.visible()
selectFolder.launch()
- }
- lastPath.isContentScheme() -> {
- val rootUri = Uri.parse(lastPath)
- kotlin.runCatching {
- val doc = DocumentFile.fromTreeUri(this, rootUri)
- if (doc == null || doc.name.isNullOrEmpty()) {
+ } else {
+ val rootUri = if (lastPath.isUri()) {
+ Uri.parse(lastPath)
+ } else {
+ Uri.fromFile(File(lastPath))
+ }
+ when {
+ rootUri.isContentScheme() -> {
+ kotlin.runCatching {
+ val doc = DocumentFile.fromTreeUri(this, rootUri)
+ if (doc == null || doc.name.isNullOrEmpty()) {
+ binding.tvEmptyMsg.visible()
+ selectFolder.launch()
+ } else {
+ viewModel.subDocs.clear()
+ viewModel.rootDoc = FileDoc.fromDocumentFile(doc)
+ upDocs(viewModel.rootDoc!!)
+ }
+ }.onFailure {
+ binding.tvEmptyMsg.visible()
+ selectFolder.launch()
+ }
+ }
+ Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
binding.tvEmptyMsg.visible()
selectFolder.launch()
- } else {
- viewModel.subDocs.clear()
- viewModel.rootDoc = FileDoc.fromDocumentFile(doc)
- upDocs(viewModel.rootDoc!!)
}
- }.onFailure {
- binding.tvEmptyMsg.visible()
- selectFolder.launch()
+ else -> initRootPath(rootUri.path!!)
}
}
- Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
- binding.tvEmptyMsg.visible()
- selectFolder.launch()
- }
- else -> initRootPath(lastPath)
}
}
@@ -228,7 +233,7 @@ class ImportBookActivity : BaseImportBookActivity
+ val docList = fileDoc.list { item ->
when {
item.name.startsWith(".") -> false
item.isDir -> true
else -> item.name.matches(bookFileRegex)
}
}
- dataCallback?.setItems(docList)
+ dataCallback?.setItems(docList!!)
}.onError {
context.toastOnUi("获取文件列表出错\n${it.localizedMessage}")
}
@@ -132,7 +132,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
kotlin.runCatching {
val list = ArrayList()
- DocumentUtils.listFiles(fileDoc.uri).forEach { docItem ->
+ fileDoc.list()!!.forEach { docItem ->
if (!scope.isActive) {
finally?.invoke()
return
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
index 7b67229be..cf051edff 100644
--- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
@@ -18,6 +18,7 @@ import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.removeType
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException
import io.legado.app.model.BookCover
@@ -236,6 +237,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
bookSource = source
bookData.value?.changeTo(book, toc)
if (inBookshelf) {
+ book.removeType(BookType.updateError)
appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray())
}
diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
index b7b4d6302..62ee98767 100644
--- a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
@@ -184,6 +184,7 @@ class BookshelfManageActivity :
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
+ AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(viewModel.groupId)
}.conflate().map { list ->
val books = if (searchKey.isNullOrBlank()) {
diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
index 341d612b0..a28561cad 100644
--- a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
@@ -3,10 +3,12 @@ package io.legado.app.ui.book.manage
import android.app.Application
import androidx.compose.runtime.mutableStateOf
import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.removeType
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.toastOnUi
@@ -58,6 +60,7 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
context.toastOnUi("获取目录出错\n${it.localizedMessage}")
}.getOrNull()?.let { toc ->
book.changeTo(newBook, toc)
+ book.removeType(BookType.updateError)
appDb.bookDao.insert(newBook)
appDb.bookChapterDao.insert(*toc.toTypedArray())
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
index 5b6444171..2743662d7 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
@@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
+import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@@ -19,6 +20,7 @@ import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.removeType
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadAloud
@@ -215,6 +217,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
changeSourceCoroutine = execute {
ReadBook.upMsg(context.getString(R.string.loading))
ReadBook.book?.changeTo(book, toc)
+ book.removeType(BookType.updateError)
appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray())
ReadBook.resetData(book)
diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
index 210b27160..fc94136ff 100644
--- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
@@ -221,6 +221,9 @@ class SearchActivity : VMBaseActivity) : this(groups.joinToString(","))
- constructor(source: BookSource) : this("${source.bookSourceName}::${source.bookSourceUrl}")
+ constructor(source: BookSource) : this(
+ "${
+ source.bookSourceName.replace("::", "")
+ }::${source.bookSourceUrl}"
+ )
override fun toString(): String {
return scope
}
+ val stateLiveData = MutableLiveData("")
+
fun update(scope: String) {
this.scope = scope
}
@@ -54,9 +61,9 @@ data class SearchScope(private var scope: String) {
}
if (list.isEmpty()) {
list.add("全部书源")
+ }
+ return list
}
- return list
- }
/**
* 搜索范围书源
@@ -70,12 +77,20 @@ data class SearchScope(private var scope: String) {
}
}
} else {
- scope.splitNotBlank(",").forEach {
- list.addAll(appDb.bookSourceDao.getByGroup(it))
+ val oldScope = scope.splitNotBlank(",")
+ val newScope = oldScope.filter {
+ val bookSources = appDb.bookSourceDao.getEnabledByGroup(it)
+ list.addAll(bookSources)
+ bookSources.isNotEmpty()
+ }
+ if (oldScope.size != newScope.size) {
+ update(newScope)
+ stateLiveData.postValue("")
}
}
if (list.isEmpty()) {
scope = ""
+ stateLiveData.postValue("")
return appDb.bookSourceDao.allEnabled
}
return list.sortedBy { it.customOrder }
diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
index 7824d8579..b478d6957 100644
--- a/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
@@ -14,6 +14,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogSearchScopeBinding
import io.legado.app.databinding.ItemCheckBoxBinding
import io.legado.app.databinding.ItemRadioButtonBinding
+import io.legado.app.help.source.contains
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.setLayout
@@ -72,7 +73,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
}
private fun initOtherView() {
- binding.rgScope.setOnCheckedChangeListener { group, checkedId ->
+ binding.rgScope.setOnCheckedChangeListener { _, checkedId ->
binding.toolBar.menu.findItem(R.id.menu_screen)?.isVisible = checkedId == R.id.rb_source
upData()
}
@@ -101,7 +102,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
private fun initData() {
launch {
groups = withContext(IO) {
- appDb.bookSourceDao.allGroups
+ appDb.bookSourceDao.allEnabledGroups
}
sources = withContext(IO) {
appDb.bookSourceDao.allEnabled
@@ -116,9 +117,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
withContext(IO) {
if (binding.rbSource.isChecked) {
sources.filter { source ->
- screenText?.let { screenText ->
- source.bookSourceName.contains(screenText)
- } ?: true
+ source.contains(screenText)
}.let {
screenSources.clear()
screenSources.addAll(it)
diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
index 4b65b2695..477f273ac 100644
--- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
@@ -193,7 +193,7 @@ class SearchContentActivity :
}
}
- val isLocalBook: Boolean
+ private val isLocalBook: Boolean
get() = viewModel.book?.isLocal == true
override fun openSearchResult(searchResult: SearchResult, index: Int) {
diff --git a/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt
index 73b1de7e1..09b887f06 100644
--- a/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt
@@ -24,6 +24,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import java.io.File
+/**
+ * 字体选择对话框
+ */
class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
Toolbar.OnMenuItemClickListener,
FontAdapter.CallBack {
@@ -35,11 +38,11 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
}
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
- if (uri.toString().isContentScheme()) {
+ if (uri.isContentScheme()) {
putPrefString(PreferKey.fontFolder, uri.toString())
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc != null) {
- loadFontFiles(doc)
+ loadFontFiles(FileDoc.fromDocumentFile(doc))
} else {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
loadFontFilesByPermission(path)
@@ -75,7 +78,7 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
if (fontPath.isContentScheme()) {
val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath))
if (doc?.canRead() == true) {
- loadFontFiles(doc)
+ loadFontFiles(FileDoc.fromDocumentFile(doc))
} else {
openFolder()
}
@@ -117,40 +120,29 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
private fun getLocalFonts(): ArrayList {
val path = FileUtils.getPath(requireContext().externalFiles, "font")
- return DocumentUtils.listFiles(path) {
+ return File(path).listFileDocs {
it.name.matches(fontRegex)
}
}
- private fun loadFontFiles(doc: DocumentFile) {
- execute {
- val fontItems = DocumentUtils.listFiles(doc.uri) {
- it.name.matches(fontRegex)
- }
- mergeFontItems(fontItems, getLocalFonts())
- }.onSuccess {
- adapter.setItems(it)
- }.onError {
- toastOnUi("getFontFiles:${it.localizedMessage}")
- }
- }
-
private fun loadFontFilesByPermission(path: String) {
PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
- loadFontFiles(path)
+ loadFontFiles(
+ FileDoc.fromFile(File(path))
+ )
}
.request()
}
- private fun loadFontFiles(path: String) {
+ private fun loadFontFiles(fileDoc: FileDoc) {
execute {
- val fontItems = DocumentUtils.listFiles(path) {
+ val fontItems = fileDoc.list {
it.name.matches(fontRegex)
}
- mergeFontItems(fontItems, getLocalFonts())
+ mergeFontItems(fontItems!!, getLocalFonts())
}.onSuccess {
adapter.setItems(it)
}.onError {
diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
index b6629fa99..d09ef387d 100644
--- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
+import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@@ -12,7 +13,9 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppWebDav
import io.legado.app.help.DefaultData
import io.legado.app.help.book.BookHelp
+import io.legado.app.help.book.addType
import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.removeType
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CacheBook
@@ -126,6 +129,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
WebBook.getBookInfoAwait(source, book)
}
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
+ book.removeType(BookType.updateError)
if (book.bookUrl == bookUrl) {
appDb.bookDao.update(book)
} else {
@@ -137,6 +141,8 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*toc.toTypedArray())
addDownload(source, book)
}.onError(upTocPool) {
+ book.addType(BookType.updateError)
+ appDb.bookDao.update(book)
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
}.onCancel(upTocPool) {
upTocCancel(bookUrl)
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
index 91fa9318a..a2a434497 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
@@ -118,6 +118,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
+ AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) {
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
index e6bfa36c8..ecfa099ad 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
@@ -130,6 +130,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
+ AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) {
diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
index 427632170..4d0f74306 100644
--- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
+++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
@@ -15,6 +15,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.FragmentRssBinding
import io.legado.app.databinding.ItemRssBinding
+import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.rss.article.RssSortActivity
@@ -179,7 +180,13 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss),
}
override fun del(rssSource: RssSource) {
- viewModel.del(rssSource)
+ alert(R.string.draw) {
+ setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName)
+ noButton()
+ yesButton {
+ viewModel.del(rssSource)
+ }
+ }
}
override fun disable(rssSource: RssSource) {
diff --git a/app/src/main/java/io/legado/app/utils/DocumentExtensions.kt b/app/src/main/java/io/legado/app/utils/DocumentExtensions.kt
deleted file mode 100644
index 5e2411706..000000000
--- a/app/src/main/java/io/legado/app/utils/DocumentExtensions.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-package io.legado.app.utils
-
-import android.content.Context
-import android.database.Cursor
-import android.net.Uri
-import android.provider.DocumentsContract
-import androidx.documentfile.provider.DocumentFile
-import io.legado.app.exception.NoStackTraceException
-import splitties.init.appCtx
-import java.io.File
-import java.nio.charset.Charset
-
-
-@Suppress("MemberVisibilityCanBePrivate")
-object DocumentUtils {
-
- fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean {
- val parent = getDirDocument(root, *subDirs) ?: return false
- return parent.findFile(fileName)?.exists() ?: false
- }
-
- fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) {
- val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
- parent?.findFile(fileName)?.delete()
- }
-
- fun createFileIfNotExist(
- root: DocumentFile,
- fileName: String,
- mimeType: String = "",
- vararg subDirs: String
- ): DocumentFile? {
- val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
- return parent?.findFile(fileName) ?: parent?.createFile(mimeType, fileName)
- }
-
- fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? {
- var parent: DocumentFile? = root
- for (subDirName in subDirs) {
- val subDir = parent?.findFile(subDirName)
- ?: parent?.createDirectory(subDirName)
- parent = subDir
- }
- return parent
- }
-
- fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? {
- var parent = root
- for (subDirName in subDirs) {
- val subDir = parent.findFile(subDirName)
- parent = subDir ?: return null
- }
- return parent
- }
-
- @JvmStatic
- @Throws(Exception::class)
- fun writeText(
- context: Context,
- data: String,
- fileUri: Uri,
- charset: Charset = Charsets.UTF_8
- ): Boolean {
- return writeBytes(context, data.toByteArray(charset), fileUri)
- }
-
- @JvmStatic
- @Throws(Exception::class)
- fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean {
- context.contentResolver.openOutputStream(fileUri)?.let {
- it.write(data)
- it.close()
- return true
- }
- return false
- }
-
- @JvmStatic
- @Throws(Exception::class)
- fun readText(context: Context, uri: Uri): String {
- return String(readBytes(context, uri))
- }
-
- @JvmStatic
- @Throws(Exception::class)
- fun readBytes(context: Context, uri: Uri): ByteArray {
- context.contentResolver.openInputStream(uri)?.let {
- val len: Int = it.available()
- val buffer = ByteArray(len)
- it.read(buffer)
- it.close()
- return buffer
- } ?: throw NoStackTraceException("打开文件失败\n${uri}")
- }
-
- @Throws(Exception::class)
- fun listFiles(uri: Uri, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList {
- if (!uri.isContentScheme()) {
- return listFiles(uri.path!!, filter)
- }
- val childrenUri = DocumentsContract
- .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
- val docList = arrayListOf()
- var cursor: Cursor? = null
- try {
- cursor = appCtx.contentResolver.query(
- childrenUri, arrayOf(
- DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- DocumentsContract.Document.COLUMN_DISPLAY_NAME,
- DocumentsContract.Document.COLUMN_LAST_MODIFIED,
- DocumentsContract.Document.COLUMN_SIZE,
- DocumentsContract.Document.COLUMN_MIME_TYPE
- ), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME
- )
- cursor?.let {
- val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
- val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
- val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
- val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
- val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
- if (cursor.moveToFirst()) {
- do {
- val item = FileDoc(
- name = cursor.getString(nci),
- isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR,
- size = cursor.getLong(sci),
- lastModified = cursor.getLong(dci),
- uri = DocumentsContract
- .buildDocumentUriUsingTree(uri, cursor.getString(ici))
- )
- if (filter == null || filter.invoke(item)) {
- docList.add(item)
- }
- } while (cursor.moveToNext())
- }
- }
- } finally {
- cursor?.close()
- }
- return docList
- }
-
- @Throws(Exception::class)
- fun listFiles(path: String, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList {
- val docList = arrayListOf()
- val file = File(path)
- file.listFiles()?.forEach {
- val item = FileDoc(
- it.name,
- it.isDirectory,
- it.length(),
- it.lastModified(),
- Uri.fromFile(it)
- )
- if (filter == null || filter.invoke(item)) {
- docList.add(item)
- }
- }
- return docList
- }
-
-}
-
-data class FileDoc(
- val name: String,
- val isDir: Boolean,
- val size: Long,
- val lastModified: Long,
- val uri: Uri
-) {
-
- override fun toString(): String {
- return if (uri.isContentScheme()) uri.toString() else uri.path!!
- }
-
- val isContentScheme get() = uri.isContentScheme()
-
- fun readBytes(): ByteArray {
- return uri.readBytes(appCtx)
- }
-
- companion object {
-
- fun fromDocumentFile(doc: DocumentFile): FileDoc {
- return FileDoc(
- name = doc.name ?: "",
- isDir = doc.isDirectory,
- size = doc.length(),
- lastModified = doc.lastModified(),
- uri = doc.uri
- )
- }
-
- fun fromFile(file: File): FileDoc {
- return FileDoc(
- name = file.name,
- isDir = file.isDirectory,
- size = file.length(),
- lastModified = file.lastModified(),
- uri = Uri.fromFile(file)
- )
- }
-
- }
-}
-
-@Throws(Exception::class)
-fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
- DocumentUtils.writeText(context, data, this.uri, charset)
-}
-
-@Throws(Exception::class)
-fun DocumentFile.writeBytes(context: Context, data: ByteArray) {
- DocumentUtils.writeBytes(context, data, this.uri)
-}
-
-@Throws(Exception::class)
-fun DocumentFile.readText(context: Context): String {
- return DocumentUtils.readText(context, this.uri)
-}
-
-@Throws(Exception::class)
-fun DocumentFile.readBytes(context: Context): ByteArray {
- return DocumentUtils.readBytes(context, this.uri)
-}
diff --git a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt
new file mode 100644
index 000000000..c2b93c61c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt
@@ -0,0 +1,47 @@
+package io.legado.app.utils
+
+import androidx.documentfile.provider.DocumentFile
+
+@Suppress("MemberVisibilityCanBePrivate")
+object DocumentUtils {
+
+ fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean {
+ val parent = getDirDocument(root, *subDirs) ?: return false
+ return parent.findFile(fileName)?.exists() ?: false
+ }
+
+ fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) {
+ val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
+ parent?.findFile(fileName)?.delete()
+ }
+
+ fun createFileIfNotExist(
+ root: DocumentFile,
+ fileName: String,
+ mimeType: String = "",
+ vararg subDirs: String
+ ): DocumentFile? {
+ val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
+ return parent?.findFile(fileName) ?: parent?.createFile(mimeType, fileName)
+ }
+
+ fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? {
+ var parent: DocumentFile? = root
+ for (subDirName in subDirs) {
+ val subDir = parent?.findFile(subDirName)
+ ?: parent?.createDirectory(subDirName)
+ parent = subDir
+ }
+ return parent
+ }
+
+ fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? {
+ var parent = root
+ for (subDirName in subDirs) {
+ val subDir = parent.findFile(subDirName)
+ parent = subDir ?: return null
+ }
+ return parent
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
new file mode 100644
index 000000000..798262602
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
@@ -0,0 +1,255 @@
+@file:Suppress("unused")
+
+package io.legado.app.utils
+
+import android.app.DownloadManager
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.DocumentsContract
+import androidx.documentfile.provider.DocumentFile
+import io.legado.app.exception.NoStackTraceException
+import splitties.init.appCtx
+import splitties.systemservices.downloadManager
+import java.io.File
+import java.nio.charset.Charset
+
+
+data class FileDoc(
+ val name: String,
+ val isDir: Boolean,
+ val size: Long,
+ val lastModified: Long,
+ val uri: Uri
+) {
+
+ override fun toString(): String {
+ return if (uri.isContentScheme()) uri.toString() else uri.path!!
+ }
+
+ val isContentScheme get() = uri.isContentScheme()
+
+ fun readBytes(): ByteArray {
+ return uri.readBytes(appCtx)
+ }
+
+ companion object {
+
+ fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
+ if (uri.isContentScheme()) {
+ val doc = if (isDir) {
+ DocumentFile.fromTreeUri(appCtx, uri)!!
+ } else if (uri.host == "downloads") {
+ val query = DownloadManager.Query()
+ query.setFilterById(uri.lastPathSegment!!.toLong())
+ downloadManager.query(query).use {
+ if (it.moveToFirst()) {
+ val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
+ val lUri = it.getString(lUriColum)
+ DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!!
+ } else {
+ DocumentFile.fromSingleUri(appCtx, uri)!!
+ }
+ }
+ } else {
+ DocumentFile.fromSingleUri(appCtx, uri)!!
+ }
+ return FileDoc(doc.name ?: "", isDir, doc.length(), doc.lastModified(), doc.uri)
+ }
+ val file = File(uri.path!!)
+ return FileDoc(file.name, isDir, file.length(), file.lastModified(), uri)
+ }
+
+ fun fromDocumentFile(doc: DocumentFile): FileDoc {
+ return FileDoc(
+ name = doc.name ?: "",
+ isDir = doc.isDirectory,
+ size = doc.length(),
+ lastModified = doc.lastModified(),
+ uri = doc.uri
+ )
+ }
+
+ fun fromFile(file: File): FileDoc {
+ return FileDoc(
+ name = file.name,
+ isDir = file.isDirectory,
+ size = file.length(),
+ lastModified = file.lastModified(),
+ uri = Uri.fromFile(file)
+ )
+ }
+
+ }
+}
+
+/**
+ * 过滤器
+ */
+typealias FileDocFilter = (file: FileDoc) -> Boolean
+
+private val projection by lazy {
+ arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ DocumentsContract.Document.COLUMN_SIZE,
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ )
+}
+
+/**
+ * 返回子文件列表,如果不是文件夹则返回null
+ */
+fun FileDoc.list(filter: FileDocFilter? = null): ArrayList? {
+ if (isDir) {
+ if (uri.isContentScheme()) {
+ /**
+ * DocumentFile 的 listFiles() 非常的慢,所以这里直接从数据库查询
+ */
+ val childrenUri = DocumentsContract
+ .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
+ val docList = arrayListOf()
+ var cursor: Cursor? = null
+ try {
+ cursor = appCtx.contentResolver.query(
+ childrenUri,
+ projection,
+ null,
+ null,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME
+ )
+ cursor?.let {
+ val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
+ val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
+ val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
+ val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
+ val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
+ if (cursor.moveToFirst()) {
+ do {
+ val item = FileDoc(
+ name = cursor.getString(nci),
+ isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR,
+ size = cursor.getLong(sci),
+ lastModified = cursor.getLong(dci),
+ uri = DocumentsContract.buildDocumentUriUsingTree(
+ uri,
+ cursor.getString(ici)
+ )
+ )
+ if (filter == null || filter.invoke(item)) {
+ docList.add(item)
+ }
+ } while (cursor.moveToNext())
+ }
+ }
+ } finally {
+ cursor?.close()
+ }
+ return docList
+ } else {
+ return File(uri.path!!).listFileDocs(filter)
+ }
+ }
+ return null
+}
+
+/**
+ * 查找文档, 如果存在则返回文档,如果不存在返回空
+ * @param name 文件名
+ * @param depth 查找文件夹深度
+ */
+fun FileDoc.find(name: String, depth: Int = 0): FileDoc? {
+ val list = list()
+ list?.forEach {
+ if (it.name == name) {
+ return it
+ }
+ }
+ if (depth > 0) {
+ list?.forEach {
+ if (it.isDir) {
+ val fileDoc = it.find(name, depth - 1)
+ if (fileDoc != null) {
+ return fileDoc
+ }
+ }
+ }
+ }
+ return null
+}
+
+fun FileDoc.createFileIfNotExist(
+ fileName: String,
+ mimeType: String = "",
+ vararg subDirs: String
+): FileDoc {
+ return if (uri.isContentScheme()) {
+ val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
+ val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, mimeType, *subDirs)!!
+ FileDoc.fromDocumentFile(tmp)
+ } else {
+ val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
+ val tmp = FileUtils.createFileIfNotExist(path)
+ FileDoc.fromFile(tmp)
+ }
+}
+
+fun FileDoc.createFolderIfNotExist(
+ vararg subDirs: String
+): FileDoc {
+ return if (uri.isContentScheme()) {
+ val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
+ val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!!
+ FileDoc.fromDocumentFile(tmp)
+ } else {
+ val path = FileUtils.getPath(uri.path!!, *subDirs)
+ val tmp = FileUtils.createFolderIfNotExist(path)
+ FileDoc.fromFile(tmp)
+ }
+}
+
+fun FileDoc.exists(
+ fileName: String,
+ vararg subDirs: String
+): Boolean {
+ return if (uri.isContentScheme()) {
+ DocumentUtils.exists(DocumentFile.fromTreeUri(appCtx, uri)!!, fileName, *subDirs)
+ } else {
+ val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
+ FileUtils.exist(path)
+ }
+}
+
+/**
+ * DocumentFile 的 listFiles() 非常的慢,尽量不要使用
+ */
+fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList? {
+ return FileDoc.fromDocumentFile(this).list(filter)
+}
+
+@Throws(Exception::class)
+fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
+ uri.writeText(context, data, charset)
+}
+
+@Throws(Exception::class)
+fun DocumentFile.writeBytes(context: Context, data: ByteArray) {
+ uri.writeBytes(context, data)
+}
+
+@Throws(Exception::class)
+fun DocumentFile.readText(context: Context): String {
+ return String(readBytes(context))
+}
+
+@Throws(Exception::class)
+fun DocumentFile.readBytes(context: Context): ByteArray {
+ return context.contentResolver.openInputStream(uri)?.let {
+ val len: Int = it.available()
+ val buffer = ByteArray(len)
+ it.read(buffer)
+ it.close()
+ return buffer
+ } ?: throw NoStackTraceException("打开文件失败\n${uri}")
+}
diff --git a/app/src/main/java/io/legado/app/utils/FileExtensions.kt b/app/src/main/java/io/legado/app/utils/FileExtensions.kt
index 31f03c04c..8c2c07f3a 100644
--- a/app/src/main/java/io/legado/app/utils/FileExtensions.kt
+++ b/app/src/main/java/io/legado/app/utils/FileExtensions.kt
@@ -1,5 +1,6 @@
package io.legado.app.utils
+import android.net.Uri
import java.io.File
fun File.getFile(vararg subDirFiles: String): File {
@@ -11,3 +12,23 @@ fun File.exists(vararg subDirFiles: String): Boolean {
return getFile(*subDirFiles).exists()
}
+@Throws(Exception::class)
+fun File.listFileDocs(filter: FileDocFilter? = null): ArrayList {
+ val docList = arrayListOf()
+ listFiles()
+ listFiles()?.forEach {
+ val item = FileDoc(
+ it.name,
+ it.isDirectory,
+ it.length(),
+ it.lastModified(),
+ Uri.fromFile(it)
+ )
+ if (filter == null || filter.invoke(item)) {
+ docList.add(item)
+ }
+ }
+ return docList
+}
+
+
diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt
index 11fe7de72..4f6037d19 100644
--- a/app/src/main/java/io/legado/app/utils/FileUtils.kt
+++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt
@@ -4,7 +4,6 @@ import android.os.Environment
import android.webkit.MimeTypeMap
import androidx.annotation.IntDef
import splitties.init.appCtx
-
import java.io.*
import java.nio.charset.Charset
import java.text.SimpleDateFormat
@@ -115,7 +114,7 @@ object FileUtils {
const val BY_EXTENSION_DESC = 7
@IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC])
- @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
+ @Retention(AnnotationRetention.SOURCE)
annotation class SortType
/**
diff --git a/app/src/main/java/io/legado/app/utils/RegexExtensions.kt b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt
index 23a3ea14c..f1a4a300f 100644
--- a/app/src/main/java/io/legado/app/utils/RegexExtensions.kt
+++ b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt
@@ -1,35 +1,64 @@
package io.legado.app.utils
+import android.util.Log
+import androidx.core.os.postDelayed
import com.script.SimpleBindings
import io.legado.app.constant.AppConst
import io.legado.app.exception.RegexTimeoutException
+import io.legado.app.help.CrashHandler
+import io.legado.app.help.coroutine.Coroutine
+import kotlinx.coroutines.suspendCancellableCoroutine
+import splitties.init.appCtx
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+private val handler by lazy { buildMainHandler() }
/**
* 带有超时检测的正则替换
*/
-fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
- val startTime = System.currentTimeMillis()
- val charSequence = this
+suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
+ val charSequence = this@replace
val isJs = replacement.startsWith("@js:")
val replacement1 = if (isJs) replacement.substring(4) else replacement
- val pattern = regex.toPattern()
- val matcher = pattern.matcher(charSequence)
- val stringBuffer = StringBuffer()
- while (matcher.find()) {
- if (System.currentTimeMillis() - startTime > timeout) {
- val timeoutMsg = "替换超时,将禁用替换规则"
- throw RegexTimeoutException(timeoutMsg)
+ return suspendCancellableCoroutine { block ->
+ val coroutine = Coroutine.async {
+ try {
+ val pattern = regex.toPattern()
+ val matcher = pattern.matcher(charSequence)
+ val stringBuffer = StringBuffer()
+ while (matcher.find()) {
+ if (isJs) {
+ val bindings = SimpleBindings()
+ bindings["result"] = matcher.group()
+ val jsResult =
+ AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
+ matcher.appendReplacement(stringBuffer, jsResult)
+ } else {
+ matcher.appendReplacement(stringBuffer, replacement1)
+ }
+ }
+ matcher.appendTail(stringBuffer)
+ Log.e("regex", "end")
+ block.resume(stringBuffer.toString())
+ } catch (e: Exception) {
+ block.resumeWithException(e)
+ }
}
- if (isJs) {
- val bindings = SimpleBindings()
- bindings["result"] = matcher.group()
- val jsResult = AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
- matcher.appendReplacement(stringBuffer, jsResult)
- } else {
- matcher.appendReplacement(stringBuffer, replacement1)
+ handler.postDelayed(timeout) {
+ if (coroutine.isActive) {
+ val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
+ val exception = RegexTimeoutException(timeoutMsg)
+ block.cancel(exception)
+ appCtx.longToastOnUi(timeoutMsg)
+ CrashHandler.saveCrashInfo2File(exception)
+ handler.postDelayed(3000) {
+ if (coroutine.isActive) {
+ appCtx.restart()
+ }
+ }
+ }
}
}
- matcher.appendTail(stringBuffer)
- return stringBuffer.toString()
}
diff --git a/app/src/main/java/io/legado/app/utils/StringExtensions.kt b/app/src/main/java/io/legado/app/utils/StringExtensions.kt
index 9d62eec3e..d4ce56ab2 100644
--- a/app/src/main/java/io/legado/app/utils/StringExtensions.kt
+++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt
@@ -19,9 +19,7 @@ fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri {
- return if (isContentScheme()) {
- Uri.parse(this)
- } else {
+ return if (isUri()) Uri.parse(this) else {
Uri.fromFile(File(this))
}
}
diff --git a/app/src/main/java/io/legado/app/utils/UriExtensions.kt b/app/src/main/java/io/legado/app/utils/UriExtensions.kt
index aea7beff2..1c1fa6cc3 100644
--- a/app/src/main/java/io/legado/app/utils/UriExtensions.kt
+++ b/app/src/main/java/io/legado/app/utils/UriExtensions.kt
@@ -13,9 +13,12 @@ import io.legado.app.lib.permission.PermissionsCompat
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
+import java.nio.charset.Charset
fun Uri.isContentScheme() = this.scheme == "content"
+fun Uri.isFileScheme() = this.scheme == "file"
+
/**
* 读取URI
*/
@@ -100,7 +103,13 @@ fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputSt
@Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray {
return if (this.isContentScheme()) {
- DocumentUtils.readBytes(context, this)
+ context.contentResolver.openInputStream(this)?.let {
+ val len: Int = it.available()
+ val buffer = ByteArray(len)
+ it.read(buffer)
+ it.close()
+ return buffer
+ } ?: throw NoStackTraceException("打开文件失败\n${this}")
} else {
val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) {
@@ -124,7 +133,12 @@ fun Uri.writeBytes(
byteArray: ByteArray
): Boolean {
if (this.isContentScheme()) {
- return DocumentUtils.writeBytes(context, byteArray, this)
+ context.contentResolver.openOutputStream(this)?.let {
+ it.write(byteArray)
+ it.close()
+ return true
+ }
+ return false
} else {
val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) {
@@ -136,8 +150,8 @@ fun Uri.writeBytes(
}
@Throws(Exception::class)
-fun Uri.writeText(context: Context, text: String): Boolean {
- return writeBytes(context, text.toByteArray())
+fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean {
+ return writeBytes(context, text.toByteArray(charset))
}
fun Uri.writeBytes(
diff --git a/gradle.properties b/gradle.properties
index 30d03a858..9cb481bd8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m
+org.gradle.jvmargs=-Xmx3000m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects