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