docs: what to do when neither keycode route reports a dead button (#853)

The page and config.xml send a reader two ways to find a dead remote button: the
InputReader:D logcat filter while the button is pressed, and the getevent Linux
scancode into settings_key_scancodes. On a Google TV 14 PEAQ "AI PONT DE"
(#853) both came back empty for the same button, so it read as never delivered:
InputReader logged no key line at all, and the MSC_SCAN getevent printed
(0x000c009f) is an evdev code, not the scanCode the framework reports, so both
spellings of it (159, 786591) matched nothing. A temporary Log.d at the top of the
helper's onKeyEvent named the delivered values (keyCode=83 scanCode=240) and
settings_key_keycodes = [83] then opened the TV's Settings.

So the note gains the route that works, the reason the other two can fail, and
the fact that a getevent code matching nothing is not proof the press never
arrived. No separate device note: the finding is about finding the number, not
about the set, and it belongs where a reader looks for a keycode. The same two
caveats go into the settings_key_scancodes and settings_key_keycodes comments a
customiser reads before the page. PROMPT.md carries the same half-truth ("read
the scancode with getevent -lt") and is left for the maintainer.
This commit is contained in:
xbsheng committed 2026-10-22 13:56:52 +08:00
1 parent c2e4600601
commit 355fb43c65
3 files changed
+175 -2

No files matched your search

+15
View File
@@ -447,6 +447,21 @@ tools/tv.py 'dumpsys accessibility'
`tools/tv.py 'getevent -lt'` in `settings_key_scancodes` and install with `HOME_KEY_SERVICE=1`
(an RC-36 remote sends its gear on scancode 240 and its notifications button on 362 that way; see
[the FPD CG50-C3 note](devices/fpd-cg50-c3.md)).
Not every set answers those two routes, though ([#853](https://github.com/Gathin23/cleanyourtv/issues/853),
a PEAQ "AI PONT DE", `DVB99PQ` / Changhong `changi`, Google TV 14, `UKNV.260504.001`): a HID
consumer-control usage prints an evdev `MSC_SCAN` that is not necessarily the `KeyEvent.getScanCode()`
the framework reports, so a value read with `getevent -lt` can match nothing in
`settings_key_scancodes` (that set's gear printed `MSC_SCAN 000c009f`, and neither 159 nor 786591, the
two spellings of it, did anything), and `logcat -v time -s InputReader:D`, the filter `config.xml`
suggests, printed no key line at all there, for any button, only `Reconfiguring input devices`. What
Android really delivers comes from a temporary `Log.d` at the top of the helper's `onKeyEvent` printing
`getKeyCode()`, `getScanCode()` and `getDeviceId()`, rebuilt with `HOME_KEY_SERVICE=1` and read with
`tools/tv.py "logcat -s TvHomeKeys:D"` while pressing the real button: that set reported
`action=0 keyCode=83 scanCode=240 deviceId=9`, and `settings_key_keycodes = [83]` opened the TV's
Settings at once. So a `getevent` code that matches nothing is not proof the press never arrived, and
consuming the key in the helper also keeps it from the vendor receiver
`com.homwee.factory/.FactoryKeyReciver`, which `dumpsys window policy` routes `KEYCODE_NOTIFICATION`
to once the stock launcher is gone.
- **All vendor buttons die at once** and `logcat` says `GLOBAL_BUTTON ... process is bad`: the
global-key receiver crashes on the disabled launcherx and Android stops starting it. Map every such
keycode in the helpers, also for disabled apps. See [the MOSR4 note](devices/xiaomi-mitv-mosr4.md).
+7 -2
View File
@@ -112,13 +112,18 @@
<!-- Remote buttons that open Settings, by Linux scancode (HOME_KEY_SERVICE=1 build-and-install.sh). Remotes
that send KEYCODE_SETTINGS need no entry. Some send a code Android does not map: the Xiaomi/Fengmi
projector remote's gear button is scancode 384 (KEY_TAPE). Find yours with
`tools/tv.py 'getevent -lt'` while pressing the button. Leave empty to omit. -->
`tools/tv.py 'getevent -lt'` while pressing the button - but a HID consumer-control usage prints an
evdev MSC_SCAN there, which is not always the scanCode the framework reports, so a number that takes
nothing is no proof the press never arrived. Leave empty to omit. -->
<integer-array name="settings_key_scancodes">
</integer-array>
<!-- The same by Android keycode, for buttons whose scancode is shared. TCL Google TV remotes send their
Settings button as keycode 83 (KEYCODE_NOTIFICATION, which the stock Google TV home turned into its quick
settings panel) on scancode 240 (KEY_UNKNOWN), the scancode several TCL buttons share. Find the keycode
with `tools/tv.py 'logcat -v time -s InputReader:D'` while pressing the button. Leave empty to omit. -->
with `tools/tv.py 'logcat -v time -s InputReader:D'` while pressing the button - some firmwares print no
key line there at any verbosity, and a `getevent` MSC_SCAN is a different number, so the reliable route is
a temporary `Log.d` in the helper's `onKeyEvent`: `getKeyCode()`, `getScanCode()`, `getDeviceId()`, read
after a HOME_KEY_SERVICE=1 rebuild. Leave empty to omit. -->
<integer-array name="settings_key_keycodes">
</integer-array>
<!-- What those Settings buttons open: false, the TV's Settings; true, a small glass panel at the top right
+153
View File
@@ -0,0 +1,153 @@
"""Finding a dead remote button's keycode when the documented routes come back empty (#853).
The page and `config.xml` used to send a reader two ways: `logcat -v time -s InputReader:D` while
pressing the button, and, for a press that arrives as `KEY_UNKNOWN`, the Linux scancode from
`getevent -lt` into `settings_key_scancodes`. On a Google TV 14 PEAQ "AI PONT DE" both failed for the
same button - `InputReader` logged no key line at all, and the `MSC_SCAN` `getevent` printed
(`0x000c009f`) is not the scanCode the framework reports - so the button read as "never delivered".
A temporary `Log.d` at the top of the helper's `onKeyEvent` is what named the delivered values
(`keyCode=83 scanCode=240`), and the keycode then worked.
Documentation only, so these tests pin what a reader has to be able to act on: that the logcat route
can come back empty, that a `getevent` number is not the framework's scanCode, the recipe that does
work, the evidence that it does, and that the same two caveats reach the `config.xml` comments a
customiser reads before the page. The command names and the `Log.d` calls are asserted verbatim
because a misspelled one is a reader reporting "no output" again.
Run from the repository root: python3 -m unittest discover -s tests -v
"""
import re
from pathlib import Path
import unittest
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
TROUBLE = ROOT / "docs" / "TROUBLESHOOTING.md"
CONFIG_XML = ROOT / "launcher" / "app" / "src" / "main" / "res" / "values" / "config.xml"
START = "- **Which keycode** a dead button sends"
LINK = re.compile(r"\]\(([^)\s]+)\)")
def bullet(text, start):
"""The markdown bullet that begins with `start`, plus the continuation lines under it."""
lines = text.split("\n")
first = next(i for i, line in enumerate(lines) if line.startswith(start))
out = [lines[first]]
for line in lines[first + 1:]:
if line.startswith("- **") or line.startswith("## "):
break
out.append(line)
return "\n".join(out)
def comment_before(xml_text, array_name):
"""The `<!-- ... -->` immediately above `<integer-array name="array_name">`.
Searched in the text before the tag, not with one greedy pattern: several comments above it end
with `-->` too, and the launcher config opens with a file-header comment.
"""
marker = '<integer-array name="%s">' % array_name
if marker not in xml_text:
return ""
comments = re.findall(r"<!--(.*?)-->", xml_text.split(marker, 1)[0], re.S)
return comments[-1] if comments else ""
class KeycodeDiscoveryNoteTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.trouble_text = TROUBLE.read_text(encoding="utf-8")
cls.config_text = CONFIG_XML.read_text(encoding="utf-8")
cls.note = bullet(cls.trouble_text, START)
cls.keycodes_comment = comment_before(cls.config_text, "settings_key_keycodes")
cls.scancodes_comment = comment_before(cls.config_text, "settings_key_scancodes")
def test_bullet_exists_once(self):
self.assertEqual(self.trouble_text.count(START), 1,
"the note must live in one bullet so the two routes cannot drift apart")
def test_bullet_says_the_logcat_route_can_be_empty(self):
# The whole point of the report: following the page on this firmware printed nothing at all,
# which reads as "the button is dead" instead of "this filter sees nothing here".
self.assertIn("InputReader:D", self.note)
self.assertIn("no key line", self.note)
def test_bullet_separates_getevent_from_the_framework_scancode(self):
self.assertIn("MSC_SCAN", self.note)
self.assertIn("evdev", self.note)
self.assertIn("getScanCode()", self.note)
self.assertIn("settings_key_scancodes", self.note)
def test_bullet_gives_the_logd_recipe_verbatim(self):
self.assertIn("Log.d", self.note)
self.assertIn("onKeyEvent", self.note)
for call in ("getKeyCode()", "getScanCode()", "getDeviceId()"):
self.assertIn(call, self.note)
self.assertIn("TvHomeKeys", self.note)
self.assertIn("logcat -s TvHomeKeys:D", self.note)
self.assertIn("HOME_KEY_SERVICE=1", self.note)
def test_bullet_keeps_the_evidence(self):
# A reader needs the worked example to recognise their own dump, and the two spellings that
# were tried and did nothing, so the getevent route is not retried as if it were untested.
self.assertIn("MSC_SCAN 000c009f", self.note)
self.assertIn("159", self.note)
self.assertIn("786591", self.note)
self.assertIn("keyCode=83 scanCode=240", self.note)
self.assertIn("settings_key_keycodes = [83]", self.note)
def test_bullet_names_the_device_and_build(self):
self.assertIn("DVB99PQ", self.note)
self.assertIn("changi", self.note)
self.assertIn("UKNV.260504.001", self.note)
def test_bullet_says_a_mismatch_is_not_proof(self):
# Without this sentence the old advice still reads as authoritative: no match, dead button.
self.assertIn("not proof the press never arrived", self.note)
def test_bullet_keeps_the_vendor_receiver_fact(self):
self.assertIn("com.homwee.factory/.FactoryKeyReciver", self.note)
self.assertIn("dumpsys window policy", self.note)
def test_bullet_links_the_issue(self):
self.assertIn("[#853](https://github.com/Gathin23/cleanyourtv/issues/853)", self.note)
def test_config_xml_keycodes_comment_points_at_the_logd_route(self):
self.assertIn("InputReader:D", self.keycodes_comment)
self.assertIn("Log.d", self.keycodes_comment)
self.assertIn("onKeyEvent", self.keycodes_comment)
def test_config_xml_scancodes_comment_warns_about_msc_scan(self):
self.assertIn("MSC_SCAN", self.scancodes_comment)
self.assertIn("scanCode", self.scancodes_comment)
def test_comment_before_finds_the_comment_it_was_asked_for(self):
# The two comments are adjacent and nearly identical in shape; a helper that returned the
# first one it saw would make both comment tests pass on the wrong text.
self.assertIn("keycode 83", self.keycodes_comment)
self.assertNotIn("KEY_TAPE", self.keycodes_comment)
self.assertIn("KEY_TAPE", self.scancodes_comment)
self.assertNotIn("keycode 83", self.scancodes_comment)
self.assertEqual(comment_before(self.config_text, "no_such_array"), "")
def test_bullet_extraction_stops_at_the_next_bullet(self):
sample = "\n".join([START + " one", " two", "- **Next** three"])
self.assertEqual(bullet(sample, START), START + " one\n two")
def test_bullet_links_resolve(self):
broken = [target for target in LINK.findall(self.note)
if not target.startswith(("http://", "https://", "#"))
and not (TROUBLE.parent / target.split("#", 1)[0]).exists()]
self.assertEqual(broken, [], "the bullet must not link a page that does not exist")
def test_config_xml_is_still_well_formed(self):
# The change is a comment, but a comment edit that breaks the XML breaks the launcher build.
root = ET.fromstring(self.config_text)
self.assertEqual(root.tag, "resources")
for name in ("settings_key_scancodes", "settings_key_keycodes"):
self.assertIsNotNone(root.find("./integer-array[@name='%s']" % name))
if __name__ == "__main__":
unittest.main()