|
|
@@ -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}")
|
|
|
+}
|