From a04206f600af4aa0ac1e6ef7d18a0266543c83cf Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:25:09 +0800 Subject: [PATCH] Refine UniFile and evict lookup cache on delete Also add comments on functions that rely on implementation details. --- .../hippo/unifile/DocumentsContractApi21.kt | 2 + .../kotlin/com/hippo/unifile/MediaFile.kt | 3 +- .../main/kotlin/com/hippo/unifile/RawFile.kt | 22 +++++--- .../com/hippo/unifile/SingleDocumentFile.kt | 3 +- .../com/hippo/unifile/TreeDocumentFile.kt | 49 ++++++++++------ .../main/kotlin/com/hippo/unifile/UniFile.kt | 56 ++++++++++++------- 6 files changed, 87 insertions(+), 48 deletions(-) diff --git a/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt b/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt index aefc41e88..e466ab022 100644 --- a/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt +++ b/app/src/main/kotlin/com/hippo/unifile/DocumentsContractApi21.kt @@ -40,6 +40,8 @@ object DocumentsContractApi21 { ) } + // This is not a good approach as the document identifier is an opaque implementation detail of + // the provider, but works for ExternalStorageProvider fun buildChildUri(uri: Uri, displayName: String): Uri { return DocumentsContract.buildDocumentUriUsingTree( uri, diff --git a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt index c06ec3df3..74befa1df 100644 --- a/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/MediaFile.kt @@ -18,7 +18,7 @@ package com.hippo.unifile import android.net.Uri import java.io.IOException -class MediaFile(override val uri: Uri) : UniFile() { +class MediaFile(override val uri: Uri) : UniFile { override fun createFile(displayName: String) = null override fun createDirectory(displayName: String) = null @@ -27,6 +27,7 @@ class MediaFile(override val uri: Uri) : UniFile() { get() = MediaContract.getName(uri) override val type: String? get() = MediaContract.getType(uri) + override val parent = null override val isDirectory = false override val isFile: Boolean get() = DocumentsContractApi19.isFile(uri) diff --git a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt index 558cbf6ff..5cb9d9a9c 100644 --- a/app/src/main/kotlin/com/hippo/unifile/RawFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/RawFile.kt @@ -23,19 +23,15 @@ import android.webkit.MimeTypeMap import java.io.File import java.util.Locale -class RawFile(private val parent: RawFile?, private var file: File) : UniFile() { +class RawFile(override val parent: RawFile?, private var file: File) : UniFile { private var cachePresent = false private val allChildren by lazy { - val current = this cachePresent = true - mutableListOf().apply { - val fs = file.listFiles()?.map { RawFile(current, it) } - if (fs != null) addAll(fs) - } + file.listFiles().orEmpty().mapTo(mutableListOf()) { RawFile(this, it) } } - private fun popCacheIfPresent(file: RawFile) = apply { + private fun popCacheIfPresent(file: RawFile) { if (cachePresent) { synchronized(allChildren) { allChildren.add(file) @@ -43,6 +39,14 @@ class RawFile(private val parent: RawFile?, private var file: File) : UniFile() } } + private fun evictCacheIfPresent(file: RawFile) { + if (cachePresent) { + synchronized(allChildren) { + allChildren.remove(file) + } + } + } + override fun createFile(displayName: String): UniFile? { val target = RawFile(this, File(file, displayName)) if (target.exists()) { @@ -101,7 +105,9 @@ class RawFile(private val parent: RawFile?, private var file: File) : UniFile() override fun resolve(displayName: String) = RawFile(this, File(file, displayName)) - override fun delete() = file.deleteRecursively() + override fun delete() = file.deleteRecursively().also { + if (it) parent?.evictCacheIfPresent(this) + } override fun exists() = file.exists() diff --git a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt index 2b0e4d2b1..961febe25 100644 --- a/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/SingleDocumentFile.kt @@ -17,7 +17,7 @@ package com.hippo.unifile import android.net.Uri -class SingleDocumentFile(override val uri: Uri) : UniFile() { +class SingleDocumentFile(override val uri: Uri) : UniFile { override fun createFile(displayName: String) = null @@ -27,6 +27,7 @@ class SingleDocumentFile(override val uri: Uri) : UniFile() { get() = DocumentsContractApi19.getName(uri) override val type: String? get() = DocumentsContractApi19.getType(uri) + override val parent = null override val isDirectory: Boolean get() = DocumentsContractApi19.isDirectory(uri) override val isFile: Boolean diff --git a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt index fc9ea4411..5210e737f 100644 --- a/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/TreeDocumentFile.kt @@ -20,11 +20,11 @@ import android.provider.DocumentsContract.Document.MIME_TYPE_DIR import android.webkit.MimeTypeMap class TreeDocumentFile( - private val parent: UniFile?, - override var uri: Uri, - override var name: String = getFilenameForUri(uri), + override val parent: TreeDocumentFile?, + uri: Uri, + name: String = getFilenameForUri(uri), mimeType: String? = null, -) : UniFile() { +) : UniFile { private var cachePresent = false private val allChildren by lazy { cachePresent = true @@ -33,7 +33,7 @@ class TreeDocumentFile( } } - private fun popCacheIfPresent(file: TreeDocumentFile) = apply { + private fun popCacheIfPresent(file: TreeDocumentFile) { if (cachePresent) { synchronized(allChildren) { allChildren.add(file) @@ -41,6 +41,14 @@ class TreeDocumentFile( } } + private fun evictCacheIfPresent(file: TreeDocumentFile) { + if (cachePresent) { + synchronized(allChildren) { + allChildren.remove(file) + } + } + } + override fun createFile(displayName: String): UniFile? { val child = findFile(displayName) if (child != null) { @@ -73,14 +81,22 @@ class TreeDocumentFile( return null } - private val rawType by lazy { - mimeType ?: DocumentsContractApi19.getRawType(uri) - } + private var mimeType = mimeType + get() { + if (field == null) { + field = DocumentsContractApi19.getRawType(uri) + } + return field + } + override var uri = uri + private set + override var name = name + private set override val type: String? - get() = rawType.takeUnless { isDirectory } + get() = mimeType.takeUnless { isDirectory } override val isDirectory: Boolean - get() = rawType == MIME_TYPE_DIR + get() = mimeType == MIME_TYPE_DIR override val isFile: Boolean get() = !type.isNullOrEmpty() @@ -117,7 +133,9 @@ class TreeDocumentFile( return TreeDocumentFile(this, childUri, displayName) } - override fun delete() = DocumentsContractApi19.delete(uri) + override fun delete() = DocumentsContractApi19.delete(uri).also { + if (it) parent?.evictCacheIfPresent(this) + } override fun exists() = DocumentsContractApi19.exists(uri) @@ -134,18 +152,15 @@ class TreeDocumentFile( if (result != null) { uri = result name = displayName + mimeType = null return true } return false } } +// Technically the Uris should be treated as opaque, but it works for ExternalStorageProvider private fun getFilenameForUri(uri: Uri): String { val path = requireNotNull(uri.path) - val index = path.lastIndexOf('/') - return if (index >= 0) { - path.substring(index + 1) - } else { - return path - } + return path.substringAfterLast('/') } diff --git a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt index 2f3e671a6..0aa341b08 100644 --- a/app/src/main/kotlin/com/hippo/unifile/UniFile.kt +++ b/app/src/main/kotlin/com/hippo/unifile/UniFile.kt @@ -28,7 +28,7 @@ import splitties.init.appCtx * In Android files can be accessed via [java.io.File] and [android.net.Uri]. * The UniFile is designed to emulate File interface for both File and Uri. */ -sealed class UniFile { +sealed interface UniFile { /** * Create a new file as a direct child of this directory. * @@ -36,7 +36,7 @@ sealed class UniFile { * @return file representing newly created document, or null if failed * @see android.provider.DocumentsContract.createDocument */ - abstract fun createFile(displayName: String): UniFile? + fun createFile(displayName: String): UniFile? /** * Create a new directory as a direct child of this directory. @@ -45,7 +45,7 @@ sealed class UniFile { * @return file representing newly created directory, or null if failed * @see android.provider.DocumentsContract.createDocument */ - abstract fun createDirectory(displayName: String): UniFile? + fun createDirectory(displayName: String): UniFile? /** * Return a Uri for the underlying document represented by this file. This @@ -61,7 +61,7 @@ sealed class UniFile { * @see ContentResolver.openOutputStream * @see ContentResolver.openFileDescriptor */ - abstract val uri: Uri + val uri: Uri /** * Return the display name of this file. @@ -69,7 +69,7 @@ sealed class UniFile { * @return name of the file, or null if failed * @see android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME */ - abstract val name: String? + val name: String? /** * Return the MIME type of this file. @@ -77,7 +77,21 @@ sealed class UniFile { * @return MIME type of the file, or null if failed * @see android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE */ - abstract val type: String? + val type: String? + + /** + * Return the parent file of this file. Only defined inside of the + * user-selected tree; you can never escape above the top of the tree. + * + * + * The underlying [android.provider.DocumentsProvider] only defines a + * forward mapping from parent to child, so the reverse mapping of child to + * parent offered here is purely a convenience method, and it may be + * incorrect if the underlying tree structure changes. + * + * @return parent of the file, or null if it is the top of the file tree + */ + val parent: UniFile? /** * Indicates if this file represents a *directory*. @@ -86,7 +100,7 @@ sealed class UniFile { * otherwise. * @see android.provider.DocumentsContract.Document.MIME_TYPE_DIR */ - abstract val isDirectory: Boolean + val isDirectory: Boolean /** * Indicates if this file represents a *file*. @@ -94,7 +108,7 @@ sealed class UniFile { * @return `true` if this file is a file, `false` otherwise. * @see android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE */ - abstract val isFile: Boolean + val isFile: Boolean /** * Returns the time when this file was last modified, measured in @@ -104,7 +118,7 @@ sealed class UniFile { * @return the time when this file was last modified, `-1L` if can't get it * @see android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED */ - abstract fun lastModified(): Long + fun lastModified(): Long /** * Returns the length of this file in bytes. Returns -1 if the file does not @@ -114,14 +128,14 @@ sealed class UniFile { * @return the number of bytes in this file, `-1L` if can't get it * @see android.provider.DocumentsContract.Document.COLUMN_SIZE */ - abstract fun length(): Long + fun length(): Long /** * Indicates whether the current context is allowed to read from this file. * * @return `true` if this file can be read, `false` otherwise. */ - abstract fun canRead(): Boolean + fun canRead(): Boolean /** * Indicates whether the current context is allowed to write to this file. @@ -136,7 +150,7 @@ sealed class UniFile { * * @see android.provider.DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE */ - abstract fun canWrite(): Boolean + fun canWrite(): Boolean /** * It works like mkdirs, but it will return true if the UniFile is directory @@ -144,7 +158,7 @@ sealed class UniFile { * @return `true` if the directory was created * or if the directory already existed. */ - abstract fun ensureDir(): Boolean + fun ensureDir(): Boolean /** * Make sure the UniFile is file @@ -152,14 +166,14 @@ sealed class UniFile { * @return `true` if the file can be created * or if the file already existed. */ - abstract fun ensureFile(): Boolean + fun ensureFile(): Boolean /** * Get child file of this directory, the child might not exist. * * @return the child file */ - abstract fun resolve(displayName: String): UniFile + fun resolve(displayName: String): UniFile operator fun div(name: String) = resolve(name) @@ -173,20 +187,20 @@ sealed class UniFile { * @return `true` if this file was deleted, `false` otherwise. * @see android.provider.DocumentsContract.deleteDocument */ - abstract fun delete(): Boolean + fun delete(): Boolean /** * Returns a boolean indicating whether this file can be found. * * @return `true` if this file exists, `false` otherwise. */ - abstract fun exists(): Boolean + fun exists(): Boolean - abstract fun listFiles(): List + fun listFiles(): List - abstract fun findFirst(filter: (String) -> Boolean): UniFile? + fun findFirst(filter: (String) -> Boolean): UniFile? - open fun openFileDescriptor(mode: String) = appCtx.contentResolver.openFileDescriptor(uri, mode) ?: error("Can't open ParcelFileDescriptor") + fun openFileDescriptor(mode: String) = appCtx.contentResolver.openFileDescriptor(uri, mode) ?: error("Can't open ParcelFileDescriptor") /** * Test there is a file with the display name in the directory. @@ -215,7 +229,7 @@ sealed class UniFile { * @return true on success. * @see android.provider.DocumentsContract.renameDocument */ - abstract fun renameTo(displayName: String): Boolean + fun renameTo(displayName: String): Boolean companion object { fun fromFile(file: File) = RawFile(null, file)