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 c00ca0ecb..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 @@ -233,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/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt index 73b1de7e1..7bf5c364c 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 @@ -117,16 +117,16 @@ 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) { + val fontItems = doc.listFileDocs { it.name.matches(fontRegex) - } + }!! mergeFontItems(fontItems, getLocalFonts()) }.onSuccess { adapter.setItems(it) @@ -147,7 +147,7 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select), private fun loadFontFiles(path: String) { execute { - val fontItems = DocumentUtils.listFiles(path) { + val fontItems = File(path).listFileDocs { it.name.matches(fontRegex) } mergeFontItems(fontItems, getLocalFonts()) 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 7585d4b82..000000000 --- a/app/src/main/java/io/legado/app/utils/DocumentExtensions.kt +++ /dev/null @@ -1,238 +0,0 @@ -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 - - -@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 - } - - 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 - ) - } - - /** - * DocumentFile 的 listFiles() 非常的慢,所以这里直接从数据库查询 - */ - @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, 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 - } - - @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 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 ?: "", true, 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) - ) - } - - } -} - -fun FileDoc.list(filter: ((file: FileDoc) -> Boolean)? = null): ArrayList? { - if (isDir) { - return DocumentUtils.listFiles(uri, filter) - } - return null -} - -fun FileDoc.find(name: String): FileDoc? { - return list { - it.name == name - }?.firstOrNull() -} - -@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/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt new file mode 100644 index 000000000..e7e31d4cb --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt @@ -0,0 +1,58 @@ +package io.legado.app.utils + +import android.provider.DocumentsContract +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 + } + + 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 + ) + } + +} \ 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..26225ec32 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt @@ -0,0 +1,182 @@ +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 ?: "", true, 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 + +/** + * 返回子文件列表,如果不是文件夹则返回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, + DocumentUtils.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 +} + +fun FileDoc.find(name: String): FileDoc? { + return list { + it.name == name + }?.firstOrNull() +} + +/** + * 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 /**