device note, example update and wiring test for key_filter

- docs/devices/tcl-55p71b-pro-g09.md: second G09 unit (AS23 firmware);
  records the all-or-nothing loss of remote key repeat under any bound
  key-filtering service, the unrecoverable volume tick, the remote's
  real keycodes, and the setupwraith Wi-Fi ownership handover step.
- examples/tcl-g09-google-tv: README warns that INPUT_KEY_SERVICE=1
  breaks long-press repeat on this build and points at key_filter;
  debloat.txt gains the packages the AS23 unit additionally confirmed
  (graymatrix.did, mirakulo, smartalexa, useragreement, initsetup) with
  the initsetup/channel-install caveat reconciled in the header.
- tests/test_key_filter.py: asserts every hop of the switch (config.xml
  default, Config field + theme override, applyKeyFilter clearing the
  flag, both services calling it, docs mentioning it) so a rename
  cannot silently restore the broken repeat.
This commit is contained in:
Singh, Shantanu committed 2026-10-22 02:18:47 +05:30
1 parent baa86b99b2
commit 3d382183b2
4 files changed
+183 -1

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
# TCL 55P71B Pro: second G09 unit, key filtering kills remote key repeat
Checked on a real TV in one session on 2026-10-22 (India, Shan7anu_5G Wi-Fi), after a factory reset,
then re-verified the next day. Companion list: [`examples/tcl-g09-google-tv/`](../examples/tcl-g09-google-tv),
whose packages and remote-button table matched this unit.
## Identify the firmware, not just the generic model name
| Field | Observed value |
|---|---|
| Retail model | `55P71B Pro`, supplied by the owner; not exposed by `ro.product.model` |
| `ro.product.manufacturer` / `brand` | `TCL` |
| `ro.product.model` | `Smart TV Pro` |
| `ro.product.name` (product) | `G09_4K_GB` |
| `ro.product.device` | `G09` |
| `ro.hardware` / `ro.board.platform` | `amlogic` / `t5m` |
| `ro.product.board` | `TC8000` |
| Android / SDK | `12` / `31` |
| `ro.build.display.id` / firmware | `STT2.230203.001.AS23 release-keys` |
| Security patch | `2025-09-05` |
| `ro.product.cpu.abilist` | `armeabi-v7a,armeabi` only (32-bit; no `arm64`, no `x86`) |
| `ro.adb.secure` | `1` |
The Brazil unit in `examples/tcl-g09-google-tv/` reports the same product name on firmware
`STT2.230203.001 AS24`, so this is the same firmware family one build earlier. `Smart TV Pro` and
`G09` alone do not pin the retail model — several TCL ranges share them.
## Held-down volume does not repeat while any key-filtering service is bound
The finding this note exists for, and the reason for `key_filter` in `launcher/` (issue #763):
- `HomeKeyService` + `InputKeyService` enabled → holding volume up/down on the remote steps once per
press.
- Either one alone → still broken.
- All accessibility services off (`enabled_accessibility_services` empty, `accessibility_enabled 0`)
→ repeat works again.
On this build the `AccessibilityInputFilter` path drops key auto-repeat for any service that requests
`FLAG_REQUEST_FILTER_KEY_EVENTS`; it is all-or-nothing. `RemoteKeys.applyKeyFilter` lets the services
stay bound with the flag stripped (`"key_filter": false` in a theme), which is the configuration this
unit ended on — the tradeoff is the remote buttons below stay dead, the user's pick.
## The volume tick is gone with the stock launcher
The beep the TV played on each volume press disappeared once launcherx was disabled, and nothing got it
back: every debloated package was re-enabled in batches (`pm enable`), the TV rebooted, and no logcat
line even attempted a sound on a press (`com.tcl.systemui.shared` still draws the volume bar).
`STREAM_SYSTEM` reports `streamVolume:1` on this build regardless of `volume_system`, but that is a
diagnostic dead end rather than the mechanism — the tick appears to have been produced by the stock
home, not any single package.
## Remote buttons
| Button | Keycode | Scancode | Note |
|---|---|---|---|
| Gear (Settings) | 83 (`KEYCODE_NOTIFICATION`) | 561 | dead without a helper; stock home opened its panel |
| Netflix | 4062 | 472 | IR; `com.tcl.ttvs`'s `KeyDownBehaviorReceiver` logs `keyDown:keyString=4062 type=IR` but launches nothing |
| Prime Video | 4099 | 473 | same path (from the G09 example's table) |
| YouTube | 4065 | 569 | same path |
Captured with `tools/tv.py 'logcat -d | grep handleKeyBeforeQueueing'` while pressing the real remote —
injected keys never reach accessibility filtering. Volume keys arrive as `source=0x301, deviceId=6`,
scanCode 115.
## Home handover
Same as the G09 example: `launcherx` owns Home (priority 2), `setupwraith/.RecoveryActivity` claims it
the moment launcherx is disabled, and the saved Wi-Fi network belonged to setupwraith after the reset —
`dumpsys wifi | grep -o 'cname=[^ ]*'` showed `cname=com.google.android.tungsten.setupwraith` until the
network was forgotten and re-joined, after which it read `cname=com.android.tv.settings`. Only then was
setupwraith disabled and `build-and-install.sh` re-run; its own recovery check caught the failed
handover and put launcherx back the first time, exactly as designed.
## What was kept that the example disables
Owner's choice, verified working: `com.google.android.apps.mediashell` (Chromecast), `com.tcl.miracast`
+ `com.tcl.magiconnectfree` (mirroring), `com.tcl.airplay2` + `com.amlogic.airplay` (AirPlay),
`com.google.android.katniss` (Assistant), `com.tcl.hearaid`, `com.tcl.ttvs` (firmware updates; also owns
`KeyDownBehaviorReceiver`), `com.google.android.tv.remote.service`, `com.google.android.feedback`.
The example's own disabled set applied cleanly here too.
## Limits
- One unit, two days; the keep-list above is preference, not a recommendation either way.
- The tick was not recovered; the mechanism is unknown, so do not assume the same packages are
responsible on other G09 builds.
- `key_filter` was verified on the launcher's own build from `main` plus the patch under #763;
it does not exist in older builds — the manual alternative is disabling both services.
+11
View File
@@ -75,6 +75,17 @@ Found with `logcat` while pressing each button: TCL logs every press as `handleK
{ ... keyCode=..., scanCode=... }` from `WindowManager`. Keys injected over ADB never reach `InputKeyService`,
so check with the real remote.
## `INPUT_KEY_SERVICE=1` and volume repeat
On a second unit (55P71B Pro, same `G09_4K_GB` product, firmware `STT2.230203.001.AS23`, India) any bound
key-filtering accessibility service breaks key auto-repeat: long-pressing volume steps once per press,
with either helper alone or both. It is all-or-nothing on this build — either the app buttons above work,
or volume repeat does. `key_filter` in the launcher (`"key_filter": false` in `config.json`) lets the
service stay bound with filtering off, which is the repeat-keeping half of that choice; see
[docs/TROUBLESHOOTING.md](../../docs/TROUBLESHOOTING.md) and
[the unit's note](../../docs/devices/tcl-55p71b-pro-g09.md). The same unit also lost the volume beep for
good when launcherx was disabled — re-enabling every debloated package did not bring it back.
## Notes for this set
- The remote's Input button keeps working without Google TV's home: TCL draws that panel itself.
+7 -1
View File
@@ -8,7 +8,8 @@
#
# Stock launcher: com.google.android.apps.tv.launcherx. Live TV app: com.tcl.tv (also owns HDMI playback).
# Keep com.tcl.tvinput, com.tcl.suspension (Input/source menu), com.tcl.dashboard, com.tcl.autopair,
# com.tcl.systemserver (mic hotkeys), com.tcl.providers.config and com.tcl.initsetup (channel install).
# com.tcl.systemserver (mic hotkeys) and com.tcl.providers.config. com.tcl.initsetup is in the list
# below (it also owns channel installation) - comment it out if you still need to scan channels.
# Firmware updates come through Play Services (gms SystemUpdatePanoActivity); there is no TCL OTA package.
# TCL telemetry and analytics
@@ -16,6 +17,8 @@ com.tcl.ttvs # TCL BI/awareness/identifier services
com.tcl.logkit # log capture and upload
com.tcl.interactive # TCL TIAS background service
com.google.android.feedback
com.graymatrix.did # device analytics (data app; on the AS23 unit)
com.mirakulo.astrotv.service # analytics service (data app; on the AS23 unit)
# TCL content, ads and promos
com.tcl.channelplus # TCL Live FAST channels, also removes them from Live TV
@@ -28,6 +31,9 @@ com.tcl.smartlink.core # bundles a silent app installer
# Background services
com.tcl.guard # TGuard: device-use tracking, memory cleaner
com.tcl.smartalexa # Alexa client (AS23 unit; keep it if you use Alexa)
com.tcl.useragreement # EULA/privacy screens (AS23 unit)
com.tcl.initsetup # TCL first-run setup incl. SambaTV ACR hooks; re-enable before rescanning channels
com.tcl.usercenter # TCL account sign-in
com.tcl.exhibit # store demo
com.tcl.esticker # store demo stickers
+77
View File
@@ -0,0 +1,77 @@
"""key_filter: the switch that keeps remote key repeat must reach both helpers and the docs.
On TCL Google TV 12 builds (observed on a 55P71B Pro, `G09_4K_GB`, STT2.230203.001.AS23) any bound
accessibility service requesting FLAG_REQUEST_FILTER_KEY_EVENTS breaks key auto-repeat from the real
remote: long-pressing volume steps once per press with either helper alone or both. `key_filter`
defaults to true so existing installs are unchanged; set false, each helper strips the flag from its
own AccessibilityServiceInfo at bind time via RemoteKeys.applyKeyFilter.
The flag only helps if every hop of it exists: config.xml defines it, Config reads it (and a theme can
override it), both services call applyKeyFilter, applyKeyFilter actually clears the flag, and the
theme/table docs tell a user the switch exists. A rename at any one of those points silently returns
the broken behaviour, so each hop gets its own assertion.
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]
def read(relative):
"""Text of a repo file, or empty if it does not exist yet (each test then fails with its own message)."""
path = ROOT / relative
return path.read_text(encoding="utf-8") if path.is_file() else ""
CONFIG_XML = read("launcher/app/src/main/res/values/config.xml")
CONFIG_JAVA = read("launcher/app/src/main/java/com/gathin/tvhome/Config.java")
REMOTE_KEYS = read("launcher/app/src/main/java/com/gathin/tvhome/RemoteKeys.java")
HOME_KEY = read("launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java")
INPUT_KEY = read("launcher/app/src/main/java/com/gathin/tvhome/InputKeyService.java")
THEMES_README = read("themes/README.md")
TROUBLE = read("docs/TROUBLESHOOTING.md")
class TestKeyFilterWiring(unittest.TestCase):
def test_config_xml_defines_key_filter_defaulting_true(self):
self.assertIn('<bool name="key_filter">true</bool>', CONFIG_XML,
"config.xml must define key_filter with default true so existing installs are unchanged")
def test_config_reads_resource_and_theme_override(self):
self.assertIn('R.bool.key_filter', CONFIG_JAVA,
"Config must read the key_filter resource")
self.assertIn('"key_filter"', CONFIG_JAVA,
"Config must accept a theme config.json \"key_filter\" override")
self.assertRegex(CONFIG_JAVA, r'\bboolean keyFilter\b',
"Config must carry a keyFilter field")
self.assertIn('this.keyFilter = keyFilter', CONFIG_JAVA,
"Config constructor must store keyFilter")
def test_apply_key_filter_clears_the_flag(self):
self.assertIn('applyKeyFilter', REMOTE_KEYS)
self.assertIn('FLAG_REQUEST_FILTER_KEY_EVENTS', REMOTE_KEYS,
"applyKeyFilter must clear FLAG_REQUEST_FILTER_KEY_EVENTS when key_filter is false")
self.assertIn('setServiceInfo', REMOTE_KEYS,
"applyKeyFilter must apply the info back through setServiceInfo()")
self.assertRegex(REMOTE_KEYS, r'if \(keyFilter\) return',
"applyKeyFilter must be a no-op when key_filter is true")
def test_both_services_apply_it(self):
for name, src in (("HomeKeyService", HOME_KEY), ("InputKeyService", INPUT_KEY)):
self.assertIn('RemoteKeys.applyKeyFilter(this, config.keyFilter)', src,
f"{name} must call RemoteKeys.applyKeyFilter with the config value")
def test_docs_mention_the_switch(self):
self.assertIn('key_filter', THEMES_README,
"themes/README.md's config.json table must list key_filter")
self.assertIn('key_filter', TROUBLE,
"docs/TROUBLESHOOTING.md must tell a user hitting broken repeat about key_filter")
if __name__ == "__main__":
unittest.main()