summaryrefslogtreecommitdiff
path: root/libs/documents/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'libs/documents/src/main/java')
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/documents/DocumentTreeFactory.kt33
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DelayedCreationDocumentFile.kt36
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentDirectory.kt302
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentFile.kt47
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentMetadata.kt83
-rw-r--r--libs/documents/src/main/java/org/the_jk/cleversync/io/documents/DocumentTree.kt23
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
+}