Refine UniFile and evict lookup cache on delete

Also add comments on functions that rely on implementation details.
This commit is contained in:
FooIbar committed 2024-04-26 15:46:47 +08:00
1 parent 8d64a6f1b2
commit a04206f600
6 files changed
+87 -48

No files matched your search

@@ -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,
@@ -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)
@@ -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<RawFile>().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()
@@ -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
@@ -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('/')
}
@@ -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<UniFile>
fun listFiles(): List<UniFile>
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)