package link.roadio.riff.huprovision; import android.content.Context; import android.net.Uri; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import org.json.JSONObject; /** Explicit device probe using the existing disposable Indexer SAF fixture, never working grants. */ final class ProvisionerSelfTest { static JSONObject run(Context context) throws Exception { if (!context.getPackageName().equals("link.roadio.riff.indexer.module.test")) { throw new IllegalStateException("Self-test requires the disposable Indexer test package"); } ClassLoader loader = context.getClassLoader(); Class provider = loader.loadClass("link.roadio.riff.indexer.android.cuetree.SafProbeDocumentsProvider"); Object companion = provider.getField("Companion").get(null); String root = "opaque-music-root", second = "opaque-books-root"; Uri tree = DocumentsContract.buildTreeDocumentUri(context.getPackageName() + ".safprobe.documents", root); Uri otherTree = DocumentsContract.buildTreeDocumentUri(context.getPackageName() + ".safprobe.documents", second); context.getContentResolver().getType(DocumentsContract.buildDocumentUriUsingTree(tree, root)); java.lang.reflect.Method reset = companion.getClass().getMethod("reset"); java.lang.reflect.Method seed = companion.getClass().getMethod("seed", String.class, String.class, String.class, byte[].class); reset.invoke(companion); ConfigurationDocuments docs = new ConfigurationDocuments(context.getContentResolver()); try { String folder = (String) seed.invoke(companion, root, ".riff", Document.MIME_TYPE_DIR, new byte[0]); String other = (String) seed.invoke(companion, second, ".riff", Document.MIME_TYPE_DIR, new byte[0]); seed.invoke(companion, other, "ui_config.json", "application/json", "{\"player\":{\"bookmarkMinSpacingSeconds\":42}}".getBytes(StandardCharsets.UTF_8)); java.util.List trees = Arrays.asList(tree, otherTree); docs.provision(loader, trees, tree, true); if (docs.child(tree, docs.directory(tree), "ui_config.json", false) != null) throw new AssertionError("Dry run wrote a file"); JSONObject created = docs.provision(loader, trees, tree, false); Uri file = Uri.parse(created.getString("configuration_uri")); String initial = docs.read(file); Class discoveryType = loader.loadClass("link.roadio.riff.shared.config.ExternalConfigurationDiscovery"); Object discovery = discoveryType.getConstructor(android.content.ContentResolver.class).newInstance(context.getContentResolver()); @SuppressWarnings("unchecked") java.util.List discovered = (java.util.List) discoveryType.getMethod("readAll", java.util.List.class).invoke(discovery, trees); Object actual = ConfigurationDocuments.parseAll(loader, discovered); ConfigurationDocuments.requireSettings(actual); if (((Number) ConfigurationDocuments.value(actual, "player.bookmarkMinSpacingSeconds", true, false)).intValue() != 42) { throw new AssertionError("Production discovery lost a complementary source"); } docs.provision(loader, trees, tree, false); if (!initial.equals(docs.read(file))) throw new AssertionError("Provisioning is not idempotent"); docs.write(file, "{\"player\":{\"rewindPreviousThresholdSeconds\":{\"music\":31}}}"); JSONObject updated = docs.provision(loader, trees, tree, false); if (new JSONObject(docs.read(file)).getJSONObject("player").getJSONObject("rewindPreviousThresholdSeconds").getInt("music") != 31) { throw new AssertionError("Unrelated value changed"); } String valid = docs.read(file); Uri otherFile = docs.child(otherTree, docs.directory(otherTree), "ui_config.json", false); docs.write(otherFile, "{\"skins\":{\"default\":{\"behavior\":{\"dimBacklightPercent\":30}}}}"); boolean rejected = false; try { docs.provision(loader, trees, tree, false); } catch (Exception expected) { rejected = true; } if (!rejected || !valid.equals(docs.read(file))) throw new AssertionError("Conflicting source was overwritten"); docs.write(file, "{\"schemaVersion\":1}"); rejected = false; try { docs.provision(loader, Collections.singletonList(tree), tree, false); } catch (Exception expected) { rejected = true; } if (!rejected || !docs.read(file).equals("{\"schemaVersion\":1}")) throw new AssertionError("Invalid input was overwritten"); docs.write(file, valid); docs.write(otherFile, "{}"); ConfigurationDocuments failing = new ConfigurationDocuments(context.getContentResolver()) { int writes; @Override void write(Uri uri, String text) throws Exception { super.write(uri, text); if (++writes == 1) throw new java.io.IOException("Injected failure after write"); } }; rejected = false; try { failing.provision(loader, trees, tree, false); } catch (Exception expected) { rejected = true; } if (!rejected || !valid.equals(docs.read(file))) throw new AssertionError("Original bytes not restored"); DocumentsContract.deleteDocument(context.getContentResolver(), file); ConfigurationDocuments failingCreate = new ConfigurationDocuments(context.getContentResolver()) { @Override void write(Uri uri, String text) throws Exception { super.write(uri, text); throw new java.io.IOException("Injected new-file write failure"); } }; rejected = false; try { failingCreate.provision(loader, trees, tree, false); } catch (Exception expected) { rejected = true; } if (!rejected || docs.child(tree, docs.directory(tree), "ui_config.json", false) != null) { throw new AssertionError("Failed new file not removed"); } return new JSONObject().put("passed", true).put("checks", 9).put("fixture_only", true) .put("cases", "dry-run, creation, production discovery, idempotence, multi-root preservation, conflicting source rejection, invalid input rejection, existing-file rollback, new-file rollback"); } finally { reset.invoke(companion); } } }