docs: the key-helper volume trade on a second set, a Xiaomi MiTV-MOSR4 (#944)

This commit is contained in:
xbsheng committed 2026-10-22 20:34:04 +08:00
1 parent 22dc48fe21
commit 8d42904320
5 files changed
+197 -2

No files matched your search

+4 -1
View File
@@ -607,7 +607,10 @@ alone, `InputKeyService` alone and both together all broke it, with `settings pu
enabled_accessibility_services ''` plus `accessibility_enabled 0` restoring it. The dumps show
`capabilities=8` (`CAPABILITY_CAN_FILTER_KEY_EVENTS`), so the platform routes every key through a filter
path that drops the repeats, before an app sees them; the launcher's own helpers return `false` for keys
they do not map, so they are not consuming the volume key. Check the state first, do not rewrite it blind:
they do not map, so they are not consuming the volume key. The same was reported on a Xiaomi MiTV-MOSR4
(`irobot`, Android 11, `RTM5.220609.003.1265`) with the launcher's own helpers, both and `InputKeyService`
alone (#944), where the empty list was not tried because it also unhooks the buttons the helpers were
installed for. Check the state first, do not rewrite it blind:
```sh
tools/tv.py 'settings get secure enabled_accessibility_services'
+24
View File
@@ -78,6 +78,30 @@ home, and the [G09 example list](../../examples/tcl-g09-google-tv/debloat.txt) d
after the cleanup only a helper can answer them. That is where the trade comes from: on this firmware
the buttons and the volume ramp cannot both be had, because both need the same filter capability.
## The same trade on a Xiaomi MediaTek set
A second owner reported it on [#944](https://github.com/Gathin23/cleanyourtv/issues/944): Xiaomi
MiTV-MOSR4 (`irobot`, MediaTek), Google TV / Android 11 (SDK 30), firmware `RTM5.220609.003.1265`.
Holding volume up with `InputKeyService` and `HomeKeyService` bound moves it one step, and
`InputKeyService` alone is still one step; `dumpsys accessibility` lists the bound service with
`capabilities=8`. Same symptom, same capability, second vendor and second Android version.
Two things differ from the bisect above, and both are worth keeping. The empty-list control was not run:
on this set clearing the list unhooks the buttons the helpers were installed for - with launcherx
disabled, its Input, Settings, YouTube and Netflix buttons reach only
`com.xiaomi.android.tvsetup.partnercustomizer/.GlobalKeyReceiver`, which starts launcherx activities and
crashes (the [MOSR4 note](xiaomi-mitv-mosr4.md) has that part, and its install line passes both helpers).
So "the ramp returns with no service bound" is still measured on the G09 alone. And the build string in
the issue is not the one that note carries (`RTM5.220609.003/1585`, written on another unit of the same
model), so treat the two as separate units until both fingerprints are read: the model, the firmware
family and the behaviour are what they agree on.
That is still enough to unhook the mechanism from "this firmware": the volume keys take the early
question from the window policy on Android 11 and 12, on two vendors, whenever a bound service holds
`CAPABILITY_CAN_FILTER_KEY_EVENTS`. The trade and the choices below are the same on both sets. The
reporter's own suggestion - a helper that synthesizes the repeat with `adjustSuggestedStreamVolume` while
a volume key is held - is launcher code, and an off-by-default switch at best; it is not in this note.
## Choices
- **Keep the repeat and skip the helpers.** Do not pass `INPUT_KEY_SERVICE=1` or `HOME_KEY_SERVICE=1`
+8
View File
@@ -55,6 +55,14 @@ the real remote (no `GLOBAL_BUTTON` lines, no crashes), using an equivalent key
stand-alone launcher; not yet with `com.gathin.tvhome`. The Netflix button went to this receiver,
not to overseaservice. The mic button keeps working.
Both helpers cost the remote's live key repeat on this set: holding volume up or down moves one step
per press, with `InputKeyService` alone as well as with both, and `dumpsys accessibility` shows the bound
service at `capabilities=8` (`CAPABILITY_CAN_FILTER_KEY_EVENTS`) - [#944](https://github.com/Gathin23/cleanyourtv/issues/944).
The bisect, the ways back to either behaviour and the way the repeat is lost are in
[the G09 note](tcl-55p71b-g09-key-repeat.md). The empty-list control was not tried here: clearing the
list unhooks the buttons this section installs the helpers for, so "the ramp returns with nothing bound"
is that note's measurement, not this set's.
## Other
- `com.mitv.tvhome.mitvplus` is re-enabled at every boot.
+1 -1
View File
@@ -222,7 +222,7 @@ on that firmware, grouped and commented. These are starting points: comment out
| `xiaomi-mitv-mftr0/` | Xiaomi MiTV-MFTR0 (freeguy, MediaTek mt9653), China firmware OS2.0.23.0.RFTMATV (HyperOS TV 2) | Android 11 (HyperOS TV 2, CN build) | Second CN set from the ASTR0 sessions, one HyperOS generation earlier (Android 11): same ad screensaver carrier (`com.xiaomi.mitv.hyper.screensaver`) and the same `com.ts.launcher` allowlist trampoline for third-party Home with PatchWall disabled, both carried over and verified; no WPS/overseaservice/adservices here; keep `com.xiaomi.mitv.smartpanel` (Menu-button panel); ADB needs the on-TV RSA confirmation once and closes on every reboot; [debloat and Home notes](../docs/devices/xiaomi-mitv-mftr0-debloat-home.md), [inputs/settings notes](../docs/devices/xiaomi-mitv-mftr0-freeguy.txt) |
| `xiaomi-mitv-moeu0/` | Xiaomi TV S Mini LED 75 2025 (MiTV-MOEU0, ladybird, MediaTek) | Google TV 14 | Mi TV+ banners and MiChannel rows, Mi analytics, TV Manager cleaner, Play auto-installs; setupwraith off (its Wi-Fi network belonged to the system here); remote's Settings 176, YouTube 190, Apps 284 and Xiaomi TV+ 192 noted |
| `xiaomi-mitv-moor2/` | Xiaomi MiTV-MOOR2 (venom, MediaTek) | Google TV 11 | Mi ad row and PatchWall, Mi analytics, MiLink, store demo; setup wizard off so a third-party launcher can own Home |
| `xiaomi-mitv-mosr4/` | Xiaomi MiTV-MOSR4 (irobot, MediaTek) | Google TV 11 | Mi ad rows and PatchWall, Mi analytics, TV Manager, Miracast sink, OTA off by choice; setup wizard off so a third-party launcher can own Home; `overseaservice` hosts the Netflix key and must stay; second unit: setupwraith before launcherx, remote buttons need both key services and `config.json`; [notes](../docs/devices/xiaomi-mitv-mosr4.md) |
| `xiaomi-mitv-mosr4/` | Xiaomi MiTV-MOSR4 (irobot, MediaTek) | Google TV 11 | Mi ad rows and PatchWall, Mi analytics, TV Manager, Miracast sink, OTA off by choice; setup wizard off so a third-party launcher can own Home; `overseaservice` hosts the Netflix key and must stay; second unit: setupwraith before launcherx, remote buttons need both key services and `config.json`, and those services cost the volume's live repeat; [notes](../docs/devices/xiaomi-mitv-mosr4.md) |
| `xiaomi-mitv-mssp0/` | Xiaomi MiTV-MSSP0 / Xiaomi Mi TV 4A 32 (amelie), firmware PI.5926 | Android TV 9 | Xiaomi telemetry and promo helpers, Google recommendation feeds; Assistant, Gallery and media browser off by owner choice; Clock replaces Backdrop; TV inputs, Gboard, Bluetooth and firmware update settings kept; 11 packages, applied October 19 and checked again October 21 |
| `xiaomi-mitv-mssp1/` | Xiaomi MiTV-MSSP1 (nino), firmware PI.5925 | Android TV 9 | Eight Xiaomi telemetry/content and Google recommendation packages; replacement Home reboot-checked; updater, Assistant, Chromecast, USB media and TV input services kept enabled |
| `xiaomi-mitv-mssp3/` | Xiaomi MiTV-MSSP3 (machuca, MediaTek) | Android TV 11 | PatchWall, Mi analytics, Netflix observer; Chromecast, screensavers, unused streaming apps off by choice |
+160
View File
@@ -0,0 +1,160 @@
"""Pins the second data point for the key-helper/auto-repeat trade (#944).
docs/devices/tcl-55p71b-g09-key-repeat.md was written on one TCL build, and its own Limits said the
claim belongs to that build: whether other firmwares do the same was open. #944 answers part of that
with a Xiaomi MiTV-MOSR4 (Google TV 11, MediaTek) where the launcher's own helpers cost the same repeat,
which is worth recording next to the bisect - but it is a weaker report than the first one: the owner did
not run the empty-list control (clearing the list unhooks the buttons he installed the helpers for), and
the build string he gives is not the one docs/devices/xiaomi-mitv-mosr4.md carries. These tests keep the
section from growing into a claim the report does not support, and keep the two notes and the two
entry points pointing at each other.
"""
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
G09 = ROOT / "docs" / "devices" / "tcl-55p71b-g09-key-repeat.md"
MOSR4 = ROOT / "docs" / "devices" / "xiaomi-mitv-mosr4.md"
TROUBLE = ROOT / "docs" / "TROUBLESHOOTING.md"
INDEX = ROOT / "examples" / "README.md"
JAVA = ROOT / "launcher" / "app" / "src" / "main" / "java"
HEADING = "## The same trade on a Xiaomi MediaTek set"
BUILD_IN_ISSUE = "RTM5.220609.003.1265"
BUILD_IN_NOTE = "RTM5.220609.003/1585"
RECEIVER = "com.xiaomi.android.tvsetup.partnercustomizer/.GlobalKeyReceiver"
def flat(path):
return re.sub(r"\s+", " ", path.read_text(encoding="utf-8"))
def section_of(text, heading):
"""The flattened body under `heading`, up to the next `##` heading; None when it is not there."""
flat_text = re.sub(r"\s+", " ", text)
if heading not in flat_text:
return None
rest = flat_text.split(heading, 1)[1]
following = re.search(r"\s##\s", rest)
return rest if following is None else rest[:following.start()]
def section(path):
return section_of(path.read_text(encoding="utf-8"), HEADING)
def links(path):
text = path.read_text(encoding="utf-8")
return [str((path.parent / target).resolve()) for target in re.findall(r"\]\(([^)\s]+)\)", text)
if not target.startswith(("http", "#"))]
class SecondUnitTest(unittest.TestCase):
def setUp(self):
body = section(G09)
self.assertIsNotNone(body, f"{HEADING} is gone from the G09 note")
self.section = body or ""
def test_says_which_set_and_which_build(self):
for needle in ("#944", "MiTV-MOSR4", "`irobot`", BUILD_IN_ISSUE, "Android 11", "MediaTek"):
with self.subTest(needle=needle):
self.assertIn(needle, self.section)
def test_keeps_both_rows_of_the_bisect(self):
self.assertIn("with `InputKeyService` and `HomeKeyService` bound moves it one step", self.section)
self.assertIn("`InputKeyService` alone is still one step", self.section)
def test_names_the_missing_control_instead_of_borrowing_it(self):
self.assertIn("empty-list control was not run", self.section)
self.assertIn("still measured on the G09 alone", self.section,
"the control was not repeated on this set, so it must not be reported as if it was")
def test_names_the_capability_and_how_it_was_read(self):
for needle in ("capabilities=8", "CAPABILITY_CAN_FILTER_KEY_EVENTS", "dumpsys accessibility"):
with self.subTest(needle=needle):
self.assertIn(needle, self.section)
def test_the_two_build_strings_are_kept_apart(self):
self.assertIn(BUILD_IN_NOTE, self.section,
"the MOSR4 note's own build has to be named, or the two units read as one")
self.assertIn("separate units", self.section)
self.assertIn(BUILD_IN_NOTE, flat(MOSR4), "the note this section defers to no longer carries that build")
def test_the_receiver_claim_matches_the_mosr4_note(self):
self.assertIn(RECEIVER, self.section)
note = flat(MOSR4)
self.assertIn(RECEIVER, note, "the receiver is the MOSR4 note's finding, not something to invent here")
for button in ("Input", "Settings", "YouTube", "Netflix"):
with self.subTest(button=button):
self.assertIn(button, self.section)
def test_no_fix_is_promised_and_the_suggestion_is_left_to_the_launcher(self):
self.assertIn("adjustSuggestedStreamVolume", self.section)
self.assertIn("is not in this note", self.section)
self.assertNotIn("solves", self.section.lower(),
"the suggestion is untested here; it does not belong in a note as an answer")
if JAVA.is_dir():
sources = "\n".join(p.read_text(encoding="utf-8") for p in JAVA.rglob("*.java"))
self.assertNotIn("adjustSuggestedStreamVolume", sources,
"the launcher now synthesises the repeat: the note's wording has to change with it")
self.assertIn("does the same is untested", flat(G09),
"the G09 Limits sentence this section defers to was reworded away")
class EntryPointsTest(unittest.TestCase):
def test_the_mosr4_note_points_back(self):
text = flat(MOSR4)
self.assertIn("#944", text)
self.assertIn("capabilities=8", text)
self.assertIn(str(G09.resolve()), links(MOSR4))
self.assertIn("unhooks the buttons", text, "the reason the control was skipped belongs there too")
def test_troubleshooting_mentions_the_second_set(self):
text = flat(TROUBLE)
heading = "## A key helper is bound and a held remote button stopped repeating"
self.assertEqual(text.count(heading), 1)
body = text[text.index(heading):]
body = body[:body.find(" ## ")] if " ## " in body else body
self.assertIn("MiTV-MOSR4", body)
self.assertIn(BUILD_IN_ISSUE, body)
self.assertIn("#944", body)
self.assertIn("empty list was not tried", body)
def test_the_index_row_warns_before_the_helpers_are_installed(self):
rows = [line for line in INDEX.read_text(encoding="utf-8").split("\n")
if line.startswith("| `xiaomi-mitv-mosr4/`")]
self.assertEqual(len(rows), 1)
self.assertIn("cost the volume's live repeat", rows[0])
self.assertIn(str(MOSR4.resolve()), links(INDEX))
def test_links_resolve(self):
for page in (G09, MOSR4):
for target in links(page):
with self.subTest(page=page.name, target=target):
self.assertTrue(Path(target).exists(), "relative link target does not exist")
class SectionReaderTest(unittest.TestCase):
def test_missing_heading_is_none(self):
self.assertIsNone(section_of("# A page\n\nSome text.\n", HEADING))
def test_stops_at_the_next_heading(self):
text = f"# Page\n\n{HEADING}\n\nBody.\n\n## Choices\n\nLater.\n"
self.assertIn("Body.", section_of(text, HEADING))
self.assertNotIn("Later.", section_of(text, HEADING))
def test_a_deeper_heading_does_not_end_it(self):
text = f"# Page\n\n{HEADING}\n\nBody.\n\n### Detail\n\nStill here.\n\n## Choices\n\nGone.\n"
body = section_of(text, HEADING)
self.assertIn("Still here.", body)
self.assertNotIn("Gone.", body)
def test_last_section_runs_to_the_end(self):
text = f"# Page\n\n{HEADING}\n\nBody.\n"
self.assertIn("Body.", section_of(text, HEADING))
if __name__ == "__main__":
unittest.main()