From c66cfadad59e32a6ab7a67e3bcec40a0cb63de54 Mon Sep 17 00:00:00 2001 From: xbsheng Date: Wed, 21 Oct 2026 06:33:29 +0800 Subject: [PATCH] docs: the Google TV Streamer reproduces the setupwraith casting and crash-loop notes (#500) docs/TROUBLESHOOTING.md: the casting break was scoped to Google's own Chromecast hardware; a kirkwood box (UTTK.260317.003) hits it with the same log lines, and there the credentials live only in the receiver's process, so am force-stop loses them just as a reboot does. The crash-loop bullet gets that box's trace (positionChildAt / Task.moveToBack / TransitionController, every ~47 s, DeadSystemException) and the HOME bullet gets the measurement that the resolver table and the HOME role holder can both name the new launcher while the key still opens setupwraith/.RecoveryActivity. PROMPT.md says to confirm with 'dumpsys activity activities | grep mFocusedApp' before claiming the takeover worked; README's setupwraith bullet no longer implies only Chromecast loses casting. No examples/ change: #501 brings the kirkwood list, and its header already carries these quirks. --- PROMPT.md | 6 +- README.md | 2 +- docs/TROUBLESHOOTING.md | 27 +++++- tests/test_kirkwood_setupwraith_note.py | 116 ++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 tests/test_kirkwood_setupwraith_note.py diff --git a/PROMPT.md b/PROMPT.md index f38d0836..a0e54c31 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -74,7 +74,11 @@ Launcher Android TV launcher instead (Java, AndroidX RecyclerView, minSdk matching the TV, debug build is fine), install it over ADB, set it as the default home and disable the stock launcher. If several apps claim the HOME intent, list them with `cmd package query-activities -a android.intent.action.MAIN -c - android.intent.category.HOME` and set the default with `cmd package set-home-activity`. + android.intent.category.HOME` and set the default with `cmd package set-home-activity`. Those two + commands can both name your launcher while Home still opens something else: on some Google TV builds + `set-home-activity` prints `Success` and changes nothing, and setupwraith's `.RecoveryActivity` + keeps the key. Confirm with `dumpsys activity activities | grep mFocusedApp` before telling me the + takeover worked. - Before disabling the stock launcher, check which other apps claim HOME: on Google TV the setup wizard (`com.google.android.tungsten.setupwraith`) takes over even after the stock launcher is disabled, and the matching `examples/` header says what to do. Before disabling setupwraith, run diff --git a/README.md b/README.md index 84e1dc5b..a99ed620 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,7 @@ three that matter most: - **macOS blocks `adb connect`** ("No route to host") under Local Network privacy. `tools/tv.py` avoids the binary. - **Disable the stock launcher only after the new one is installed and running**, or Home lands on a factory test app. The install script orders this for you. -- **On Google TV the setup wizard** (`com.google.android.tungsten.setupwraith`) can take Home and crash-loop the system after the stock launcher is disabled. Disable it too, and list it in your `debloat.txt` so restore brings it back. But check `dumpsys wifi | grep -o 'cname=[^ ]*'` first: if the Wi-Fi network was saved during setup it belongs to setupwraith, and disabling it deletes the network (re-join it in Settings first; `tools/disable.sh` skips setupwraith until you do). On Google's own Chromecast it also costs phone casting, see [Troubleshooting](docs/TROUBLESHOOTING.md). Stuck already? `tools/tv.py 'pm enable --user 0 com.google.android.apps.tv.launcherx'`. +- **On Google TV the setup wizard** (`com.google.android.tungsten.setupwraith`) can take Home and crash-loop the system after the stock launcher is disabled. Disable it too, and list it in your `debloat.txt` so restore brings it back. But check `dumpsys wifi | grep -o 'cname=[^ ]*'` first: if the Wi-Fi network was saved during setup it belongs to setupwraith, and disabling it deletes the network (re-join it in Settings first; `tools/disable.sh` skips setupwraith until you do). On Google's own Chromecast and on the Google TV Streamer it also costs phone casting, see [Troubleshooting](docs/TROUBLESHOOTING.md). Stuck already? `tools/tv.py 'pm enable --user 0 com.google.android.apps.tv.launcherx'`. ## Questions people ask diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index aec21959..d12889b5 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -88,7 +88,14 @@ Things that bit us or contributors, so they don't bite you. Commands use `tools/ setupwraith's components is refused with a SecurityException. Disabling the whole `com.google.android.tungsten.setupwraith` package is what frees the slot. It is the first-run setup wizard, so everyday use is unaffected, but re-enable it before a factory reset - list it in - your `debloat.txt` and `tools/restore.sh` does that for you. + your `debloat.txt` and `tools/restore.sh` does that for you. Measured again on a Google TV + Streamer (`kirkwood`, `UTTK.260317.003`): a launcher built with `android:priority="3"` on its HOME + filter (`aapt2 dump xmltree` finds it in the installed APK) still has no `mPriority` line in + `dumpsys package`, and `cmd role get-role-holders android.app.role.HOME` names the new launcher + while the HOME key lands on `setupwraith/.RecoveryActivity` anyway. `cmd package resolve-activity` + and `query-activities` answer with the resolver table's winner, which is not necessarily the + activity that takes focus: `dumpsys activity activities | grep mFocusedApp` is the ground truth, + and it is what to check before saying the takeover worked. - **The TV drops off Wi-Fi for good the moment setupwraith is disabled**, seen on Google TV 14 (Sony BRAVIA 9 II, Philips R8G, Skyworth SWTV-24NA-4K NF, Xiaomi TV Box S 3rd gen MiTV-AFMU0) and on a TCL G07 on Google TV 11 (build AR17; on AR20 the network belonged to `android.uid.system`). A Wi-Fi @@ -113,9 +120,14 @@ Things that bit us or contributors, so they don't bite you. Commands use `tools/ `pm disable-user` on `setupwraith/.RecoveryActivity` is refused with `SecurityException: Shell cannot change component state`. So either debloat without setupwraith and launcherx, or re-enable setupwraith after each boot, wait for `ClientAuthSigner`, then disable it again: the receiver keeps - the credentials in memory, so casting survives until the next reboot. Reported on Google's own - Chromecast hardware so far; the Xiaomi Mi Box S 2nd gen list says casting from a Pixel still works - there with setupwraith off. + the credentials in memory, so casting survives until the next reboot - and only in that process. + `am force-stop com.google.android.apps.mediashell` followed by the receiver coming back prints + `Retry loading factory credentials` and `No private key, hence empty signature` again, so anything + that restarts it (a crash, a force-stop, an update) needs the same sequence. Reported on Google's + own Chromecast hardware (`boreal`) and, with the same log lines, on a Google TV Streamer + (`kirkwood`, `UTTK.260317.003`), where the `CastInfoProvider` also lives inside the setupwraith + APK; the Xiaomi Mi Box S 2nd gen list says casting from a Pixel still works there with + setupwraith off. - **The TV drops off Wi-Fi every minute right after the install**, seen on Google TV 14 (Xiaomi MiTV-MZTU0): the same setupwraith `.RecoveryActivity` took Home and system_server crash-looped on it. The install script now starts the new launcher before disabling the stock one, sets the @@ -123,7 +135,12 @@ Things that bit us or contributors, so they don't bite you. Commands use `tools/ Home resolves and re-enables the stock launcher if it is not the new one. Disable setupwraith as above. To confirm it is this bug: `logcat -b crash` shows `FATAL EXCEPTION IN SYSTEM PROCESS: android.display`, and the main log shows `setupwraith/.RecoveryActivity` being started every - 200 ms or so. If you're already in the loop: + 200 ms or so. On a Google TV Streamer (`kirkwood`, `UTTK.260317.003`) the same loop came with its + own signature - `IllegalArgumentException: positionChildAt: container=Task=... is not a child of + container=Task=1 current parent=null` at `WindowContainer.positionChildAt` / + `Task.moveToBack` / `TransitionController.tryStartCollectFromQueue` - and repeated roughly every + 47 s, each cycle taking setupwraith and SystemUI down with `DeadSystemException` too. If you're + already in the loop: `tools/tv.py 'pm enable --user 0 com.google.android.apps.tv.launcherx'`. - **`theme.sh` fails with "couldn't create file: Permission denied"**, seen on TCL G07 (Android 11): the shell is not in the group that may write `Android/data`. `theme.sh` stages the files in diff --git a/tests/test_kirkwood_setupwraith_note.py b/tests/test_kirkwood_setupwraith_note.py new file mode 100644 index 00000000..d44af6f9 --- /dev/null +++ b/tests/test_kirkwood_setupwraith_note.py @@ -0,0 +1,116 @@ +"""The setupwraith notes must carry the second box that reproduces them (#500). + +docs/TROUBLESHOOTING.md already documented the casting break and the `.RecoveryActivity` crash loop +from a Chromecast with Google TV HD and a Xiaomi MiTV-MZTU0. A Google TV Streamer (`kirkwood`, +`UTTK.260317.003`) reproduces both and adds two facts the doc was missing: the cast credentials live +only in the receiver's process, so a force-stop loses them just as a reboot does, and the HOME +resolver / role holder can both name the new launcher while the key still opens setupwraith. + +The tests read the bullets as bullets, not as grep hits: a sentence that drifts into a neighbouring +bullet would leave the doc just as useless. Run from the repository root: +python3 -m unittest discover -s tests -v +""" +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +DOC = (ROOT / "docs" / "TROUBLESHOOTING.md").read_text(encoding="utf-8") +README = (ROOT / "README.md").read_text(encoding="utf-8") +PROMPT = (ROOT / "PROMPT.md").read_text(encoding="utf-8") +KIRKWOOD = "UTTK.260317.003" + + +def bullets(text): + """Split a markdown doc into its `- **Title**` bullets, wrapping lines included.""" + found, current = [], None + for line in text.splitlines(): + if line.startswith("- **"): + if current is not None: + found.append(current) + current = line + elif current is not None and (line.startswith(" ") or not line.strip()): + current += "\n" + line + else: + if current is not None: + found.append(current) + current = None + if current is not None: + found.append(current) + return found + + +def bullet_containing(needle): + matches = [b for b in bullets(DOC) if needle in b] + assert len(matches) == 1, f"expected one bullet with {needle!r}, found {len(matches)}" + return re.sub(r"\s+", " ", matches[0]) + + +class KirkwoodNoteTest(unittest.TestCase): + def setUp(self): + self.casting = bullet_containing("Casting from a phone fails") + self.crash = bullet_containing("crash-looped") + self.home = bullet_containing("still doesn't own Home") + + def test_the_casting_break_is_not_scoped_to_googles_own_hardware(self): + self.assertIn("Google TV Streamer", self.casting) + self.assertIn(KIRKWOOD, self.casting) + self.assertNotIn("hardware so far", self.casting, + "the 'so far' wording is what the report corrects") + + def test_the_casting_bullet_names_where_the_provider_lives_on_kirkwood(self): + # The reporter checked `dumpsys package providers`: there is no separate tvsetup package, + # the CastInfoProvider classes ship inside the setupwraith APK. That is why disabling the + # wizard takes casting with it on a box Google never shipped a wizard for. + self.assertIn("CastInfoProvider", self.casting) + self.assertIn("setupwraith", self.casting) + + def test_a_receiver_restart_loses_the_credentials_too(self): + self.assertIn("force-stop", self.casting) + self.assertIn("mediashell", self.casting) + self.assertIn("Retry loading factory credentials", self.casting) + self.assertIn("next reboot", self.casting, + "the reboot case is still the common one and must stay") + + def test_the_casting_workaround_still_says_what_to_do(self): + # The correction must not cost the instruction: enable, wait for the signer, disable again. + self.assertIn("ClientAuthSigner", self.casting) + self.assertIn("re-enable setupwraith after each boot", self.casting) + self.assertIn("cannot change component state", self.casting) + + def test_the_home_bullet_says_resolver_and_role_holder_can_both_lie(self): + self.assertIn("mFocusedApp", self.home) + self.assertIn("resolve-activity", self.home) + self.assertIn("get-role-holders", self.home) + self.assertIn("RecoveryActivity", self.home) + + def test_the_home_bullet_keeps_the_package_level_way_out(self): + # Flipping to the component would be wrong: the shell cannot change component state. + self.assertIn("pm disable-user", self.home) + self.assertIn("SecurityException", self.home) + + def test_the_crash_loop_bullet_carries_the_kirkwood_trace(self): + self.assertIn("positionChildAt", self.crash) + self.assertIn("Task.moveToBack", self.crash) + self.assertIn("DeadSystemException", self.crash) + self.assertIn(KIRKWOOD, self.crash) + + def test_the_crash_loop_bullet_keeps_the_way_out(self): + self.assertIn("pm enable --user 0 com.google.android.apps.tv.launcherx", self.crash) + self.assertIn("FATAL EXCEPTION IN SYSTEM PROCESS", self.crash) + + def test_the_readme_clause_covers_both_boxes(self): + clause = next(line for line in README.splitlines() if "costs phone casting" in line) + self.assertIn("Google TV Streamer", clause) + self.assertIn("Chromecast", clause) + self.assertIn("docs/TROUBLESHOOTING.md", clause) + + def test_the_prompt_asks_for_the_focused_app_before_claiming_success(self): + self.assertIn("mFocusedApp", PROMPT) + self.assertIn("set-home-activity", PROMPT) + self.assertIn("query-activities", PROMPT) + + +if __name__ == "__main__": + unittest.main()