diff options
Diffstat (limited to 'libs/documents/src/main/java/org/the_jk')
6 files changed, 524 insertions, 0 deletions
diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/documents/DocumentTreeFactory.kt b/libs/documents/src/main/java/org/the_jk/cleversync/documents/DocumentTreeFactory.kt new file mode 100644 index 0000000..45064aa --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/documents/DocumentTreeFactory.kt @@ -0,0 +1,33 @@ +package org.the_jk.cleversync.documents + +import android.content.ContentResolver +import android.net.Uri +import android.provider.DocumentsContract +import org.the_jk.cleversync.io.ModifiableTree +import org.the_jk.cleversync.io.Tree +import org.the_jk.cleversync.io.documents.DocumentTree +import org.the_jk.cleversync.io.documents.getMetadata +import java.io.IOException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +object DocumentTreeFactory { + fun tree(contentResolver: ContentResolver, treeUri: Uri): Tree { + return modifiableTree(contentResolver, treeUri) + } + + fun modifiableTree(contentResolver: ContentResolver, treeUri: Uri): ModifiableTree { + return createModifiableTree(contentResolver, treeUri, liveUpdateInterval = 10.seconds) + } + + internal fun createModifiableTree( + contentResolver: ContentResolver, + treeUri: Uri, + liveUpdateInterval: Duration, + ): ModifiableTree { + val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)) + val metadata = contentResolver.getMetadata(documentUri) ?: throw IOException("Unable to open dir") + return DocumentTree(contentResolver, treeUri, metadata, liveUpdateInterval) + } +} diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DelayedCreationDocumentFile.kt b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DelayedCreationDocumentFile.kt new file mode 100644 index 0000000..9fdfb91 --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DelayedCreationDocumentFile.kt @@ -0,0 +1,36 @@ +package org.the_jk.cleversync.io.documents + +import org.the_jk.cleversync.io.ModifiableFile +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.time.Instant + +internal class DelayedCreationDocumentFile constructor( + private val metadata: DocumentMetadata, + private val create: () -> DocumentFile +) : ModifiableFile { + private var file: DocumentFile? = null + + override fun read(): InputStream { + return file?.read() ?: throw IOException("File does not exist") + } + + override fun write(): OutputStream { + if (file == null) { + file = create() + } + return file!!.write() + } + + override val name: String + get() = file?.name ?: metadata.displayName + override val size: ULong + get() = file?.size ?: metadata.size ?: 0UL + override val lastModified: Instant + get() = file?.lastModified ?: metadata.lastModified ?: Instant.EPOCH + + override fun equals(other: Any?) = file?.equals(other) ?: (this === other) + override fun hashCode(): Int = file?.hashCode() ?: super.hashCode() + override fun toString(): String = file?.toString() ?: metadata.displayName +} diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentDirectory.kt b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentDirectory.kt new file mode 100644 index 0000000..258d5b6 --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentDirectory.kt @@ -0,0 +1,302 @@ +package org.the_jk.cleversync.io.documents + +import android.content.ContentResolver +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.provider.DocumentsContract +import android.webkit.MimeTypeMap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import org.the_jk.cleversync.io.Directory +import org.the_jk.cleversync.io.File +import org.the_jk.cleversync.io.Link +import org.the_jk.cleversync.io.ModifiableDirectory +import org.the_jk.cleversync.io.ModifiableFile +import org.the_jk.cleversync.io.ModifiableLink +import java.io.IOException +import kotlin.time.Duration + +internal open class DocumentDirectory( + private val contentResolver: ContentResolver, + private val treeUri: Uri, + private val metadata: DocumentMetadata, + private val liveUpdateInterval: Duration, +) : ModifiableDirectory { + private val modifiableLiveContent = object : MutableLiveData<ModifiableDirectory.Content>() { + private val looper = Looper.myLooper() + private val handler = if (looper != null) Handler(looper) else null + private val updateCallback = Runnable { update() } + + override fun onActive() { + super.onActive() + + value = modifiableList() + handler?.postDelayed(updateCallback, liveUpdateInterval.inWholeMilliseconds) + } + + override fun onInactive() { + super.onInactive() + + handler?.removeCallbacks(updateCallback) + } + + private fun update() { + val newValue = modifiableList() + if (value != newValue) postValue(newValue) + handler?.postDelayed(updateCallback, liveUpdateInterval.inWholeMilliseconds) + } + } + + private val liveContent: LiveData<Directory.Content> by lazy { + modifiableLiveContent.map { + Directory.Content( + it.directories, + it.files, + it.links, + ) + } + } + + override fun modifiableOpenDir(name: String): ModifiableDirectory? { + val childMetadata = findChild(name) ?: return null + if (!childMetadata.isDir()) return null + val childTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, childMetadata.documentId) + return DocumentDirectory(contentResolver, childTreeUri, childMetadata, liveUpdateInterval) + } + + override fun modifiableOpenFile(name: String): ModifiableFile? { + val childMetadata = findChild(name) ?: return null + if (childMetadata.isDir()) return null + val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childMetadata.documentId) + return DocumentFile(contentResolver, documentUri, childMetadata) + } + + override fun modifiableOpenLink(name: String): ModifiableLink? { + return null + } + + override fun modifiableList(): ModifiableDirectory.Content { + val list = contentResolver.getAllMetadata( + DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, metadata.documentId), + ) ?: throw IOException("Unable to list dir") + val dirs = mutableListOf<ModifiableDirectory>() + val files = mutableListOf<ModifiableFile>() + val links = mutableListOf<ModifiableLink>() + list.forEach { childMetadata -> + if (childMetadata.isDir()) { + val childTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, childMetadata.documentId) + dirs.add(DocumentDirectory(contentResolver, childTreeUri, childMetadata, liveUpdateInterval)) + } else { + val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childMetadata.documentId) + files.add(DocumentFile(contentResolver, documentUri, childMetadata)) + } + } + return ModifiableDirectory.Content(dirs, files, links) + } + + override fun modifiableLiveList(): LiveData<ModifiableDirectory.Content> { + return modifiableLiveContent + } + + override fun createDirectory(name: String): ModifiableDirectory { + if (!metadata.supportsCreate()) throw IOException("Directory doesn't support creation") + val documentUri = DocumentsContract.createDocument( + contentResolver, + DocumentsContract.buildDocumentUriUsingTree(treeUri, metadata.documentId), + DocumentsContract.Document.MIME_TYPE_DIR, + name, + ) ?: throw IOException("Failed to create") + // TODO: Can we just create metadata to save time? We need documentId and flags tho. + val childMetadata = contentResolver.getMetadata(documentUri) + ?: throw IOException("Document was not actually created") + val childTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, childMetadata.documentId) + return DocumentDirectory(contentResolver, childTreeUri, childMetadata, liveUpdateInterval) + } + + override fun createFile(name: String): ModifiableFile { + if (!metadata.supportsCreate()) throw IOException("Directory doesn't support creation") + // TODO: Handle .tar.gz and such? Is it worth it (will android find them anyway) + val dotIndex = name.lastIndexOf('.') + val mimeType = if (dotIndex > 0) { + MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(name.substring(dotIndex + 1)) + } else { + null + } ?: "application/octet-stream" + return DelayedCreationDocumentFile( + DocumentMetadata( + displayName = name, + documentId = "invalid", + flags = 0, + lastModified = null, + mimeType = mimeType, + size = null, + ) + ) { + val documentUri = DocumentsContract.createDocument( + contentResolver, + DocumentsContract.buildDocumentUriUsingTree(treeUri, metadata.documentId), + mimeType, + name, + ) ?: throw IOException("Failed to create") + // TODO: Can we just create metadata to save time? We need documentId and flags tho. + val childMetadata = contentResolver.getMetadata(documentUri) + ?: throw IOException("Document was not actually created") + DocumentFile(contentResolver, documentUri, childMetadata) + } + } + + override fun createLink(name: String, target: Directory): ModifiableLink { + throw IOException("Symlinks are not supported") + } + + override fun createLink(name: String, target: File): ModifiableLink { + throw IOException("Symlinks are not supported") + } + + override fun createLink(name: String, target: String): ModifiableLink { + throw IOException("Symlinks are not supported") + } + + private fun removeDirectory( + documentUri: Uri, + documentTreeUri: Uri, + documentMetadata: DocumentMetadata, + parentDocumentUri: Uri, + ): Boolean { + contentResolver.getAllMetadata( + documentTreeUri, + ).forEach { childMetadata -> + val childUri = DocumentsContract.buildDocumentUriUsingTree(documentTreeUri, childMetadata.documentId) + if (childMetadata.isDir()) { + val childTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree( + documentTreeUri, + childMetadata.documentId, + ) + if (!removeDirectory(childUri, childTreeUri, childMetadata, documentUri)) return false + } else { + val ret = if (childMetadata.supportsRemove()) { + DocumentsContract.removeDocument( + contentResolver, + childUri, + documentUri + ) + } else if (childMetadata.supportsDelete()) { + DocumentsContract.deleteDocument( + contentResolver, + childUri + ) + } else false + if (!ret) return false + } + } + return if (documentMetadata.supportsRemove()) { + DocumentsContract.removeDocument( + contentResolver, + documentUri, + parentDocumentUri, + ) + } else if (documentMetadata.supportsDelete()) { + DocumentsContract.deleteDocument( + contentResolver, + documentUri + ) + } else false + } + + override fun removeDirectory(name: String): Boolean { + val childMetadata = findChild(name) ?: return false + if (!childMetadata.isDir()) return false + val childUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childMetadata.documentId) + val childTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, childMetadata.documentId) + val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, metadata.documentId) + return removeDirectory(childUri, childTreeUri, childMetadata, parentUri) + } + + override fun removeFile(name: String): Boolean { + val childMetadata = findChild(name) ?: return false + if (childMetadata.isDir()) return false + val documentUri = DocumentsContract.buildDocumentUriUsingTree( + treeUri, + childMetadata.documentId + ) + if (childMetadata.supportsRemove()) { + return DocumentsContract.removeDocument( + contentResolver, + documentUri, + DocumentsContract.buildDocumentUri( + treeUri.authority, + metadata.documentId + ), + ) + } else if (childMetadata.supportsDelete()) { + return DocumentsContract.deleteDocument( + contentResolver, + documentUri, + ) + } else { + return false + } + } + + override fun removeLink(name: String): Boolean { + return false + } + + override val name: String + get() = metadata.displayName + + override fun openDir(name: String): Directory? { + return modifiableOpenDir(name) + } + + override fun openFile(name: String): File? { + return modifiableOpenFile(name) + } + + override fun openLink(name: String): Link? { + return modifiableOpenLink(name) + } + + override fun list(): Directory.Content { + return with(modifiableList()) { + Directory.Content( + directories, + files, + links, + ) + } + } + + override fun liveList(): LiveData<Directory.Content> { + return liveContent + } + + override fun equals(other: Any?) = other is DocumentDirectory && + getRealTreeDocumentId(other.treeUri) == getRealTreeDocumentId(treeUri) + override fun hashCode() = getRealTreeDocumentId(treeUri).hashCode() + override fun toString() = getRealTreeDocumentId(treeUri) ?: "null" + + private fun findChild(displayName: String): DocumentMetadata? { + val queryArgs = Bundle() + queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, displayName) + return contentResolver.getAllMetadata( + DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, metadata.documentId), + queryArgs = queryArgs, + ).singleOrNull() + } + + private companion object { + fun getRealTreeDocumentId(treeUri: Uri): String { + // For some reason, getTreeDocumentId returns the root tree, not + // the tree for the children for ChildDocuments uris. + if (treeUri.lastPathSegment == "children") { + return DocumentsContract.getDocumentId(treeUri) + } + return DocumentsContract.getTreeDocumentId(treeUri) + } + } +} diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentFile.kt b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentFile.kt new file mode 100644 index 0000000..768b679 --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentFile.kt @@ -0,0 +1,47 @@ +package org.the_jk.cleversync.io.documents + +import android.content.ContentResolver +import android.net.Uri +import android.provider.DocumentsContract +import org.the_jk.cleversync.io.ModifiableFile +import java.io.FilterOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.time.Instant + +internal class DocumentFile( + private val contentResolver: ContentResolver, + private val documentUri: Uri, + private var metadata: DocumentMetadata, +) : ModifiableFile { + override val name: String + get() = metadata.displayName + override val size: ULong + get() = metadata.size ?: 0UL + override val lastModified: Instant + get() = metadata.lastModified ?: Instant.EPOCH + + override fun read(): InputStream { + return contentResolver.openInputStream(documentUri) ?: throw IOException("Provider crashed") + } + + override fun write(): OutputStream { + val ret = contentResolver.openOutputStream(documentUri, "wt") ?: throw IOException("Provider crashed") + return object : FilterOutputStream(ret) { + override fun close() { + super.close() + + // Update metadata + contentResolver.getMetadata(documentUri)?.let { + metadata = it + } + } + } + } + + override fun equals(other: Any?) = other is DocumentFile && + DocumentsContract.getDocumentId(other.documentUri) == DocumentsContract.getDocumentId(documentUri) + override fun hashCode() = DocumentsContract.getDocumentId(documentUri).hashCode() + override fun toString() = DocumentsContract.getDocumentId(documentUri) ?: "null" +} diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentMetadata.kt b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentMetadata.kt new file mode 100644 index 0000000..932de58 --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentMetadata.kt @@ -0,0 +1,83 @@ +package org.the_jk.cleversync.io.documents + +import android.content.ContentResolver +import android.net.Uri +import android.os.Bundle +import android.provider.DocumentsContract +import java.time.Instant + +internal data class DocumentMetadata( + val displayName: String, + val documentId: String, + val flags: Int, + val lastModified: Instant?, + val mimeType: String, + val size: ULong?, +) + +internal fun DocumentMetadata.isDir(): Boolean { + return mimeType == DocumentsContract.Document.MIME_TYPE_DIR +} + +internal fun DocumentMetadata.supportsCreate(): Boolean { + return (flags and DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0 +} + +internal fun DocumentMetadata.supportsDelete(): Boolean { + return (flags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0 +} + +internal fun DocumentMetadata.supportsRemove(): Boolean { + return (flags and DocumentsContract.Document.FLAG_SUPPORTS_REMOVE) != 0 +} + +internal fun ContentResolver.getAllMetadata( + uri: Uri, + queryArgs: Bundle? = null, +): List<DocumentMetadata> { + query( + uri, + arrayOf( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_SIZE, + ), + queryArgs, + null, + )?.use { cursor -> + val displayNameIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val documentIdIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS) + val lastModifiedIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + val mimeTypeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) + val sizeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) + return buildList { + while (cursor.moveToNext()) { + val lastModified = if (cursor.isNull(lastModifiedIndex)) { + null + } else { + Instant.ofEpochMilli(cursor.getLong(lastModifiedIndex)) + } + val size = if (cursor.isNull(sizeIndex)) null else cursor.getLong(sizeIndex) + add( + DocumentMetadata( + cursor.getString(displayNameIndex), + cursor.getString(documentIdIndex), + cursor.getInt(flagsIndex), + lastModified, + cursor.getString(mimeTypeIndex), + size?.toULong(), + ), + ) + } + } + } + return listOf() +} + +internal fun ContentResolver.getMetadata(uri: Uri): DocumentMetadata? { + return getAllMetadata(uri).singleOrNull() +} diff --git a/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentTree.kt b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentTree.kt new file mode 100644 index 0000000..ef1d04a --- /dev/null +++ b/libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentTree.kt @@ -0,0 +1,23 @@ +package org.the_jk.cleversync.io.documents + +import android.content.ContentResolver +import android.content.res.Resources +import android.net.Uri +import android.provider.DocumentsContract +import org.the_jk.cleversync.io.ModifiableTree +import kotlin.time.Duration + +internal class DocumentTree( + private val contentResolver: ContentResolver, + private val treeUri: Uri, + metadata: DocumentMetadata, + liveUpdateInterval: Duration, +) : DocumentDirectory(contentResolver, treeUri, metadata, liveUpdateInterval), ModifiableTree { + override fun description(resources: Resources): CharSequence { + val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)) + return DocumentsContract.findDocumentPath(contentResolver, documentUri).toString() + } + + override fun close() = Unit +} |
