perf(android): batch SAF library recovery metadata queries

This commit is contained in:
zarzet committed 2026-09-17 17:38:15 +07:00
1 parent 1ef81b4530
commit 1387a28d66
3 files changed
+183 -21

No files matched your search

@@ -71,6 +71,11 @@ public final class LookupTestDocumentsProvider extends DocumentsProvider {
}
add("nested:音楽/opaque", "root", "音楽 🎵", Document.MIME_TYPE_DIR);
add("song:opaque/%", "nested:音楽/opaque", "歌 🎵.flac", "audio/flac");
if (extras.getBoolean("mimeCases")) {
add("mime:null", "root", "Null.flac", null);
add("mime:empty", "root", "Empty.flac", "");
add("mime:generic", "root", "Generic.flac", "application/octet-stream");
}
Entry original = add("original", "root", "Song.flac", "audio/flac");
try (FileOutputStream output = new FileOutputStream(original.file)) {
output.write(new byte[] {'o', 'r', 'i', 'g', 'i', 'n', 'a', 'l'});
@@ -84,7 +89,8 @@ public final class LookupTestDocumentsProvider extends DocumentsProvider {
extras.getString("targetPackage"),
DocumentsContract.buildTreeDocumentUri("com.spotiflac.test.documents.lookup", "root"),
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION |
(extras.getBoolean("persistable") ? Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION : 0)
);
} finally {
Binder.restoreCallingIdentity(identity);
@@ -136,6 +142,7 @@ public final class LookupTestDocumentsProvider extends DocumentsProvider {
case Document.COLUMN_DISPLAY_NAME: row[i] = entry.name; break;
case Document.COLUMN_MIME_TYPE: row[i] = entry.mime; break;
case Document.COLUMN_SIZE: row[i] = entry.file.length(); break;
case Document.COLUMN_LAST_MODIFIED: row[i] = 1234L; break;
case Document.COLUMN_FLAGS:
row[i] = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE |
Document.FLAG_SUPPORTS_RENAME | Document.FLAG_DIR_SUPPORTS_CREATE;
@@ -173,6 +180,7 @@ public final class LookupTestDocumentsProvider extends DocumentsProvider {
@Override
public Cursor queryChildDocuments(String parentId, String[] projection, String sortOrder) {
childQueries++;
if (projectionMode.equals("all-null")) return null;
boolean projected = projection != null && Arrays.asList(projection).contains(Document.COLUMN_DISPLAY_NAME);
if (projected && projectionMode.equals("throw")) throw new UnsupportedOperationException("projection");
if (projected && projectionMode.equals("null")) return null;
@@ -0,0 +1,130 @@
package com.zarz.spotiflac
import android.content.Intent
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.IOException
import org.json.JSONArray
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SafScanChildrenTest {
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 val grantFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
private fun reset(count: Int = 0, mode: String = "normal", mimeCases: Boolean = false): DocumentFile {
context.contentResolver.call(controlUri, "lookup.reset", null, Bundle().apply {
putInt("count", count)
putString("mode", mode)
putBoolean("mimeCases", mimeCases)
putBoolean("persistable", true)
putString("targetPackage", context.packageName)
})
context.contentResolver.takePersistableUriPermission(treeUri, grantFlags)
return requireNotNull(DocumentFile.fromTreeUri(context, treeUri))
}
private fun stats() = requireNotNull(context.contentResolver.call(controlUri, "lookup.stats", null, null))
@After
fun releaseTreeGrant() {
if (context.contentResolver.persistedUriPermissions.any { it.uri == treeUri }) {
context.contentResolver.releasePersistableUriPermission(treeUri, grantFlags)
}
}
@Test
fun richRowsContainFileMetadataAndTraversableChildrenWithoutNameQueries() {
val root = reset(count = 2000)
val children = context.listSafChildrenOrThrow(root)
assertEquals(2002, children.size)
assertEquals(1, stats().getInt("children"))
assertEquals(0, stats().getInt("documents"))
assertTrue(children.all { it.lastModified == 1234L })
val directory = children.single { it.isDirectory }
assertFalse(directory.isFile)
assertEquals("nested:音楽/opaque", DocumentsContract.getDocumentId(directory.doc.uri))
val nested = context.listSafChildrenOrThrow(directory.doc, includeLastModified = false).single()
assertEquals("歌 🎵.flac", nested.name)
assertTrue(nested.isFile)
assertEquals(0L, nested.lastModified)
assertEquals(2, stats().getInt("children"))
assertEquals(0, stats().getInt("documents"))
assertTrue(nested.doc.renameTo("Renamed.flac"))
assertEquals("Renamed.flac", nested.doc.name)
}
@Test
fun unknownAndEmptyMimeStayNonFilesWhileGenericMimeRemainsAFile() {
val root = reset(mimeCases = true)
val entries = context.listSafChildrenOrThrow(root, includeLastModified = false).associateBy { it.name }
assertFalse(requireNotNull(entries["Null.flac"]).isFile)
assertFalse(requireNotNull(entries["Empty.flac"]).isFile)
assertTrue(requireNotNull(entries["Generic.flac"]).isFile)
assertEquals("", JSONObject(context.resolveSafFile(treeUri.toString(), "", "Null.flac")).getString("uri"))
assertEquals("", JSONObject(context.resolveSafFile(treeUri.toString(), "", "Empty.flac")).getString("uri"))
assertTrue(JSONObject(context.resolveSafFile(treeUri.toString(), "", "Generic.flac")).getString("uri").isNotBlank())
}
@Test
fun rejectedRichProjectionRetainsLegacyMetadataFallback() {
val root = reset(mode = "throw", mimeCases = true)
val entries = context.listSafChildrenOrThrow(root).associateBy { it.name }
assertEquals(2, stats().getInt("children"))
assertTrue(stats().getInt("documents") > 0)
assertTrue(requireNotNull(entries["音楽 🎵"]).isDirectory)
assertFalse(requireNotNull(entries["Null.flac"]).isFile)
assertFalse(requireNotNull(entries["Empty.flac"]).isFile)
assertTrue(requireNotNull(entries["Generic.flac"]).isFile)
assertTrue(entries.values.all { it.lastModified == 1234L })
}
@Test
fun resolverWalksNestedDirectoriesWithConstantQueryCount() {
reset(count = 2000)
val result = JSONObject(context.resolveSafFile(treeUri.toString(), "", "歌 🎵.flac"))
assertEquals("song:opaque/%", DocumentsContract.getDocumentId(Uri.parse(result.getString("uri"))))
assertEquals("音楽 🎵", result.getString("relative_dir"))
assertEquals(3, stats().getInt("children"))
assertEquals(0, stats().getInt("documents"))
}
private fun inspect(vararg names: String): JSONArray {
val requests = JSONArray()
for ((index, name) in names.withIndex()) {
requests.put(JSONObject().put("key", index.toString()).put("tree_uri", treeUri.toString())
.put("relative_dir", "").put("current_uri", "").put("file_names", JSONArray().put(name)))
}
return JSONObject(context.inspectSafFiles(requests.toString())).getJSONArray("results")
}
@Test
fun batchInspectionFindsNestedRowsAndSeparatesMissingFromUnreadable() {
reset(count = 2000)
val results = inspect("歌 🎵.flac", "Missing.flac")
assertEquals("found", results.getJSONObject(0).getString("status"))
assertEquals("音楽 🎵", results.getJSONObject(0).getString("relative_dir"))
assertEquals("missing", results.getJSONObject(1).getString("status"))
assertEquals(3, stats().getInt("children"))
// Only fixed root permission/type checks; no per-child lookups.
assertTrue(stats().getInt("documents") < 10)
val root = reset(mode = "all-null")
assertThrows(IOException::class.java) { context.listSafChildrenOrThrow(root) }
assertEquals("unknown", inspect("Missing.flac").getJSONObject(0).getString("status"))
}
}