TouchLock — Taming Ghost Touches on a Cheap Android Car Head Unit
Background
My RAV4 has an aftermarket Android head unit (a YT9216BJ board with a MediaTek MT8227L), and its touchscreen has ghost touches: it registers taps nobody made. While driving, that means apps open by themselves, the volume changes, or the radio starts over the music. Replacing the screen is one fix. I wanted to see whether software could make the problem harmless.
The idea is simple: when nobody is using the screen, ignore it. Real use needs a gesture that a phantom tap can’t produce by accident.
What I built
TouchLock is a small Android app with no dependencies. It runs as a foreground service and puts a transparent overlay over the whole screen.
- Locked by default. A full-screen overlay swallows every touch. To unlock, you hold exactly three fingers in three calibrated zones for a set time. Ghost touches are single, short taps, so they never manage it.
- Relocks by itself. It relocks after an idle timeout, after a hard cap on unlocked time, when the screen turns off or on, or when you tap the small lock icon.
- Steering-wheel toggle. Holding the mute button on the wheel toggles the lock, so there’s no need to reach for the screen.
- Visible ghost touches. Every touch on the locked screen sets off a small spark burst. It started as decoration and turned out to be a useful live map of where the phantom taps land.
- Can’t lock you out. The app refuses to lock until the zones are calibrated and the overlay permission is granted. Two exits always work: the notification’s Stop action and
adb force-stop. - Matches the launcher. It later gained a preset for my own Omarchy-style launcher. The unlock zones become three of the launcher’s panels, and the overlay stays fully invisible. The only sign it’s locked is a lock glyph next to the clock turning blue.
Problems we had to solve
The unit never really reboots. Turning the ignition off doesn’t power down. It’s a “quickboot” sleep that force-stops every app, and on wake-up Android’s BOOT_COMPLETED is never sent. A disassembly of the vendor’s system_server showed that the ROM relaunches apps from its own hidden “boot-start” settings slots. TouchLock registers itself in one of those, and a normal boot receiver still covers a true cold boot.
The radio took over the music. The same disassembly explained a separate annoyance. The ROM remembers the last app opened from a hardcoded whitelist. FM radio is on it and Bluetooth music isn’t. So opening FM once, even through a ghost touch, meant FM relaunched over Bluetooth audio after every ignition-on. Disabling FM outright worked but took Radio away. The lasting fix was replacing the vendor radio with my own CarRadio app, which never starts itself.
Steering-wheel buttons don’t send key events. An AccessibilityService key filter got nothing, because this board’s wheel buttons never produce real KeyEvents. Decoding the vendor’s raw CAN broadcast worked, but it depended on a board-specific byte code found only by watching logcat. The approach that stayed listens for Android’s mute-changed broadcast instead. That broadcast is hidden from the public SDK, so it’s referenced by its string value, but it needs no per-board setup.
Overlay windows are easy to get wrong. A small non-fullscreen window is touch-modal by default, so the unlocked-state lock button at first swallowed touches across the whole screen. It needs FLAG_NOT_TOUCH_MODAL. The locked overlay also has to cover the screen at (0,0) with no insets, so that its coordinates match the raw screen pixels used during calibration.
Multi-touch edge cases. ACTION_POINTER_UP still reports the finger count from before the finger lifted. The unlock logic treats any finger lifting as a cancelled hold instead of trusting that count.
The Android version on the box is wrong. The unit’s settings say Android 10. It actually runs Android 9 (API 27), so everything targets that.
Outcome
- The head unit is usable while driving: phantom taps hit the overlay and do nothing.
- It starts on ignition-on without opening any screen, and it can’t lock you out of your own car.
- The hardware quirks above were checked on the real unit over
adb, with screenshots and logcat, not assumed from documentation.
Stack rationale
Plain Java, no AndroidX, minSdk 26. The app is a handful of views and one service. Skipping libraries keeps the APK small and avoids compatibility surprises on an old vendor ROM.
Programmatic UI. The settings screen is built in code, with no layout XML, which made it easy to restyle later to match the launcher.
adb as the main tool. Most of the work was investigation, not app code: dumpsys, logcat -b events, and dexdump of the vendor framework to find out what the ROM actually does.
Built with Claude Code. It did the disassembly digging, the on-device verification loops, and most of the code, while I tested in the car.
Personal project for my own car. The head unit’s vendor software belongs to its respective owner.