mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
perf(android): batch SAF lookups during download publication
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user