Turn your TV remote's D-pad into a free-moving cursor.
An Android TV accessibility service that replaces focus-based navigation with a real cursor you steer using the remote's arrow keys. Built for the apps that were never designed for a TV — web views, phone apps sideloaded onto a TV box, settings screens buried behind controls the D-pad simply can't reach.
TV interfaces move focus between elements. That works until an app has no focus order — a web page, a phone-first app, a video player with a scrub bar. Then you're stuck.
A cursor solves that, but the obvious implementation doesn't work: injecting touch gestures does nothing on most TV apps. They're built around focus navigation and ignore touch events entirely. On a TCL launcher, a synthetic tap just resets focus to the first tile — the app never opens.
Remouse clicks through the accessibility node tree instead. It finds the node under the cursor and calls ACTION_CLICK on it, falling back to gesture injection only when no clickable node is there. That one difference is what makes it actually work.
- Free cursor — arrow keys move it in 8 directions, starting slow and accelerating smoothly the longer you hold. A quick tap nudges it ~10px, so small buttons stay reachable.
- Node-based clicking —
ACTION_CLICKon the accessibility node, not injected touch. Works where gesture injection silently fails. - Snap to nearby targets — the cursor doesn't have to sit exactly on a button. Anything within 5% of the screen width counts, with the smallest enclosing node winning when several overlap.
- Hover highlight — a neutral outline marks the element that will be clicked, so you confirm before pressing OK rather than guessing.
- Scroll mode — long-press OK to switch; arrow keys then page through the list under the cursor.
- Edge paging — hold the cursor against a screen edge for 300 ms and the container underneath pages in that direction.
- Pause without disabling — long-press BACK to hand every key back to the system for apps where the cursor gets in the way. Long-press again to return.
- Idle fade — the cursor disappears 3 s after the last key and returns instantly on the next one.
The pointer itself is deliberately quiet: a dark translucent fill with a white stroke, no glow and no animation. The two contrasting layers keep it visible over both bright and dark content.
| Key | Cursor mode | Scroll mode |
|---|---|---|
| Arrow keys | Move cursor (tap = nudge, hold = accelerate) | Page the list under the cursor |
| OK | Click the highlighted element | Click |
| Long-press OK | Switch to scroll mode | Switch back to cursor mode |
| Long-press BACK | Pause / resume Remouse | Pause / resume |
| BACK (short) | Normal back | Normal back |
| Volume +/− | Cursor speed | Scroll speed |
| Cursor against a screen edge | Page in that direction | — |
Build the APK (see below) or grab one from Releases, install it, then enable the accessibility service:
Settings → Accessibility → Remouse → On
Chinese-brand TVs block most of that from the UI. If installing fails or the accessibility toggle refuses to stick, see docs/install-on-tcl.md — it covers getting an ADB connection on TCL / FFALCON sets and the three vendor restrictions you have to clear first.
git clone https://github.com/zy0816/remouse.git
cd remouse
./gradlew assembleReleaseThe APK lands in app/build/outputs/apk/release/.
Signing: if a platform.keystore exists in the project root it's used for release builds (the author signs with an AOSP platform key so the app can be updated in place on a specific TV); without it, Gradle falls back to the default debug signing config and the build still works. No key material is in this repository.
Requirements: Android 7.0 (API 24) or newer, compiled against API 33.
The whole thing is one AccessibilityService:
flagRequestFilterKeyEvents+canRequestFilterKeyEventsletonKeyEventintercept the remote's keys before the foreground app sees them. Both are required — the runtime flag alone silently does nothing.- The cursor and the highlight box are two
TYPE_ACCESSIBILITY_OVERLAYwindows, so no overlay permission is needed. - Hit testing walks the window list from the top layer down, prunes any subtree whose bounds miss a small search box around the cursor, and picks the smallest node containing the point — or the nearest one within the snap radius.
- All node work (
getWindows(), tree traversal,performAction) runs on a dedicatedHandlerThread. These are cross-process calls; on main they stall the frame the cursor is being drawn in.
MIT — see LICENSE.