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.
This commit is contained in:
xbsheng committed 2026-10-21 06:33:29 +08:00
1 parent 687b5ee2a3
commit c66cfadad5
4 files changed
+144 -7

No files matched your search

+5 -1
View File
@@ -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
+1 -1
View File
@@ -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
+22 -5
View File
@@ -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
+116
View File
@@ -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()