perf(android): batch SAF lookups during download publication

This commit is contained in:
zarzet committed 2026-09-17 03:30:30 +07:00
1 parent 3eb44dc892
commit c868aac2cc
11 files changed
+668 -48

No files matched your search

@@ -0,0 +1,170 @@
package com.zarz.spotiflac
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SafDocumentLookupTest {
private val context get() = InstrumentationRegistry.getInstrumentation().targetContext
private val authority = "com.spotiflac.test.documents.lookup"
private val controlUri = Uri.parse("content://$authority.control")
private val treeUri = DocumentsContract.buildTreeDocumentUri(authority, "root")
private fun reset(count: Int = 0, mode: String = "normal", failFinalRename: Boolean = false): DocumentFile {
context.contentResolver.call(controlUri, "lookup.reset", null, Bundle().apply {
putInt("count", count)
putString("mode", mode)
putBoolean("failFinalRename", failFinalRename)
putString("targetPackage", context.packageName)
})
return requireNotNull(DocumentFile.fromTreeUri(context, treeUri))
}
private fun stats() = requireNotNull(context.contentResolver.call(controlUri, "lookup.stats", null, null))
private fun clearCounters() {
context.contentResolver.call(controlUri, "lookup.clearCounters", null, null)
}
@Test
fun projectedBatchReplacesThousandsOfNameQueriesAndClosesItsCursor() {
val parent = reset(count = 2000)
assertNull(parent.findFile("missing.flac"))
assertEquals(1, stats().getInt("children"))
assertEquals(2002, stats().getInt("documents"))
clearCounters()
val found = findSafChildren(context, parent, setOf("Track 0.flac", "Track 1999.flac", "missing.flac"))
assertEquals(setOf("Track 0.flac", "Track 1999.flac"), found.keys)
assertEquals(1, stats().getInt("children"))
assertEquals(0, stats().getInt("documents"))
assertEquals(1, stats().getInt("closed"))
clearCounters()
assertNull(findSafChild(context, parent, "missing.flac"))
assertEquals(1, stats().getInt("children"))
assertEquals(0, stats().getInt("documents"))
clearCounters()
assertTrue(findSafChildren(context, parent, emptySet()).isEmpty())
assertEquals(0, stats().getInt("children"))
}
@Test
fun nestedOpaqueIdsUnicodeAndRenameKeepTheChildDocument() {
val parent = reset()
val directory = requireNotNull(findSafChild(context, parent, "音楽 🎵"))
assertEquals("nested:音楽/opaque", DocumentsContract.getDocumentId(directory.uri))
assertTrue(directory.isDirectory)
val child = requireNotNull(findSafChild(context, directory, "歌 🎵.flac"))
val oldUri = child.uri
assertTrue(child.renameTo("Renamed.flac"))
assertNotEquals(oldUri, child.uri)
assertEquals("Renamed.flac", child.name)
assertEquals(child.uri, findSafChild(context, directory, "Renamed.flac")?.uri)
assertNull(findSafChild(context, parent, "Renamed.flac"))
assertNotNull(directory.createDirectory("Created"))
}
@Test
fun unsupportedProjectionNullCursorAndMissingColumnUseLegacyLookup() {
for (mode in listOf("throw", "null", "missing")) {
val parent = reset(count = 3, mode = mode)
val found = requireNotNull(findSafChild(context, parent, "Song.flac"))
assertEquals("original", DocumentsContract.getDocumentId(found.uri))
assertEquals(2, stats().getInt("children"))
assertEquals(5, stats().getInt("documents"))
assertEquals(if (mode == "missing") 7 else 6, stats().getInt("closed"))
}
}
@Test
fun rawDocumentFileRetainsLegacyBehavior() {
val directory = File(context.cacheDir, "lookup-raw").apply { mkdirs() }
try {
File(directory, "local.flac").writeText("audio")
val found = findSafChild(context, DocumentFile.fromFile(directory), "local.flac")
assertEquals("local.flac", found?.name)
} finally {
directory.deleteRecursively()
}
}
@Test
fun malformedTreeUriLeavesSourceIntact() {
val source = File(context.cacheDir, "lookup-invalid.flac").apply { writeText("original audio") }
try {
assertNull(SafDownloadHandler.writeFileToSaf(
context, "content://$authority/invalid", "", "Song.flac", "audio/flac", source.path,
))
assertEquals("original audio", source.readText())
} finally {
source.delete()
}
}
@Test
fun failedPublicationRenameRestoresOriginalWhenProviderChangesDocumentIds() {
val parent = reset(failFinalRename = true)
val source = File(context.cacheDir, "lookup-source.flac").apply { writeText("replacement") }
try {
assertNull(SafDownloadHandler.writeFileToSaf(context, treeUri.toString(), "", "Song.flac", "audio/flac", source.path))
val restored = requireNotNull(findSafChild(context, parent, "Song.flac"))
val content = context.contentResolver.openInputStream(restored.uri)?.bufferedReader()?.use { it.readText() }
assertEquals("original", content)
assertEquals("replacement", source.readText())
assertNull(findSafChild(context, parent, "Song.flac.replaced"))
assertNull(findSafChild(context, parent, "Song.flac.partial"))
} finally {
source.delete()
}
}
@Test
fun publicationPreservesBytesReportsStagesAndDetectsLaterExistingFile() {
val parent = reset(count = 2000)
val bytes = ByteArray(262144) { (it % 251).toByte() }
val source = File(context.cacheDir, "lookup-publish.flac").apply { writeBytes(bytes) }
try {
assertNull(findSafChild(context, parent, "Fresh.flac"))
clearCounters()
val result = requireNotNull(SafDownloadHandler.writeFileToSafIfAbsent(
context, treeUri.toString(), "", "Fresh.flac", "audio/flac", source.path,
))
assertTrue(!result.alreadyExists)
assertEquals(4, stats().getInt("children"))
assertEquals(1, stats().getInt("documents"))
val expectedStages = setOf(
"lock_wait", "directory", "existing_check", "cleanup", "create",
"open", "copy", "sync", "close", "replace", "total",
)
assertTrue(result.publishTimingsMs.keys.containsAll(expectedStages))
assertTrue(result.publishTimingsMs.values.all { it >= 0 })
val actual = context.contentResolver.openInputStream(Uri.parse(result.uri))?.use { it.readBytes() }
assertArrayEquals(bytes, actual)
assertEquals(result.uri, findSafChild(context, parent, "Fresh.flac")?.uri.toString())
assertNull(findSafChild(context, parent, "Fresh.flac.partial"))
source.writeText("must not replace existing audio")
val existing = requireNotNull(SafDownloadHandler.writeFileToSafIfAbsent(
context, treeUri.toString(), "", "Fresh.flac", "audio/flac", source.path,
))
assertTrue(existing.alreadyExists)
assertEquals(result.uri, existing.uri)
assertArrayEquals(bytes, context.contentResolver.openInputStream(Uri.parse(existing.uri))?.use { it.readBytes() })
} finally {
source.delete()
}
}
}