From de492a65181f535d1a3e3dce742fe04d4f3376af Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 8 Feb 2026 14:02:58 +0530 Subject: [PATCH 1/9] feat: custom projections support. --- .../dfc/controller/DocumentController.kt | 20 +++--- .../dfc/file/DocumentFileCompat.kt | 22 +++--- .../file/internals/RawDocumentFileCompat.kt | 12 +++- .../internals/SingleDocumentFileCompat.kt | 4 +- .../file/internals/TreeDocumentFileCompat.kt | 8 +-- .../dfc/resolver/ResolverCompat.kt | 67 +++++++++++++------ 6 files changed, 87 insertions(+), 46 deletions(-) diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/controller/DocumentController.kt b/dfc/src/main/java/com/lazygeniouz/dfc/controller/DocumentController.kt index c713cf8..e98107e 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/controller/DocumentController.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/controller/DocumentController.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.provider.DocumentsContract -import android.provider.DocumentsContract.Document.MIME_TYPE_DIR +import android.provider.DocumentsContract.Document import com.lazygeniouz.dfc.file.DocumentFileCompat import com.lazygeniouz.dfc.resolver.ResolverCompat @@ -31,10 +31,12 @@ internal class DocumentController( /** * This will return a list of [DocumentFileCompat] with all the defined fields. */ - internal fun listFiles(): List { + internal fun listFiles( + projection: Array = ResolverCompat.fullProjection, + ): List { return if (!isDirectory()) throw UnsupportedOperationException("Selected document is not a Directory.") - else ResolverCompat.listFiles(context, fileCompat) + else ResolverCompat.listFiles(context, fileCompat, projection) } /** @@ -70,14 +72,14 @@ internal class DocumentController( * Returns True if the Document is a File. */ internal fun isFile(): Boolean { - return !(MIME_TYPE_DIR == fileCompat.documentMimeType || fileCompat.documentMimeType.isEmpty()) + return !(Document.MIME_TYPE_DIR == fileCompat.documentMimeType || fileCompat.documentMimeType.isEmpty()) } /** * Returns True if the Document is a Directory */ internal fun isDirectory(): Boolean { - return MIME_TYPE_DIR == fileCompat.documentMimeType + return Document.MIME_TYPE_DIR == fileCompat.documentMimeType } /** @@ -95,7 +97,7 @@ internal class DocumentController( } /** - * Returns True if the Document Folder / File is Writable. + * Returns `true` if the Document Folder / File is writable. */ internal fun canWrite(): Boolean { if (context.checkCallingOrSelfUriPermission( @@ -105,15 +107,15 @@ internal class DocumentController( if (fileCompat.documentMimeType.isEmpty()) return false - if (fileCompat.documentFlags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE != 0) + if (fileCompat.documentFlags and Document.FLAG_SUPPORTS_DELETE != 0) return true - if (MIME_TYPE_DIR == fileCompat.documentMimeType && + if (Document.MIME_TYPE_DIR == fileCompat.documentMimeType && fileCompat.documentFlags and DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE != 0 ) return true else if (fileCompat.documentMimeType.isNotEmpty() && - fileCompat.documentFlags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE != 0 + fileCompat.documentFlags and Document.FLAG_SUPPORTS_WRITE != 0 ) return true return false diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/file/DocumentFileCompat.kt b/dfc/src/main/java/com/lazygeniouz/dfc/file/DocumentFileCompat.kt index 0aa9c14..97482a9 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/file/DocumentFileCompat.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/file/DocumentFileCompat.kt @@ -3,13 +3,13 @@ package com.lazygeniouz.dfc.file import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE -import android.provider.DocumentsContract.Document.MIME_TYPE_DIR -import android.provider.DocumentsContract.isDocumentUri +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document import com.lazygeniouz.dfc.controller.DocumentController import com.lazygeniouz.dfc.file.internals.RawDocumentFileCompat import com.lazygeniouz.dfc.file.internals.SingleDocumentFileCompat import com.lazygeniouz.dfc.file.internals.TreeDocumentFileCompat +import com.lazygeniouz.dfc.resolver.ResolverCompat import java.io.File /** @@ -18,7 +18,7 @@ import java.io.File * * Use [DocumentFileCompat.serialize] to get a [Serializable] object. */ -abstract class DocumentFileCompat constructor( +abstract class DocumentFileCompat( internal val context: Context, uri: Uri, val name: String = "", open val length: Long = 0, @@ -63,9 +63,13 @@ abstract class DocumentFileCompat constructor( * This will return a list of [DocumentFileCompat] with all the defined fields * only when the current document is a **Directory**. * + * @param projection Columns to query. Use custom projection to improve performance by fetching only needed data. + * * A [UnsupportedOperationException] is thrown if the uri is not a directory. */ - abstract fun listFiles(): List + abstract fun listFiles( + projection: Array = ResolverCompat.fullProjection, + ): List /** * This will return the children count inside a **Directory** without creating [DocumentFileCompat] objects. @@ -128,11 +132,11 @@ abstract class DocumentFileCompat constructor( /** * Return the MIME type of this document. * - * @return A concrete mime type from [COLUMN_MIME_TYPE] column. + * @return A concrete mime type from [Document.COLUMN_MIME_TYPE] column. */ @Suppress("unused") fun getType(): String? { - return if (documentMimeType == MIME_TYPE_DIR) null else documentMimeType + return if (documentMimeType == Document.MIME_TYPE_DIR) null else documentMimeType } /** @@ -237,8 +241,8 @@ abstract class DocumentFileCompat constructor( */ @JvmStatic @Suppress("unused") - fun isDocument(context: Context, uri: Uri): Boolean { - return isDocumentUri(context, uri) + fun isDocumentUri(context: Context, uri: Uri): Boolean { + return DocumentsContract.isDocumentUri(context, uri) } /** diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/RawDocumentFileCompat.kt b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/RawDocumentFileCompat.kt index 9c312e3..13194c9 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/RawDocumentFileCompat.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/RawDocumentFileCompat.kt @@ -11,7 +11,7 @@ import java.io.File * RawDocumentFileCompat serves as an alternative to the **RawDocumentFile** * which handles Documents using the native [File] api. */ -internal class RawDocumentFileCompat constructor(context: Context, var file: File) : +internal class RawDocumentFileCompat(context: Context, var file: File) : DocumentFileCompat(context, file) { // Get file extension. @@ -96,8 +96,14 @@ internal class RawDocumentFileCompat constructor(context: Context, var file: Fil else null } - // Performance of File api is pretty great as compared to others. - override fun listFiles(): List { + /** + * Returns list of files using File API. + * + * The performance of File api is pretty great as compared to others. + * + * Note: [projection] parameter is ignored as File API doesn't support custom projections. + */ + override fun listFiles(projection: Array): List { return file.listFiles()?.map { child -> fromFile(context, child) } ?: emptyList() } diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/SingleDocumentFileCompat.kt b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/SingleDocumentFileCompat.kt index 918091a..b90dc67 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/SingleDocumentFileCompat.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/SingleDocumentFileCompat.kt @@ -46,7 +46,7 @@ internal class SingleDocumentFileCompat( * * @throws UnsupportedOperationException */ - override fun listFiles(): List { + override fun listFiles(projection: Array): List { throw UnsupportedOperationException() } @@ -64,7 +64,7 @@ internal class SingleDocumentFileCompat( * * @throws UnsupportedOperationException */ - override fun findFile(name: String, ignoreCase: Boolean): DocumentFileCompat? { + override fun findFile(name: String, ignoreCase: Boolean): DocumentFileCompat { throw UnsupportedOperationException() } diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/TreeDocumentFileCompat.kt b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/TreeDocumentFileCompat.kt index 0958c4e..3bf49fe 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/TreeDocumentFileCompat.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/file/internals/TreeDocumentFileCompat.kt @@ -17,7 +17,7 @@ import com.lazygeniouz.dfc.resolver.ResolverCompat * * Other params same as [DocumentFileCompat]. */ -internal class TreeDocumentFileCompat constructor( +internal class TreeDocumentFileCompat( context: Context, documentUri: Uri, documentName: String = "", documentSize: Long = 0, lastModifiedTime: Long = -1L, documentMimeType: String = "", documentFlags: Int = -1, ) : DocumentFileCompat( @@ -51,10 +51,10 @@ internal class TreeDocumentFileCompat constructor( } /** - * This will return a list of [DocumentFileCompat] with all the defined fields. + * This will return a list of [DocumentFileCompat] with all or the required defined fields as per passed [projection]. */ - override fun listFiles(): List { - return fileController.listFiles() + override fun listFiles(projection: Array): List { + return fileController.listFiles(projection) } /** diff --git a/dfc/src/main/java/com/lazygeniouz/dfc/resolver/ResolverCompat.kt b/dfc/src/main/java/com/lazygeniouz/dfc/resolver/ResolverCompat.kt index b5ff4dc..7ee8dce 100644 --- a/dfc/src/main/java/com/lazygeniouz/dfc/resolver/ResolverCompat.kt +++ b/dfc/src/main/java/com/lazygeniouz/dfc/resolver/ResolverCompat.kt @@ -5,6 +5,7 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document import com.lazygeniouz.dfc.file.DocumentFileCompat import com.lazygeniouz.dfc.file.internals.TreeDocumentFileCompat import com.lazygeniouz.dfc.logger.ErrorLogger @@ -14,17 +15,25 @@ import com.lazygeniouz.dfc.logger.ErrorLogger */ internal object ResolverCompat { - private val iconProjection = arrayOf(DocumentsContract.Document.COLUMN_ICON) - private val idProjection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + private val iconProjection = arrayOf(Document.COLUMN_ICON) + private val idProjection = arrayOf(Document.COLUMN_DOCUMENT_ID) val fullProjection = arrayOf( - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_FLAGS + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_SIZE, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS ) + private fun getStringOrDefault(cursor: Cursor, index: Int, default: String = ""): String { + return if (index != -1) cursor.getString(index) else default + } + + private fun getLongOrDefault(cursor: Cursor, index: Int, default: Long = 0L): Long { + return if (index != -1) cursor.getLong(index) else default + } + /** * Delete the file. * @@ -46,7 +55,7 @@ internal object ResolverCompat { */ internal fun renameTo(context: Context, uri: Uri, name: String): Uri? { return try { - return DocumentsContract.renameDocument(context.contentResolver, uri, name) + DocumentsContract.renameDocument(context.contentResolver, uri, name) } catch (exception: Exception) { ErrorLogger.logError("Exception while renaming document", exception) null @@ -105,12 +114,23 @@ internal object ResolverCompat { /** * Queries the ContentResolver & builds a list of [DocumentFileCompat] with all the required fields. */ - internal fun listFiles(context: Context, file: DocumentFileCompat): List { + internal fun listFiles( + context: Context, + file: DocumentFileCompat, + projection: Array = fullProjection, + ): List { val uri = file.uri val childrenUri = createChildrenUri(uri) val listOfDocuments = arrayListOf() - getCursor(context, childrenUri, fullProjection)?.use { cursor -> + // ensure `Document.COLUMN_DOCUMENT_ID` is always included + val finalProjection = if (Document.COLUMN_DOCUMENT_ID !in projection) { + arrayOf(Document.COLUMN_DOCUMENT_ID, *projection) + } else projection + + val cursor = getCursor(context, childrenUri, finalProjection) ?: return emptyList() + + cursor.use { val itemCount = cursor.count /** * Pre-sizing the list to avoid resizing overhead. @@ -122,19 +142,28 @@ internal object ResolverCompat { */ if (itemCount > 10) listOfDocuments.ensureCapacity(itemCount) + // Resolve column indices dynamically + val idIndex = cursor.getColumnIndex(Document.COLUMN_DOCUMENT_ID) + + val nameIndex = cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME) + val sizeIndex = cursor.getColumnIndex(Document.COLUMN_SIZE) + val modifiedIndex = cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED) + val mimeIndex = cursor.getColumnIndex(Document.COLUMN_MIME_TYPE) + val flagsIndex = cursor.getColumnIndex(Document.COLUMN_FLAGS) + while (cursor.moveToNext()) { - val documentId: String = cursor.getString(0) + val documentId = cursor.getString(idIndex) val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId) - val documentName: String = cursor.getString(1) - val documentSize: Long = cursor.getLong(2) - val documentLastModified: Long = cursor.getLong(3) - val documentMimeType: String = cursor.getString(4) - val documentFlags: Int = cursor.getLong(5).toInt() + val documentName = getStringOrDefault(cursor, nameIndex) + val documentSize = getLongOrDefault(cursor, sizeIndex) + val lastModifiedTime = getLongOrDefault(cursor, modifiedIndex, -1L) + val documentMimeType = getStringOrDefault(cursor, mimeIndex) + val documentFlags = getLongOrDefault(cursor, flagsIndex, -1L).toInt() TreeDocumentFileCompat( context, documentUri, documentName, - documentSize, documentLastModified, + documentSize, lastModifiedTime, documentMimeType, documentFlags ).also { childFile -> childFile.parentFile = file @@ -158,7 +187,7 @@ internal object ResolverCompat { /** * This exception can occur in scenarios such as - * - * - The Uri became invalid due to external changes (e.g., permissions revoked, storage unmounted, etc). + * - The Uri became invalid due to external changes (e.g., permissions revoked, storage unmounted, etc.). * - The file or directory represented by this Uri was probably deleted or became `inaccessible` after the Uri was obtained but before this operation was performed. */ ErrorLogger.logError("Exception while building the Cursor", exception) From 409ffd956a2d77df393d555dd70971fe6e589472 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 8 Feb 2026 14:25:32 +0530 Subject: [PATCH 2/9] update: sample app. --- app/build.gradle | 2 +- .../filecompat/example/MainActivity.kt | 98 +++++++++++++++++++ .../filecompat/example/TestFileGenerator.kt | 96 ++++++++++++++++++ .../example/performance/Performance.kt | 4 + .../performance/ProjectionPerformance.kt | 94 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 8 ++ app/src/main/res/menu/main_menu.xml | 6 ++ app/src/main/res/values/strings.xml | 4 + 8 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lazygeniouz/filecompat/example/TestFileGenerator.kt create mode 100644 app/src/main/java/com/lazygeniouz/filecompat/example/performance/ProjectionPerformance.kt create mode 100644 app/src/main/res/menu/main_menu.xml diff --git a/app/build.gradle b/app/build.gradle index 71433b0..4409bfc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,7 @@ android { dependencies { implementation project(":dfc") - //implementation "com.lazygeniouz:dfc:1.1" + // implementation "com.lazygeniouz:dfc:1.3" implementation "androidx.appcompat:appcompat:1.7.1" implementation "androidx.activity:activity-ktx:1.12.3" diff --git a/app/src/main/java/com/lazygeniouz/filecompat/example/MainActivity.kt b/app/src/main/java/com/lazygeniouz/filecompat/example/MainActivity.kt index dd54152..e7a9eb5 100644 --- a/app/src/main/java/com/lazygeniouz/filecompat/example/MainActivity.kt +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/MainActivity.kt @@ -1,10 +1,13 @@ package com.lazygeniouz.filecompat.example import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.Intent import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.os.storage.StorageManager +import android.view.Menu +import android.view.MenuItem import android.widget.Button import android.widget.ProgressBar import android.widget.TextView @@ -22,10 +25,40 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { private lateinit var buttonDir: Button private lateinit var buttonFile: Button + private lateinit var buttonProjections: Button private lateinit var textView: TextView private lateinit var progress: ProgressBar + private var selectedFileCount = 250 + + private val testFileGenerationLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val documentUri = result.data?.data + if (documentUri != null) { + textView.text = getString(R.string.creating_files) + + lifecycleScope.launch { + progress.isVisible = true + buttonDir.isVisible = false + buttonFile.isVisible = false + buttonProjections.isVisible = false + + val generationResult = TestFileGenerator.generateTestFiles( + this@MainActivity, documentUri, selectedFileCount + ) + + progress.isVisible = false + buttonDir.isVisible = true + buttonFile.isVisible = true + buttonProjections.isVisible = true + textView.text = generationResult + } + } + } + } + private val folderResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { @@ -37,6 +70,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { progress.isVisible = true buttonDir.isVisible = false buttonFile.isVisible = false + buttonProjections.isVisible = false val performanceResult = withContext(Dispatchers.IO) { Performance.calculateDirectoryPerformance( this@MainActivity, documentUri @@ -46,6 +80,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { progress.isVisible = false buttonDir.isVisible = true buttonFile.isVisible = true + buttonProjections.isVisible = true textView.text = performanceResult } } @@ -73,11 +108,41 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { } } + private val projectionResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val documentUri = result.data?.data + if (documentUri != null) { + textView.text = "" + + lifecycleScope.launch { + progress.isVisible = true + buttonDir.isVisible = false + buttonFile.isVisible = false + buttonProjections.isVisible = false + + val performanceResult = withContext(Dispatchers.IO) { + Performance.calculateProjectionPerformance( + this@MainActivity, documentUri + ) + } + + progress.isVisible = false + buttonDir.isVisible = true + buttonFile.isVisible = true + buttonProjections.isVisible = true + textView.text = performanceResult + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) buttonDir = findViewById(R.id.buttonDir) buttonFile = findViewById(R.id.buttonFile) + buttonProjections = findViewById(R.id.buttonProjections) textView = findViewById(R.id.fileNames) progress = findViewById(R.id.progress) @@ -90,6 +155,39 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { buttonFile.setOnClickListener { fileResultLauncher.launch(getStorageIntent(true)) } + + buttonProjections.setOnClickListener { + projectionResultLauncher.launch(getStorageIntent()) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.main_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_add_test_files -> { + showFileCountDialog() + true + } + + else -> super.onOptionsItemSelected(item) + } + } + + private fun showFileCountDialog() { + val options = arrayOf("250 files", "500 files", "1000 files") + val counts = arrayOf(250, 500, 1000) + + AlertDialog.Builder(this) + .setTitle(R.string.select_file_count) + .setItems(options) { _, which -> + selectedFileCount = counts[which] + testFileGenerationLauncher.launch(getStorageIntent()) + } + .show() } private fun getStorageIntent(single: Boolean = false): Intent { diff --git a/app/src/main/java/com/lazygeniouz/filecompat/example/TestFileGenerator.kt b/app/src/main/java/com/lazygeniouz/filecompat/example/TestFileGenerator.kt new file mode 100644 index 0000000..7b2b0fc --- /dev/null +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/TestFileGenerator.kt @@ -0,0 +1,96 @@ +package com.lazygeniouz.filecompat.example + +import android.content.Context +import android.net.Uri +import com.lazygeniouz.dfc.file.DocumentFileCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import java.util.Random +import java.util.concurrent.atomic.AtomicInteger + +object TestFileGenerator { + + private val extensions = listOf("txt", "pdf", "jpg", "png", "mp4", "mp3", "doc", "zip", "apk") + private val random = Random() + + suspend fun generateTestFiles(context: Context, directoryUri: Uri, fileCount: Int): String { + return try { + val directory = DocumentFileCompat.fromTreeUri(context, directoryUri) + ?: return "Failed to access directory" + + val startTime = System.currentTimeMillis() + val successCount = AtomicInteger(0) + val failCount = AtomicInteger(0) + + val semaphore = Semaphore(10) + + coroutineScope { + val jobs = (1..fileCount).map { index -> + async(Dispatchers.IO) { + semaphore.withPermit { + val fileName = "test_file_$index" + val extension = extensions.random() + val sizeInKb = random.nextInt(100) + 1 // 1KB to 100KB + + val file = directory.createFile( + "application/octet-stream", + "$fileName.$extension" + ) + if (file != null) { + val success = writeRandomData(context, file.uri, sizeInKb) + if (success) successCount.incrementAndGet() else failCount.incrementAndGet() + } else { + failCount.incrementAndGet() + } + } + } + } + + jobs.awaitAll() + } + + val elapsedTime = (System.currentTimeMillis() - startTime) / 1000.0 + + buildString { + append("Test Files Generation Complete!\n\n") + append("Total: $fileCount files\n") + append("Success: ${successCount.get()}\n") + append("Failed: ${failCount.get()}\n") + append("Time: ${elapsedTime}s\n\n") + append("Files have random sizes between 1KB-100KB\n") + append("Extensions: ${extensions.joinToString(", ")}") + } + } catch (e: Exception) { + "Error generating files: ${e.message}" + } + } + + /** + * Write random data to a file to achieve desired size. + */ + private fun writeRandomData(context: Context, fileUri: Uri, sizeInKb: Int): Boolean { + return try { + context.contentResolver.openOutputStream(fileUri)?.use { output -> + val bufferSize = 8192 // 8KB buffer + val buffer = ByteArray(bufferSize) + val totalBytes = sizeInKb * 1024 + var written = 0 + + while (written < totalBytes) { + random.nextBytes(buffer) + val toWrite = minOf(bufferSize, totalBytes - written) + output.write(buffer, 0, toWrite) + written += toWrite + } + output.flush() + } + true + } catch (e: Exception) { + false + } + } +} diff --git a/app/src/main/java/com/lazygeniouz/filecompat/example/performance/Performance.kt b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/Performance.kt index e6eaaf0..bcd76f7 100644 --- a/app/src/main/java/com/lazygeniouz/filecompat/example/performance/Performance.kt +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/Performance.kt @@ -16,6 +16,10 @@ object Performance { return FilesPerformance.calculateFileSidePerformance(context, uri) } + fun calculateProjectionPerformance(context: Context, uri: Uri): String { + return ProjectionPerformance.calculateProjectionPerformance(context, uri) + } + fun getUsablePath(uri: Uri): String { val path = uri.path!! return when { diff --git a/app/src/main/java/com/lazygeniouz/filecompat/example/performance/ProjectionPerformance.kt b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/ProjectionPerformance.kt new file mode 100644 index 0000000..43926bd --- /dev/null +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/ProjectionPerformance.kt @@ -0,0 +1,94 @@ +package com.lazygeniouz.filecompat.example.performance + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract.Document +import com.lazygeniouz.dfc.file.DocumentFileCompat +import com.lazygeniouz.filecompat.example.performance.Performance.measureTimeSeconds + +object ProjectionPerformance { + + fun calculateProjectionPerformance(context: Context, uri: Uri): String { + var results = "" + + results += "Testing Custom Projections Performance\n\n" + results += "=".repeat(48).plus("\n\n") + + // Test 1: Full projection (default) + results += testFullProjection(context, uri) + "\n\n" + + // Test 2: Minimal projection (ID + Name only) + results += testMinimalProjection(context, uri) + "\n\n" + + // Test 3: ID + Name + Size + results += testPartialProjection(context, uri) + "\n\n" + + results += "=".repeat(48).plus("\n\n") + + return results + } + + private fun testFullProjection(context: Context, uri: Uri): String { + var fileCount = 0 + measureTimeSeconds { + val documentFile = DocumentFileCompat.fromTreeUri(context, uri) + // Uses default fullProjection + val files = documentFile?.listFiles() + fileCount = files?.size ?: 0 + }.also { time -> + return "Full Projection:\n" + + "Files: $fileCount\n" + + "Time: ${time}s" + } + } + + private fun testMinimalProjection(context: Context, uri: Uri): String { + var fileCount = 0 + measureTimeSeconds { + val documentFile = DocumentFileCompat.fromTreeUri(context, uri) + // Only fetch ID and Name + val minimalProjection = arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME + ) + val files = documentFile?.listFiles(minimalProjection) + fileCount = files?.size ?: 0 + + // Verify we can access the names + files?.forEach { file -> + val name = file.name // Should work + } + }.also { time -> + return "Minimal Projection (ID + Name):\n" + + "Files: $fileCount\n" + + "Time: ${time}s" + } + } + + private fun testPartialProjection(context: Context, uri: Uri): String { + var fileCount = 0 + var totalSize = 0L + measureTimeSeconds { + val documentFile = DocumentFileCompat.fromTreeUri(context, uri) + // Fetch ID, Name, and Size + val partialProjection = arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_SIZE + ) + val files = documentFile?.listFiles(partialProjection) + fileCount = files?.size ?: 0 + + // Calculate total size + files?.forEach { file -> + totalSize += file.length + } + }.also { time -> + val sizeMb = Performance.getSizeInMb(totalSize) + return "Partial Projection (ID + Name + Size):\n" + + "Files: $fileCount\n" + + "Total Size: $sizeMb\n" + + "Time: ${time}s" + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8cf838c..7a537b1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -35,4 +35,12 @@ android:paddingVertical="12dp" android:text="@string/select_a_file" /> +