Merge branch 'gedoor:master' into master

pull/2436/head
Xwite 3 years ago committed by GitHub
commit 3fbdfe682f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/src/main/AndroidManifest.xml
  2. 2
      app/src/main/assets/defaultData/directLinkUpload.json
  3. 12
      app/src/main/assets/updateLog.md
  4. 1
      app/src/main/java/io/legado/app/constant/AppConst.kt
  5. 8
      app/src/main/java/io/legado/app/constant/BookType.kt
  6. 7
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  7. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  8. 1
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  9. 8
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  10. 2
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  11. 43
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  12. 2
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  13. 10
      app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt
  14. 6
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  15. 4
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  16. 8
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  17. 8
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  18. 9
      app/src/main/java/io/legado/app/service/WebService.kt
  19. 19
      app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
  20. 31
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt
  21. 8
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt
  22. 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  23. 1
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
  24. 3
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
  25. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  26. 11
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  27. 21
      app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt
  28. 9
      app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
  29. 2
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
  30. 34
      app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt
  31. 6
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  32. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
  33. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
  34. 7
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  35. 225
      app/src/main/java/io/legado/app/utils/DocumentExtensions.kt
  36. 47
      app/src/main/java/io/legado/app/utils/DocumentUtils.kt
  37. 255
      app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
  38. 21
      app/src/main/java/io/legado/app/utils/FileExtensions.kt
  39. 3
      app/src/main/java/io/legado/app/utils/FileUtils.kt
  40. 47
      app/src/main/java/io/legado/app/utils/RegexExtensions.kt
  41. 4
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  42. 22
      app/src/main/java/io/legado/app/utils/UriExtensions.kt
  43. 2
      gradle.properties

@ -4,6 +4,7 @@
package="io.legado.app"> package="io.legado.app">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

@ -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", "downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result",
"summary": "有效期2天" "summary": "有效期2天"
} }

@ -11,13 +11,21 @@
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
**2022/10/13** **2022/10/16**
* 添加更新失败分组
* web服务和朗读服务添加唤醒锁
* 修复搜索禁用书源的bug
* 修复本地书籍很慢的问题
**2022/10/14**
* 更新cronet: 106.0.5249.126 * 更新cronet: 106.0.5249.126
* 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容 * 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容
* 书架管理添加筛选功能 * 书架管理添加筛选功能
* 搜索支持搜索范围多分组和单书源设置 * 搜索支持搜索范围多分组和单书源设置
* 书源管理界面书源菜单添加单书源搜索数据功能 * 书源管理界面书源菜单添加单书源搜索书籍功能
* 复web阅读时app未退出阅读界面导致的进度bug by Xwite
**2022/10/09** **2022/10/09**

@ -43,6 +43,7 @@ object AppConst {
const val bookGroupAudioId = -3L const val bookGroupAudioId = -3L
const val bookGroupNetNoneId = -4L const val bookGroupNetNoneId = -4L
const val bookGroupLocalNoneId = -5L const val bookGroupLocalNoneId = -5L
const val bookGroupErrorId = -11L
const val notificationIdRead = -1122391 const val notificationIdRead = -1122391
const val notificationIdAudio = -1122392 const val notificationIdAudio = -1122392

@ -12,6 +12,11 @@ object BookType {
*/ */
const val text = 0b1000 const val text = 0b1000
/**
* 16 更新失败
*/
const val updateError = 0b10000
/** /**
* 32 音频 * 32 音频
*/ */
@ -35,9 +40,10 @@ object BookType {
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(text, audio, image, webFile) @IntDef(text, updateError, audio, image, webFile, local)
annotation class Type annotation class Type
/** /**
* 本地书籍书源标志 * 本地书籍书源标志
*/ */

@ -105,9 +105,14 @@ abstract class AppDatabase : RoomDatabase() {
) )
db.execSQL( db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show) """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})""" 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 book_sources set loginUi = null where loginUi = 'null'")
db.execSQL("update rssSources 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'") db.execSQL("update httpTTS set loginUi = null where loginUi = 'null'")

@ -53,6 +53,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun flowSearch(key: String): Flow<List<Book>> fun flowSearch(key: String): Flow<List<Book>>
@Query("SELECT * FROM books where type & ${BookType.updateError} > 0 order by durChapterTime desc")
fun flowUpdateError(): Flow<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0") @Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun getBooksByGroup(group: Long): List<Book> fun getBooksByGroup(group: Long): List<Book>

@ -25,6 +25,7 @@ interface BookGroupDao {
or (groupId = -1 and show > 0) 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 = -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 = -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 or (groupId = -4 and show > 0
and ( and (
select count(*) from books select count(*) from books

@ -134,6 +134,9 @@ interface BookSourceDao {
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") @get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
val allGroupsUnProcessed: List<String> val allGroupsUnProcessed: List<String>
@get:Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
val allEnabledGroupsUnProcessed: List<String>
@Query("select * from book_sources where bookSourceUrl = :key") @Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource? fun getBookSource(key: String): BookSource?
@ -175,6 +178,11 @@ interface BookSourceDao {
return dealGroups(allGroupsUnProcessed) return dealGroups(allGroupsUnProcessed)
} }
val allEnabledGroups: List<String>
get() {
return dealGroups(allEnabledGroupsUnProcessed)
}
fun flowGroups(): Flow<List<String>> { fun flowGroups(): Flow<List<String>> {
return flowGroupsUnProcessed().map { list -> return flowGroupsUnProcessed().map { list ->
dealGroups(list) dealGroups(list)

@ -166,7 +166,7 @@ interface JsExtensions {
fun importScript(path: String): String { fun importScript(path: String): String {
val result = when { val result = when {
path.startsWith("http") -> cacheFile(path) ?: "" 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) path.startsWith("/storage") -> FileUtils.readText(path)
else -> readTxtFile(path) else -> readTxtFile(path)
} }

@ -1,17 +1,16 @@
@file:Suppress("unused")
package io.legado.app.help.book package io.legado.app.help.book
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.constant.BookSourceType import io.legado.app.constant.BookSourceType
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource 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.exception.NoStackTraceException
import io.legado.app.utils.isContentScheme import io.legado.app.help.config.AppConfig.defaultBookTreeUri
import io.legado.app.utils.getFile import io.legado.app.utils.*
import java.io.File import java.io.File
import splitties.init.appCtx
val Book.isAudio: Boolean val Book.isAudio: Boolean
@ -62,17 +61,8 @@ fun Book.getLocalUri(): Uri {
//不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件 //不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件
defaultBookTreeUri ?: return originBookUri defaultBookTreeUri ?: return originBookUri
val treeUri = Uri.parse(defaultBookTreeUri) val treeUri = Uri.parse(defaultBookTreeUri)
return if (treeUri.isContentScheme()) { val treeFileDoc = FileDoc.fromUri(treeUri, true)
DocumentFile.fromTreeUri(appCtx, treeUri)?.run { return treeFileDoc.find(originName, 5)?.uri ?: originBookUri
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
}
} }
throw NoStackTraceException("不是本地书籍") throw NoStackTraceException("不是本地书籍")
} }
@ -84,6 +74,27 @@ fun Book.getRemoteUrl(): String? {
return null 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() { fun Book.upType() {
if (type < 8) { if (type < 8) {
type = when (type) { type = when (type) {

@ -145,7 +145,7 @@ class Coroutine<T>(
context: CoroutineContext, context: CoroutineContext,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
): Job { ): Job {
return (scope + Dispatchers.Main).launch(start = startOption) { return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
try { try {
start?.let { dispatchVoidCallback(this, it) } start?.let { dispatchVoidCallback(this, it) }
ensureActive() ensureActive()

@ -76,3 +76,13 @@ suspend fun BookSource.clearExploreKindsCache() {
exploreKindsMap.remove(getExploreKindsKey()) 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
}

@ -19,7 +19,7 @@ object ImportOldData {
when (doc.name) { when (doc.name) {
"myBookShelf.json" -> "myBookShelf.json" ->
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json -> doc.uri.readText(context).let { json ->
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
context.toastOnUi("成功导入书籍${importCount}") context.toastOnUi("成功导入书籍${importCount}")
} }
@ -28,7 +28,7 @@ object ImportOldData {
} }
"myBookSource.json" -> "myBookSource.json" ->
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json -> doc.uri.readText(context).let { json ->
val importCount = importOldSource(json) val importCount = importOldSource(json)
context.toastOnUi("成功导入书源${importCount}") context.toastOnUi("成功导入书源${importCount}")
} }
@ -37,7 +37,7 @@ object ImportOldData {
} }
"myBookReplaceRule.json" -> "myBookReplaceRule.json" ->
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json -> doc.uri.readText(context).let { json ->
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
context.toastOnUi("成功导入替换规则${importCount}") context.toastOnUi("成功导入替换规则${importCount}")
} }

@ -358,9 +358,9 @@ open class WebDav(val path: String, val authorization: Authorization) {
val exception = document.getElementsByTag("s:exception").firstOrNull()?.text() val exception = document.getElementsByTag("s:exception").firstOrNull()?.text()
val message = document.getElementsByTag("s:message").firstOrNull()?.text() val message = document.getElementsByTag("s:message").firstOrNull()?.text()
if (exception == "ObjectNotFound") { 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}")
} }
} }

@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
@ -36,6 +37,7 @@ import io.legado.app.utils.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import splitties.systemservices.audioManager import splitties.systemservices.audioManager
import splitties.systemservices.powerManager
/** /**
* 音频播放服务 * 音频播放服务
@ -61,6 +63,9 @@ class AudioPlayService : BaseService(),
private set private set
} }
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
}
private val mFocusRequest: AudioFocusRequestCompat by lazy { private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this) MediaHelp.buildAudioFocusRequestCompat(this)
} }
@ -82,8 +87,10 @@ class AudioPlayService : BaseService(),
private var upPlayProgressJob: Job? = null private var upPlayProgressJob: Job? = null
private var playSpeed: Float = 1f private var playSpeed: Float = 1f
@SuppressLint("WakelockTimeout")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
wakeLock.acquire()
isRun = true isRun = true
upNotification() upNotification()
exoPlayer.addListener(this) exoPlayer.addListener(this)
@ -120,6 +127,7 @@ class AudioPlayService : BaseService(),
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
wakeLock.release()
isRun = false isRun = false
abandonFocus() abandonFocus()
exoPlayer.release() exoPlayer.release()

@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.AudioManager import android.media.AudioManager
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.systemservices.audioManager 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 { private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this) MediaHelp.buildAudioFocusRequestCompat(this)
} }
@ -77,8 +82,10 @@ abstract class BaseReadAloudService : BaseService(),
} }
} }
@SuppressLint("WakelockTimeout")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
wakeLock.acquire()
isRun = true isRun = true
pause = false pause = false
initMediaSession() initMediaSession()
@ -90,6 +97,7 @@ abstract class BaseReadAloudService : BaseService(),
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
wakeLock.release()
isRun = false isRun = false
pause = true pause = true
abandonFocus() abandonFocus()

@ -1,8 +1,10 @@
package io.legado.app.service package io.legado.app.service
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService 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.utils.*
import io.legado.app.web.HttpServer import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer import io.legado.app.web.WebSocketServer
import splitties.systemservices.powerManager
import java.io.IOException 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 httpServer: HttpServer? = null
private var webSocketServer: WebSocketServer? = null private var webSocketServer: WebSocketServer? = null
private var notificationContent = "" private var notificationContent = ""
@ -40,8 +46,10 @@ class WebService : BaseService() {
NetworkChangedListener(this) NetworkChangedListener(this)
} }
@SuppressLint("WakelockTimeout")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
wakeLock.acquire()
isRun = true isRun = true
notificationContent = getString(R.string.service_starting) notificationContent = getString(R.string.service_starting)
upNotification() upNotification()
@ -73,6 +81,7 @@ class WebService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
wakeLock.release()
networkChangedListener.unRegister() networkChangedListener.unRegister()
isRun = false isRun = false
if (httpServer?.isAlive == true) { if (httpServer?.isAlive == true) {

@ -2,16 +2,11 @@ package io.legado.app.ui.association
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern.bookFileRegex import io.legado.app.constant.AppPattern.bookFileRegex
import io.legado.app.exception.NoStackTraceException
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.inputStream import io.legado.app.utils.*
import io.legado.app.utils.isJson
import io.legado.app.utils.printOnDebug
import java.io.File
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) { class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
val importBookLiveData = MutableLiveData<Uri>() val importBookLiveData = MutableLiveData<Uri>()
@ -24,15 +19,9 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
execute { execute {
lateinit var fileName: String lateinit var fileName: String
//如果是普通的url,需要根据返回的内容判断是什么 //如果是普通的url,需要根据返回的内容判断是什么
if (uri.scheme == "file" || uri.scheme == "content") { if (uri.isContentScheme() || uri.isFileScheme()) {
fileName = if (uri.scheme == "file") { val fileDoc = FileDoc.fromUri(uri, false)
val file = File(uri.path.toString()) fileName = fileDoc.name
file.name
} else {
val file = DocumentFile.fromSingleUri(context, uri)
if (file?.exists() != true) throw NoStackTraceException("文件不存在")
file.name ?: ""
}
kotlin.runCatching { kotlin.runCatching {
if (uri.inputStream(context).isJson()) { if (uri.inputStream(context).isJson()) {
importJson(uri) importJson(uri)

@ -46,22 +46,19 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
private val selectFolder = registerForActivityResult(HandleFileContract()) { private val selectFolder = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri -> it.uri?.let { uri ->
if (uri.isContentScheme()) {
AppConfig.importBookPath = uri.toString() AppConfig.importBookPath = uri.toString()
initRootDoc(true) initRootDoc(true)
} else {
AppConfig.importBookPath = uri.path
initRootDoc(true)
}
} }
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setTitle(R.string.book_local) binding.titleBar.setTitle(R.string.book_local)
launch { launch {
setBookStorage()
initView() initView()
initEvent() initEvent()
if (setBookStorage() && AppConfig.importBookPath.isNullOrBlank()) {
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
}
initData() initData()
} }
} }
@ -150,15 +147,21 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
} }
private fun initRootDoc(changedFolder: Boolean = false) { private fun initRootDoc(changedFolder: Boolean = false) {
if (viewModel.rootDoc != null && !changedFolder) {
upPath()
} else {
val lastPath = AppConfig.importBookPath val lastPath = AppConfig.importBookPath
when { if (lastPath.isNullOrBlank()) {
viewModel.rootDoc != null && !changedFolder -> upPath()
lastPath.isNullOrEmpty() -> {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
selectFolder.launch() selectFolder.launch()
} else {
val rootUri = if (lastPath.isUri()) {
Uri.parse(lastPath)
} else {
Uri.fromFile(File(lastPath))
} }
lastPath.isContentScheme() -> { when {
val rootUri = Uri.parse(lastPath) rootUri.isContentScheme() -> {
kotlin.runCatching { kotlin.runCatching {
val doc = DocumentFile.fromTreeUri(this, rootUri) val doc = DocumentFile.fromTreeUri(this, rootUri)
if (doc == null || doc.name.isNullOrEmpty()) { if (doc == null || doc.name.isNullOrEmpty()) {
@ -178,7 +181,9 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
selectFolder.launch() selectFolder.launch()
} }
else -> initRootPath(lastPath) else -> initRootPath(rootUri.path!!)
}
}
} }
} }
@ -228,7 +233,7 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
binding.tvPath.text = path binding.tvPath.text = path
adapter.selectedUris.clear() adapter.selectedUris.clear()
adapter.clearItems() adapter.clearItems()
viewModel.loadDoc(lastDoc.uri) viewModel.loadDoc(lastDoc)
} }
/** /**

@ -102,16 +102,16 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun loadDoc(uri: Uri) { fun loadDoc(fileDoc: FileDoc) {
execute { execute {
val docList = DocumentUtils.listFiles(uri) { item -> val docList = fileDoc.list { item ->
when { when {
item.name.startsWith(".") -> false item.name.startsWith(".") -> false
item.isDir -> true item.isDir -> true
else -> item.name.matches(bookFileRegex) else -> item.name.matches(bookFileRegex)
} }
} }
dataCallback?.setItems(docList) dataCallback?.setItems(docList!!)
}.onError { }.onError {
context.toastOnUi("获取文件列表出错\n${it.localizedMessage}") context.toastOnUi("获取文件列表出错\n${it.localizedMessage}")
} }
@ -132,7 +132,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
} }
kotlin.runCatching { kotlin.runCatching {
val list = ArrayList<FileDoc>() val list = ArrayList<FileDoc>()
DocumentUtils.listFiles(fileDoc.uri).forEach { docItem -> fileDoc.list()!!.forEach { docItem ->
if (!scope.isActive) { if (!scope.isActive) {
finally?.invoke() finally?.invoke()
return return

@ -18,6 +18,7 @@ import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.getRemoteUrl import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isLocal 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.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException import io.legado.app.lib.webdav.ObjectNotFoundException
import io.legado.app.model.BookCover import io.legado.app.model.BookCover
@ -236,6 +237,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
bookSource = source bookSource = source
bookData.value?.changeTo(book, toc) bookData.value?.changeTo(book, toc)
if (inBookshelf) { if (inBookshelf) {
book.removeType(BookType.updateError)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray()) appDb.bookChapterDao.insert(*toc.toTypedArray())
} }

@ -184,6 +184,7 @@ class BookshelfManageActivity :
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup() AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup() AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(viewModel.groupId) else -> appDb.bookDao.flowByGroup(viewModel.groupId)
}.conflate().map { list -> }.conflate().map { list ->
val books = if (searchKey.isNullOrBlank()) { val books = if (searchKey.isNullOrBlank()) {

@ -3,10 +3,12 @@ package io.legado.app.ui.book.manage
import android.app.Application import android.app.Application
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.BookType
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.book.isLocal 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.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@ -58,6 +60,7 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
context.toastOnUi("获取目录出错\n${it.localizedMessage}") context.toastOnUi("获取目录出错\n${it.localizedMessage}")
}.getOrNull()?.let { toc -> }.getOrNull()?.let { toc ->
book.changeTo(newBook, toc) book.changeTo(newBook, toc)
book.removeType(BookType.updateError)
appDb.bookDao.insert(newBook) appDb.bookDao.insert(newBook)
appDb.bookChapterDao.insert(*toc.toTypedArray()) appDb.bookChapterDao.insert(*toc.toTypedArray())
} }

@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book 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.BookHelp
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal 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.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadAloud import io.legado.app.model.ReadAloud
@ -215,6 +217,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
changeSourceCoroutine = execute { changeSourceCoroutine = execute {
ReadBook.upMsg(context.getString(R.string.loading)) ReadBook.upMsg(context.getString(R.string.loading))
ReadBook.book?.changeTo(book, toc) ReadBook.book?.changeTo(book, toc)
book.removeType(BookType.updateError)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray()) appDb.bookChapterDao.insert(*toc.toTypedArray())
ReadBook.resetData(book) ReadBook.resetData(book)

@ -221,6 +221,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
private fun initData() { private fun initData() {
searchScopeAdapter.setItems(viewModel.searchScope.displayNames) searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
viewModel.searchScope.stateLiveData.observe(this) {
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
}
viewModel.isSearchLiveData.observe(this) { viewModel.isSearchLiveData.observe(this) {
if (it) { if (it) {
startSearch() startSearch()
@ -400,6 +403,14 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
} }
} }
override fun finish() {
if (searchView.hasFocus()) {
searchView.clearFocus()
return
}
super.finish()
}
companion object { companion object {
fun start(context: Context, key: String?) { fun start(context: Context, key: String?) {

@ -1,5 +1,6 @@
package io.legado.app.ui.book.search package io.legado.app.ui.book.search
import androidx.lifecycle.MutableLiveData
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
@ -13,12 +14,18 @@ data class SearchScope(private var scope: String) {
constructor(groups: List<String>) : this(groups.joinToString(",")) constructor(groups: List<String>) : this(groups.joinToString(","))
constructor(source: BookSource) : this("${source.bookSourceName}::${source.bookSourceUrl}") constructor(source: BookSource) : this(
"${
source.bookSourceName.replace("::", "")
}::${source.bookSourceUrl}"
)
override fun toString(): String { override fun toString(): String {
return scope return scope
} }
val stateLiveData = MutableLiveData("")
fun update(scope: String) { fun update(scope: String) {
this.scope = scope this.scope = scope
} }
@ -70,12 +77,20 @@ data class SearchScope(private var scope: String) {
} }
} }
} else { } else {
scope.splitNotBlank(",").forEach { val oldScope = scope.splitNotBlank(",")
list.addAll(appDb.bookSourceDao.getByGroup(it)) 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()) { if (list.isEmpty()) {
scope = "" scope = ""
stateLiveData.postValue("")
return appDb.bookSourceDao.allEnabled return appDb.bookSourceDao.allEnabled
} }
return list.sortedBy { it.customOrder } return list.sortedBy { it.customOrder }

@ -14,6 +14,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogSearchScopeBinding import io.legado.app.databinding.DialogSearchScopeBinding
import io.legado.app.databinding.ItemCheckBoxBinding import io.legado.app.databinding.ItemCheckBoxBinding
import io.legado.app.databinding.ItemRadioButtonBinding import io.legado.app.databinding.ItemRadioButtonBinding
import io.legado.app.help.source.contains
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.setLayout import io.legado.app.utils.setLayout
@ -72,7 +73,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
} }
private fun initOtherView() { 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 binding.toolBar.menu.findItem(R.id.menu_screen)?.isVisible = checkedId == R.id.rb_source
upData() upData()
} }
@ -101,7 +102,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
private fun initData() { private fun initData() {
launch { launch {
groups = withContext(IO) { groups = withContext(IO) {
appDb.bookSourceDao.allGroups appDb.bookSourceDao.allEnabledGroups
} }
sources = withContext(IO) { sources = withContext(IO) {
appDb.bookSourceDao.allEnabled appDb.bookSourceDao.allEnabled
@ -116,9 +117,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
withContext(IO) { withContext(IO) {
if (binding.rbSource.isChecked) { if (binding.rbSource.isChecked) {
sources.filter { source -> sources.filter { source ->
screenText?.let { screenText -> source.contains(screenText)
source.bookSourceName.contains(screenText)
} ?: true
}.let { }.let {
screenSources.clear() screenSources.clear()
screenSources.addAll(it) screenSources.addAll(it)

@ -193,7 +193,7 @@ class SearchContentActivity :
} }
} }
val isLocalBook: Boolean private val isLocalBook: Boolean
get() = viewModel.book?.isLocal == true get() = viewModel.book?.isLocal == true
override fun openSearchResult(searchResult: SearchResult, index: Int) { override fun openSearchResult(searchResult: SearchResult, index: Int) {

@ -24,6 +24,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
/**
* 字体选择对话框
*/
class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select), class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
Toolbar.OnMenuItemClickListener, Toolbar.OnMenuItemClickListener,
FontAdapter.CallBack { FontAdapter.CallBack {
@ -35,11 +38,11 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
} }
private val selectFontDir = registerForActivityResult(HandleFileContract()) { private val selectFontDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri -> it.uri?.let { uri ->
if (uri.toString().isContentScheme()) { if (uri.isContentScheme()) {
putPrefString(PreferKey.fontFolder, uri.toString()) putPrefString(PreferKey.fontFolder, uri.toString())
val doc = DocumentFile.fromTreeUri(requireContext(), uri) val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc != null) { if (doc != null) {
loadFontFiles(doc) loadFontFiles(FileDoc.fromDocumentFile(doc))
} else { } else {
RealPathUtil.getPath(requireContext(), uri)?.let { path -> RealPathUtil.getPath(requireContext(), uri)?.let { path ->
loadFontFilesByPermission(path) loadFontFilesByPermission(path)
@ -75,7 +78,7 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
if (fontPath.isContentScheme()) { if (fontPath.isContentScheme()) {
val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath)) val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath))
if (doc?.canRead() == true) { if (doc?.canRead() == true) {
loadFontFiles(doc) loadFontFiles(FileDoc.fromDocumentFile(doc))
} else { } else {
openFolder() openFolder()
} }
@ -117,40 +120,29 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
private fun getLocalFonts(): ArrayList<FileDoc> { private fun getLocalFonts(): ArrayList<FileDoc> {
val path = FileUtils.getPath(requireContext().externalFiles, "font") val path = FileUtils.getPath(requireContext().externalFiles, "font")
return DocumentUtils.listFiles(path) { return File(path).listFileDocs {
it.name.matches(fontRegex) 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) { private fun loadFontFilesByPermission(path: String) {
PermissionsCompat.Builder(this@FontSelectDialog) PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE) .addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage) .rationale(R.string.tip_perm_request_storage)
.onGranted { .onGranted {
loadFontFiles(path) loadFontFiles(
FileDoc.fromFile(File(path))
)
} }
.request() .request()
} }
private fun loadFontFiles(path: String) { private fun loadFontFiles(fileDoc: FileDoc) {
execute { execute {
val fontItems = DocumentUtils.listFiles(path) { val fontItems = fileDoc.list {
it.name.matches(fontRegex) it.name.matches(fontRegex)
} }
mergeFontItems(fontItems, getLocalFonts()) mergeFontItems(fontItems!!, getLocalFonts())
}.onSuccess { }.onSuccess {
adapter.setItems(it) adapter.setItems(it)
}.onError { }.onError {

@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book 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.AppWebDav
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.book.BookHelp 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.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CacheBook import io.legado.app.model.CacheBook
@ -126,6 +129,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
WebBook.getBookInfoAwait(source, book) WebBook.getBookInfoAwait(source, book)
} }
val toc = WebBook.getChapterListAwait(source, book).getOrThrow() val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
book.removeType(BookType.updateError)
if (book.bookUrl == bookUrl) { if (book.bookUrl == bookUrl) {
appDb.bookDao.update(book) appDb.bookDao.update(book)
} else { } else {
@ -137,6 +141,8 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*toc.toTypedArray()) appDb.bookChapterDao.insert(*toc.toTypedArray())
addDownload(source, book) addDownload(source, book)
}.onError(upTocPool) { }.onError(upTocPool) {
book.addType(BookType.updateError)
appDb.bookDao.update(book)
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it) AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
}.onCancel(upTocPool) { }.onCancel(upTocPool) {
upTocCancel(bookUrl) upTocCancel(bookUrl)

@ -118,6 +118,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup() AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup() AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId) else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list -> }.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) { when (getPrefInt(PreferKey.bookshelfSort)) {

@ -130,6 +130,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup() AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup() AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId) else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list -> }.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) { when (getPrefInt(PreferKey.bookshelfSort)) {

@ -15,6 +15,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.FragmentRssBinding import io.legado.app.databinding.FragmentRssBinding
import io.legado.app.databinding.ItemRssBinding 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.primaryColor
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.rss.article.RssSortActivity import io.legado.app.ui.rss.article.RssSortActivity
@ -179,8 +180,14 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
} }
override fun del(rssSource: RssSource) { override fun del(rssSource: RssSource) {
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName)
noButton()
yesButton {
viewModel.del(rssSource) viewModel.del(rssSource)
} }
}
}
override fun disable(rssSource: RssSource) { override fun disable(rssSource: RssSource) {
viewModel.disable(rssSource) viewModel.disable(rssSource)

@ -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<FileDoc> {
if (!uri.isContentScheme()) {
return listFiles(uri.path!!, filter)
}
val childrenUri = DocumentsContract
.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
val docList = arrayListOf<FileDoc>()
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<FileDoc> {
val docList = arrayListOf<FileDoc>()
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)
}

@ -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
}
}

@ -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<FileDoc>? {
if (isDir) {
if (uri.isContentScheme()) {
/**
* DocumentFile listFiles() 非常的慢,所以这里直接从数据库查询
*/
val childrenUri = DocumentsContract
.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
val docList = arrayListOf<FileDoc>()
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<FileDoc>? {
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}")
}

@ -1,5 +1,6 @@
package io.legado.app.utils package io.legado.app.utils
import android.net.Uri
import java.io.File import java.io.File
fun File.getFile(vararg subDirFiles: String): File { fun File.getFile(vararg subDirFiles: String): File {
@ -11,3 +12,23 @@ fun File.exists(vararg subDirFiles: String): Boolean {
return getFile(*subDirFiles).exists() return getFile(*subDirFiles).exists()
} }
@Throws(Exception::class)
fun File.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc> {
val docList = arrayListOf<FileDoc>()
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
}

@ -4,7 +4,6 @@ import android.os.Environment
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.IntDef import androidx.annotation.IntDef
import splitties.init.appCtx import splitties.init.appCtx
import java.io.* import java.io.*
import java.nio.charset.Charset import java.nio.charset.Charset
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -115,7 +114,7 @@ object FileUtils {
const val BY_EXTENSION_DESC = 7 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]) @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 annotation class SortType
/** /**

@ -1,35 +1,64 @@
package io.legado.app.utils package io.legado.app.utils
import android.util.Log
import androidx.core.os.postDelayed
import com.script.SimpleBindings import com.script.SimpleBindings
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.exception.RegexTimeoutException 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 { suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
val startTime = System.currentTimeMillis() val charSequence = this@replace
val charSequence = this
val isJs = replacement.startsWith("@js:") val isJs = replacement.startsWith("@js:")
val replacement1 = if (isJs) replacement.substring(4) else replacement val replacement1 = if (isJs) replacement.substring(4) else replacement
return suspendCancellableCoroutine { block ->
val coroutine = Coroutine.async {
try {
val pattern = regex.toPattern() val pattern = regex.toPattern()
val matcher = pattern.matcher(charSequence) val matcher = pattern.matcher(charSequence)
val stringBuffer = StringBuffer() val stringBuffer = StringBuffer()
while (matcher.find()) { while (matcher.find()) {
if (System.currentTimeMillis() - startTime > timeout) {
val timeoutMsg = "替换超时,将禁用替换规则"
throw RegexTimeoutException(timeoutMsg)
}
if (isJs) { if (isJs) {
val bindings = SimpleBindings() val bindings = SimpleBindings()
bindings["result"] = matcher.group() bindings["result"] = matcher.group()
val jsResult = AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString() val jsResult =
AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
matcher.appendReplacement(stringBuffer, jsResult) matcher.appendReplacement(stringBuffer, jsResult)
} else { } else {
matcher.appendReplacement(stringBuffer, replacement1) matcher.appendReplacement(stringBuffer, replacement1)
} }
} }
matcher.appendTail(stringBuffer) matcher.appendTail(stringBuffer)
return stringBuffer.toString() Log.e("regex", "end")
block.resume(stringBuffer.toString())
} catch (e: Exception) {
block.resumeWithException(e)
}
}
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()
}
}
}
}
}
} }

@ -19,9 +19,7 @@ fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri { fun String.parseToUri(): Uri {
return if (isContentScheme()) { return if (isUri()) Uri.parse(this) else {
Uri.parse(this)
} else {
Uri.fromFile(File(this)) Uri.fromFile(File(this))
} }
} }

@ -13,9 +13,12 @@ import io.legado.app.lib.permission.PermissionsCompat
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.charset.Charset
fun Uri.isContentScheme() = this.scheme == "content" fun Uri.isContentScheme() = this.scheme == "content"
fun Uri.isFileScheme() = this.scheme == "file"
/** /**
* 读取URI * 读取URI
*/ */
@ -100,7 +103,13 @@ fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputSt
@Throws(Exception::class) @Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray { fun Uri.readBytes(context: Context): ByteArray {
return if (this.isContentScheme()) { 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 { } else {
val path = RealPathUtil.getPath(context, this) val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) { if (path?.isNotEmpty() == true) {
@ -124,7 +133,12 @@ fun Uri.writeBytes(
byteArray: ByteArray byteArray: ByteArray
): Boolean { ): Boolean {
if (this.isContentScheme()) { if (this.isContentScheme()) {
return DocumentUtils.writeBytes(context, byteArray, this) context.contentResolver.openOutputStream(this)?.let {
it.write(byteArray)
it.close()
return true
}
return false
} else { } else {
val path = RealPathUtil.getPath(context, this) val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) { if (path?.isNotEmpty() == true) {
@ -136,8 +150,8 @@ fun Uri.writeBytes(
} }
@Throws(Exception::class) @Throws(Exception::class)
fun Uri.writeText(context: Context, text: String): Boolean { fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean {
return writeBytes(context, text.toByteArray()) return writeBytes(context, text.toByteArray(charset))
} }
fun Uri.writeBytes( fun Uri.writeBytes(

@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

Loading…
Cancel
Save