/* ── the tree in the car, with the app FORCE-STOPPED ─────── LAST in the phone leg, and it kills the app on purpose. The claim round fifty makes is not "the browse tree is correct" but "the browse tree is there when the app is not" — a car that has to be handed a running phone first is no use to anybody — so every check below is taken against a process the SYSTEM built for the browse, with no Activity, no Tauri and no WebView in it. What drives it is round forty-nine's prober: a component in its own process that `am start-service` can start cold, connects to our own MediaLibraryService as a framework MediaBrowser and walks the tree. What is ASSERTED is the service's own log of what it answered (see autoNodes()) — which is the one reading a bug in the prober cannot fake. Nothing here can say whether AUTO renders any of it. That needs the Desktop Head Unit or a car, and ANDROID.md says so. What this proves is the half that can be proved from here, and it is the half that has always been broken in this port when something was. */ { const say = (...a) => { try { return adb(...a).toString(); } catch (e) { return "ERR " + e.message; } }; const PROBE = "com.soulstream.app/com.soulstream.mediasession.AutoProbeService"; const pidBefore = (say("shell", "pidof", "com.soulstream.app").trim().split(/\s+/)[0]) || ""; /* the WebView dies with the process; drop the socket first so the walk's own finally has nothing to trip over */ if (cdp) { cdp.close(); cdp = null; } say("shell", "am", "force-stop", "com.soulstream.app"); await sleep(3000); say("logcat", "-c"); const launched = say("shell", "am", "start-service", "-n", PROBE).trim(); /* Polled, never fixed-waited. A cold `dlopen` of a ~100 MB debug .so plus the first SQLite open on a phone is not a number anybody here has measured, and reading a slow answer as a failure has cost this project a round twice. Eight nodes is the shape the assertions want — a root, four tabs and three levels — and sixty seconds is the ceiling, not the wait. */ let rows = []; for (let i = 0; i < 60; i++) { await sleep(1000); rows = autoNodes(); if (rows.length >= 8 && rows.some((r) => r.node.startsWith("ss:root"))) break; } const seen = rows.map((r) => r.node); /* ── the root: within the limit the browser hinted, and all browsable. Google's own rule, and Auto's: only a browsable item may be a root tab, and a root that overflows the limit has its extra tabs quietly dropped or buried. `lim` is what LibraryBridge read out of the root hints, so a hardcoded four would show up here as a limit that never changes rather than as nothing at all. */ const root = rows.find((r) => r.node.startsWith("ss:root")) || null; const rootOk = !!root && root.lim >= 1 && root.n >= 1 && root.n <= root.lim && root.br === root.n && root.pl === 0 && root.uniq === root.n && root.titled === root.n && root.kinds === "tab"; if (!rootOk) problems.push("EXPECT auto-root " + JSON.stringify({ root, launched, seen })); console.log((rootOk ? " ok " : " FAIL ") + "auto-root".padEnd(16) + " " + JSON.stringify(root ? { n: root.n, lim: root.lim, br: root.br, pl: root.pl, kinds: root.kinds, ms: root.ms } : { root: null, launched })); /* ── the four tabs: each loads, each row has a stable id of its own and a title, and every cover the library HAS became a content:// URI (`fail` is the count that did not — a provider root that does not reach the artwork lands there, and nowhere else). `art >= 1` rather than `art === n` on purpose: an album with no art_path genuinely has no cover, and demanding one would be a check that lies about the library. */ const TABS = ["ss:tab:recent", "ss:tab:albums", "ss:tab:artists", "ss:tab:playlists"]; const tabs = TABS.map((id) => rows.find((r) => r.node === id) || null); const tabOk = (t) => !!t && t.n >= 1 && t.uniq === t.n && t.titled === t.n && t.fail === 0 && t.art >= 1; const tabsOk = tabs.every(tabOk); const tabReport = TABS.map((id, i) => [id.slice(7), tabs[i] ? [tabs[i].n, tabs[i].art, tabs[i].fail, tabs[i].ms] : null]); if (!tabsOk) problems.push("EXPECT auto-tabs " + JSON.stringify({ tabs, seen })); console.log((tabsOk ? " ok " : " FAIL ") + "auto-tabs".padEnd(16) + " " + JSON.stringify(tabReport)); /* ── three levels deep, which is the brief. An album's children are tracks and every one of them is playable and nothing else; an artist's children are albums and every one of them is browsable; and at least one album was reached UNDER an artist rather than only under Albums, which is the third level and the only clause here that the Albums tab alone cannot satisfy. */ const albums = rows.filter((r) => r.node.startsWith("ss:album:")); const artists = rows.filter((r) => r.node.startsWith("ss:artist:")); const lists = rows.filter((r) => r.node.startsWith("ss:playlist:")); const firstArtist = rows.findIndex((r) => r.node.startsWith("ss:artist:")); const deeper = firstArtist >= 0 && rows.slice(firstArtist + 1).some((r) => r.node.startsWith("ss:album:")); const depthOk = albums.length >= 1 && artists.length >= 1 && lists.length >= 1 && deeper && albums.every((r) => r.n >= 1 && r.kinds === "track" && r.pl === r.n && r.br === 0 && r.uniq === r.n && r.titled === r.n) && artists.every((r) => r.n >= 1 && r.kinds === "album" && r.br === r.n && r.pl === 0 && r.uniq === r.n) && lists.every((r) => r.n === 0 || (r.kinds === "plitem" && r.pl === r.n && r.uniq === r.n)); const depthSeen = { albums: albums.length, artists: artists.length, playlists: lists.length, deeper, kinds: rows.map((r) => r.node + ":" + r.kinds) }; if (!depthOk) problems.push("EXPECT auto-depth " + JSON.stringify(depthSeen)); console.log((depthOk ? " ok " : " FAIL ") + "auto-depth".padEnd(16) + " " + JSON.stringify({ albums: albums.length, artists: artists.length, playlists: lists.length, deeper })); /* ── the covers, opened rather than believed. `open` is one real openFileDescriptor against the first URI the service minted: it is what separates "the FileProvider produced a plausible URI" from "the file is there and readable through it", and a files-path root — the one this round nearly shipped — fails it while breaking nothing else. It cannot prove the GRANT: the prober shares our uid and would read the provider with no grant at all. Only a car proves that, and grantUriPermission is the reason the line exists in LibraryBridge at all. */ const artOk = rows.length >= 1 && rows.some((r) => r.open === "ok") && rows.every((r) => r.fail === 0); const artSeen = { open: [...new Set(rows.map((r) => r.open))].join("|"), fail: rows.reduce((a, r) => a + Math.max(0, r.fail), 0), art: rows.reduce((a, r) => a + Math.max(0, r.art), 0) }; if (!artOk) problems.push("EXPECT auto-art " + JSON.stringify({ artSeen, notes: rows.map((r) => r.node + " " + r.note) })); console.log((artOk ? " ok " : " FAIL ") + "auto-art".padEnd(16) + " " + JSON.stringify(artSeen)); /* ── and COLD, which is the whole round. A new pid (the app was force-stopped, so the one serving the browse cannot be the one that was playing), no resumed MainActivity, and no WebView devtools socket for it — the three things that would each mean the app quietly came back and answered from the warm path. */ const pidAfter = (say("shell", "pidof", "com.soulstream.app").trim().split(/\s+/)[0]) || ""; const acts = say("shell", "dumpsys", "activity", "activities"); const resumed = acts.split(/\r?\n/).some((l) => /ResumedActivity/.test(l) && /com\.soulstream\.app/.test(l)); const socks = say("shell", "cat", "/proc/net/unix"); const devtools = !!pidAfter && new RegExp("webview_devtools_remote_" + pidAfter + "\\b").test(socks); const coldOk = /^\d+$/.test(pidAfter) && pidAfter !== pidBefore && !resumed && !devtools && rows.length >= 1; const coldSeen = { pidBefore, pidAfter, resumed, devtools, nodes: rows.length }; if (!coldOk) problems.push("EXPECT auto-cold " + JSON.stringify({ coldSeen, launched })); console.log((coldOk ? " ok " : " FAIL ") + "auto-cold".padEnd(16) + " " + JSON.stringify(coldSeen)); /* ── and how long each node took, as numbers, the way the boot budget is read. Both bounds on purpose: an `ms` of -1 is a line that did not carry one, and a one-sided test would sail straight through it. The FIRST node pays the cold dlopen and the first SQLite open and gets a second and a half; everything after it is a warm query and gets 400 ms. Every measured value is printed, because the first real run may well move both numbers and a budget guessed at twice is worse than one printed once. */ const msAll = rows.map((r) => r.ms); const latOk = rows.length >= 1 && msAll.every((m) => m >= 0) && msAll[0] < 1500 && msAll.slice(1).every((m) => m < 400); const latSeen = { first: msAll[0] ?? -1, rest: msAll.slice(1), hop: rows.map((r) => r.hop) }; if (!latOk) problems.push("EXPECT auto-latency " + JSON.stringify({ latSeen, seen })); console.log((latOk ? " ok " : " FAIL ") + "auto-latency".padEnd(16) + " " + JSON.stringify(latSeen)); /* put the phone back the way the walk found it: the app open. It restores PAUSED (session restore's own rule), so this starts nothing playing. */ say("shell", "am", "start", "-n", "com.soulstream.app/.MainActivity"); await sleep(2000); }