Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/utils/FileDocExtensions.kt
T
2025-06-04 13:33:51 +08:00

423 lines
12 KiB
Kotlin

@file:Suppress("unused")
package io.legato.kazusa.utils
import android.app.DownloadManager
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import io.legato.kazusa.exception.NoStackTraceException
import splitties.init.appCtx
import splitties.systemservices.downloadManager
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset
import java.util.concurrent.atomic.AtomicInteger
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)
}
fun readText(): String {
return uri.readText(appCtx)
}
fun asDocumentFile(): DocumentFile? {
if (isContentScheme) {
return if (isDir) {
treeDocumentFileConstructor.newInstance(null, appCtx, uri) as DocumentFile
} else {
DocumentFile.fromSingleUri(appCtx, uri)
}
}
return null
}
fun asFile(): File? {
if (isContentScheme) {
return null
}
return File(uri.path!!)
}
companion object {
private val treeDocumentFileConstructor by lazy {
Class.forName("androidx.documentfile.provider.TreeDocumentFile")
.getDeclaredConstructor(
DocumentFile::class.java,
Context::class.java,
Uri::class.java
).apply {
isAccessible = true
}
}
fun fromDir(path: String): FileDoc {
return fromUri(path.toUri(), true)
}
fun fromFile(path: String): FileDoc {
return fromUri(path.toUri(), false)
}
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
}
/**
* 查找文档, 如果存在则返回文档,如果不存在返回空
* @param name 文件名
* @param depth 查找文件夹深度
* @param maxFinds 最大查找文件夹数量
*/
fun FileDoc.find(name: String, depth: Int = 0, maxFinds: Int = Int.MAX_VALUE): FileDoc? {
return find(name, depth, AtomicInteger(maxFinds))
}
private fun FileDoc.find(name: String, depth: Int, maxFinds: AtomicInteger): FileDoc? {
if (maxFinds.getAndDecrement() <= 0) {
return null
}
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, maxFinds)
if (fileDoc != null) {
return fileDoc
}
}
}
}
return null
}
fun FileDoc.createFileIfNotExist(
fileName: String,
vararg subDirs: String
): FileDoc {
return if (uri.isContentScheme()) {
val documentFile = asDocumentFile()!!
val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *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 = asDocumentFile()!!
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.openInputStream(): Result<InputStream> {
return uri.inputStream(appCtx)
}
fun FileDoc.openOutputStream(): Result<OutputStream> {
return uri.outputStream(appCtx)
}
fun FileDoc.openReadPfd(): Result<ParcelFileDescriptor> {
return uri.toReadPfd(appCtx)
}
fun FileDoc.openWritePfd(): Result<ParcelFileDescriptor> {
return uri.toWritePfd(appCtx)
}
fun FileDoc.exists(
fileName: String,
vararg subDirs: String
): Boolean {
return if (uri.isContentScheme()) {
DocumentUtils.exists(asDocumentFile()!!, fileName, *subDirs)
} else {
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
FileUtils.exist(path)
}
}
fun FileDoc.exists(): Boolean {
return if (uri.isContentScheme()) {
asDocumentFile()!!.exists()
} else {
FileUtils.exist(uri.path!!)
}
}
fun FileDoc.writeText(text: String) {
if (uri.isContentScheme()) {
uri.writeText(appCtx, text)
} else {
File(uri.path!!).writeText(text)
}
}
fun FileDoc.writeFile(file: File) {
openOutputStream().getOrThrow().use { out ->
file.inputStream().use {
it.copyTo(out)
}
}
}
fun FileDoc.delete() {
asFile()?.let {
FileUtils.delete(it, true)
}
asDocumentFile()?.delete()
}
fun FileDoc.checkWrite(): Boolean {
if (!isDir) {
throw NoStackTraceException("只能检查目录")
}
asFile()?.let {
return it.checkWrite()
}
return asDocumentFile()!!.checkWrite()
}
/**
* DocumentFile 的 listFiles() 非常的慢,尽量不要使用
*/
fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc>? {
return FileDoc.fromDocumentFile(this).list(filter)
}
@Throws(Exception::class)
fun DocumentFile.openInputStream(): InputStream? {
return appCtx.contentResolver.openInputStream(uri)
}
@Throws(Exception::class)
fun DocumentFile.openOutputStream(): OutputStream? {
return appCtx.contentResolver.openOutputStream(uri)
}
@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}")
}
fun DocumentFile.checkWrite(): Boolean {
var file: DocumentFile? = null
return try {
val filename = System.currentTimeMillis().toString()
file = createFile(FileUtils.getMimeType(filename), filename)
file?.openOutputStream()?.let { out ->
out.bufferedWriter().use { it.write(filename) }
file.openInputStream()?.let { input ->
input.bufferedReader().use {
return it.readText() == filename
}
}
}
false
} catch (e: Exception) {
false
} finally {
file?.delete()
}
}