Skip to content

Commit

Permalink
Optimized DocumentFileCompat.mkdirs() and DocumentFile.child() perfor…
Browse files Browse the repository at this point in the history
…mance
  • Loading branch information
anggrayudi committed Jul 4, 2021
1 parent bcd9b82 commit 15f5d07
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,13 @@ object DocumentFileCompat {
if (storageId == DATA) {
return DocumentFile.fromFile(context.dataDirectory)
}
return if (considerRawFile) {
val file = if (considerRawFile) {
getRootRawFile(context, storageId, requiresWriteAccess)?.let { DocumentFile.fromFile(it) }
?: context.fromTreeUri(createDocumentUri(storageId))
} else {
context.fromTreeUri(createDocumentUri(storageId))
}
return file?.takeIf { it.canRead() && (requiresWriteAccess && it.isWritable(context) || !requiresWriteAccess) }
}

/**
Expand Down Expand Up @@ -456,17 +457,24 @@ object DocumentFileCompat {
requiresWriteAccess: Boolean = true,
considerRawFile: Boolean = true
): DocumentFile? {
if (considerRawFile && fullPath.startsWith('/') || fullPath.startsWith(context.dataDirectory.path)) {
val tryCreateWithRawFile: () -> DocumentFile? = {
val folder = File(fullPath.removeForbiddenCharsFromFilename()).apply { mkdirs() }
if (folder.isDirectory && folder.canRead() && (requiresWriteAccess && folder.isWritable(context) || !requiresWriteAccess)) {
// Consider java.io.File for faster performance
return DocumentFile.fromFile(folder)
}
DocumentFile.fromFile(folder)
} else null
}
if (considerRawFile && fullPath.startsWith('/') || fullPath.startsWith(context.dataDirectory.path)) {
tryCreateWithRawFile()?.let { return it }
}
var currentDirectory = getAccessibleRootDocumentFile(context, fullPath, requiresWriteAccess, considerRawFile) ?: return null
if (currentDirectory.isRawFile) {
return tryCreateWithRawFile()
}
val resolver = context.contentResolver
getDirectorySequence(getBasePath(context, fullPath)).forEach {
try {
val directory = currentDirectory.findFile(it)
val directory = currentDirectory.quickFindTreeFile(context, resolver, it)
currentDirectory = when {
directory == null -> currentDirectory.createDirectory(it) ?: return null
directory.isDirectory && directory.canRead() -> directory
Expand Down Expand Up @@ -508,9 +516,11 @@ object DocumentFileCompat {
}
} else {
var currentDirectory = getAccessibleRootDocumentFile(context, path, requiresWriteAccess, considerRawFile) ?: continue
val isRawFile = currentDirectory.isRawFile
val resolver = context.contentResolver
getDirectorySequence(getBasePath(context, path)).forEach {
try {
val directory = currentDirectory.findFile(it)
val directory = if (isRawFile) currentDirectory.quickFindRawFile(it) else currentDirectory.quickFindTreeFile(context, resolver, it)
if (directory == null) {
currentDirectory = currentDirectory.createDirectory(it) ?: return@forEach
val fullPath = currentDirectory.getAbsolutePath(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
package com.anggrayudi.storage.file

import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.content.FileProvider
Expand Down Expand Up @@ -274,11 +276,12 @@ fun DocumentFile.child(context: Context, path: String, requiresWriteAccess: Bool
path.isEmpty() -> this
isDirectory -> {
val file = if (isRawFile) {
DocumentFile.fromFile(File(uri.path!!, path))
quickFindRawFile(path)
} else {
var currentDirectory = this
val resolver = context.contentResolver
DocumentFileCompat.getDirectorySequence(path).forEach {
val directory = currentDirectory.findFile(it) ?: return null
val directory = currentDirectory.quickFindTreeFile(context, resolver, it) ?: return null
if (directory.canRead()) {
currentDirectory = directory
} else {
Expand All @@ -287,12 +290,48 @@ fun DocumentFile.child(context: Context, path: String, requiresWriteAccess: Bool
}
currentDirectory
}
file.takeIf { it.canRead() && (requiresWriteAccess && it.isWritable(context) || !requiresWriteAccess) }
file?.takeIf { requiresWriteAccess && it.isWritable(context) || !requiresWriteAccess }
}
else -> null
}
}

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun DocumentFile.quickFindRawFile(name: String): DocumentFile? {
return DocumentFile.fromFile(File(uri.path!!, name)).takeIf { it.canRead() }
}

/**
* It's faster 140% than [DocumentFile.findFile].
*
* Must set [ContentResolver] as additional parameter to improve performance.
*/
@SuppressLint("NewApi")
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun DocumentFile.quickFindTreeFile(context: Context, resolver: ContentResolver, name: String): DocumentFile? {
try {
// Optimized algorithm. Do not change unless you really know algorithm complexity.
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)
resolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null)?.use {
val columnName = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
while (it.moveToNext()) {
try {
val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, it.getString(0))
resolver.query(documentUri, columnName, null, null, null)?.use { childCursor ->
if (childCursor.moveToFirst() && name == childCursor.getString(0))
return context.fromTreeUri(documentUri)
}
} catch (e: Exception) {
// ignore
}
}
}
} catch (e: Exception) {
// ignore
}
return null
}

/**
* @return File path without storage ID. Returns empty `String` if:
* * It is the root path
Expand Down Expand Up @@ -629,9 +668,10 @@ fun DocumentFile.makeFolder(context: Context, name: String, mode: CreateMode = C
return null
}

val resolver = context.contentResolver
directorySequence.forEach { folder ->
try {
val directory = currentDirectory.findFile(folder)
val directory = currentDirectory.quickFindTreeFile(context, resolver, folder)
currentDirectory = if (directory == null) {
currentDirectory.createDirectory(folder) ?: return null
} else if (directory.isDirectory && directory.canRead()) {
Expand Down

1 comment on commit 15f5d07

@anggrayudi
Copy link
Owner Author

@anggrayudi anggrayudi commented on 15f5d07 Jul 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to my benchmark, DocumentFile.child() is 36% faster than DocumentFile.findFile():

thread {
    val context = applicationContext
    val root = DocumentFileCompat.fromSimplePath(context) ?: return@thread
    val startFindFile = System.currentTimeMillis()
    repeat(50) {
        root.findFile("DCIM")?.findFile("Camera")?.findFile("Test")
    }
    val findFile = System.currentTimeMillis() - startFindFile
    val startQuickFind = System.currentTimeMillis()
    repeat(50) {
        root.child(context, "DCIM/Camera/Test")
    }
    val quickFind = System.currentTimeMillis() - startQuickFind
    val delta = abs(findFile - quickFind)
    val expected = quickFind < findFile
    val improvedPerformance = if (expected) findFile.toFloat() / quickFind * 100 - 100 else 0
    Timber.d("onCreate: Improved performance in percent: $improvedPerformance%")
}

Screen Shot 2021-07-04 at 19 50 18

Please sign in to comment.