Refine UniFile and evict lookup cache on delete
Also add comments on functions that rely on implementation details.
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user