package com.hippo.files import android.content.Context import android.net.Uri import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import android.provider.DocumentsContract.Document import android.provider.MediaStore import android.webkit.MimeTypeMap import androidx.core.database.getLongOrNull import okio.FileHandle import okio.FileMetadata import okio.FileSystem import okio.IOException import okio.Path import okio.Sink import okio.Source import splitties.init.appCtx class AndroidFileSystem(context: Context) : FileSystem() { private val contentResolver = context.contentResolver private val physicalFileSystem = SYSTEM override fun appendingSink(file: Path, mustExist: Boolean): Sink { TODO("Not yet implemented") } override fun atomicMove(source: Path, target: Path) { TODO("Not yet implemented") } override fun canonicalize(path: Path): Path { TODO("Not yet implemented") } override fun createDirectory(dir: Path, mustCreate: Boolean) { if (dir.isPhysicalFile()) { return physicalFileSystem.createDirectory(dir, mustCreate) } val alreadyExist = metadataOrNull(dir)?.isDirectory == true if (alreadyExist) { if (mustCreate) { throw IOException("$dir already exist") } else { return } } dir.parent?.let { DocumentsContract.createDocument(contentResolver, it.toUri(), Document.MIME_TYPE_DIR, dir.name) } ?: throw IOException("Failed to create directory: $dir") } override fun createSymlink(source: Path, target: Path) { TODO("Not yet implemented") } override fun delete(path: Path, mustExist: Boolean) { if (path.isPhysicalFile()) { return physicalFileSystem.delete(path, mustExist) } val metadata = metadataOrNull(path) if (metadata != null) { var uri = path.toUri() if (uri.isCifsDocument() && metadata.isDirectory) { uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getDocumentId(uri) + '/') } val deleted = DocumentsContract.deleteDocument(contentResolver, uri) if (!deleted) { throw IOException("Failed to delete $path") } } else if (mustExist) { throw IOException("$path does not exist") } } override fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean) { if (fileOrDirectory.isPhysicalFile()) { if (metadataOrNull(fileOrDirectory)?.isDirectory == true) { physicalFileSystem.deleteRecursively(fileOrDirectory, mustExist) } else { physicalFileSystem.delete(fileOrDirectory, mustExist) } } else { delete(fileOrDirectory, mustExist) } } override fun list(dir: Path): List = list(dir, throwOnFailure = true)!! override fun listOrNull(dir: Path): List? = list(dir, throwOnFailure = false) private fun list(dir: Path, throwOnFailure: Boolean): List? { if (dir.isPhysicalFile()) { return if (throwOnFailure) { physicalFileSystem.list(dir) } else { physicalFileSystem.listOrNull(dir) } } val uri = dir.toUri() var documentId = DocumentsContract.getDocumentId(uri) if (uri.isCifsDocument()) { documentId += '/' } val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId) return runCatching { contentResolver.query(childrenUri, arrayOf(Document.COLUMN_DISPLAY_NAME), null, null, null)?.use { c -> List(c.count) { c.moveToNext() val displayName = c.getString(0) dir / displayName } } }.getOrElse { if (throwOnFailure) throw it else null } } override fun metadataOrNull(path: Path): FileMetadata? { if (path.isPhysicalFile()) { return physicalFileSystem.metadataOrNull(path) } val uri = path.toUri() val isMediaUri = uri.authority == MediaStore.AUTHORITY val projection = if (isMediaUri) { arrayOf(MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.DATE_MODIFIED) } else { arrayOf(Document.COLUMN_MIME_TYPE, Document.COLUMN_LAST_MODIFIED) } return runCatching { contentResolver.query(uri, projection, null, null, null)?.use { c -> if (!c.moveToNext()) return null val mimeType = c.getString(0) val lastModified = c.getLongOrNull(1)?.let { if (isMediaUri) it * 1000 else it } val isDirectory = mimeType == Document.MIME_TYPE_DIR FileMetadata( isRegularFile = !isDirectory, isDirectory = isDirectory, lastModifiedAtMillis = lastModified, ) } }.getOrNull() } override fun openReadOnly(file: Path): FileHandle { TODO("Not yet implemented") } override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { TODO("Not yet implemented") } override fun sink(file: Path, mustCreate: Boolean): Sink { TODO("Not yet implemented") } override fun source(file: Path): Source { TODO("Not yet implemented") } fun openFileDescriptor(path: Path, mode: String): ParcelFileDescriptor { if ('w' in mode && !exists(path)) { val parent = path.parent ?: throw IOException("Failed to open file: $path") createDirectories(parent) if (path.isPhysicalFile()) { physicalFileSystem.openReadWrite(path).close() } else { val displayName = path.name val extension = displayName.substringAfterLast('.', "").ifEmpty { null }?.lowercase() val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream" DocumentsContract.createDocument(contentResolver, parent.toUri(), mimeType, displayName) ?: throw IOException("Failed to open file: $path") } } return if (path.isPhysicalFile()) { ParcelFileDescriptor.open(path.toFile(), ParcelFileDescriptor.parseMode(mode)) } else { contentResolver.openFileDescriptor(path.toUri(), mode) } ?: throw IOException("Failed to open file: $path") } } private fun Path.isPhysicalFile() = toString().first() == '/' private fun Uri.isCifsDocument() = authority == "com.wa2c.android.cifsdocumentsprovider.documents" val SystemFileSystem = AndroidFileSystem(appCtx)