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/DirectoryPerformance.kt b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/DirectoryPerformance.kt index d792bce..efe18d9 100644 --- a/app/src/main/java/com/lazygeniouz/filecompat/example/performance/DirectoryPerformance.kt +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/DirectoryPerformance.kt @@ -24,7 +24,10 @@ object DirectoryPerformance { results += "Fetching only Uris will always be faster (after File)" + "\nBut try fetching the Documents' Names.\n\n" results += calculateDocumentFileCompatPerformanceWithName(context, uri) + "\n" results += calculateDocumentFilePerformanceOnlyUri(context, uri) + "\n" - results += calculateDocumentFilePerformanceWithName(context, uri) + results += calculateDocumentFilePerformanceWithName(context, uri) + "\n\n" + + results += "=".repeat(48).plus("\n\n") + results += calculateCountVsListSize(context, uri) return results } @@ -141,4 +144,25 @@ object DirectoryPerformance { return ("DFC Performance (With Names) = ${time}s") } } + + private fun calculateCountVsListSize(context: Context, uri: Uri): String { + val documentFile = DocumentFileCompat.fromTreeUri(context, uri) + ?: return "Failed to access directory" + + val dfListSizeTime = measureTimeSeconds { + DocumentFile.fromTreeUri(context, uri)?.listFiles()?.size + } + + val dfcCountTime = measureTimeSeconds { + documentFile.count() + } + + val dfcListSizeTime = measureTimeSeconds { + documentFile.listFiles().size + } + + return "DocumentFile.listFiles().size = ${dfListSizeTime}s\n" + + "DFC count() Performance = ${dfcCountTime}s\n" + + "DFC listFiles().size Performance = ${dfcListSizeTime}s" + } } \ No newline at end of file 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..d13613b --- /dev/null +++ b/app/src/main/java/com/lazygeniouz/filecompat/example/performance/ProjectionPerformance.kt @@ -0,0 +1,122 @@ +package com.lazygeniouz.filecompat.example.performance + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract.Document +import androidx.documentfile.provider.DocumentFile +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") + + // Test 4: count() vs listFiles().size + results += testCountVsListSize(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" + } + } + + private fun testCountVsListSize(context: Context, uri: Uri): String { + val documentFile = DocumentFileCompat.fromTreeUri(context, uri) + ?: return "Failed to access directory" + + val dfListSizeTime = measureTimeSeconds { + DocumentFile.fromTreeUri(context, uri)?.listFiles()?.size + } + + val dfcCountTime = measureTimeSeconds { + documentFile.count() + } + + val dfcListSizeTime = measureTimeSeconds { + documentFile.listFiles().size + } + + return "count() vs listFiles().size:\n\n" + + "DocumentFile.listFiles().size = ${dfListSizeTime}s\n" + + "DFC.count() = ${dfcCountTime}s\n" + + "DFC.listFiles().size = ${dfcListSizeTime}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" /> + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..87713dd --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,6 @@ + +
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f374ee..613a2ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,8 @@