Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,13 @@ handled go dead, the same trade without a `settings` write. Write back the exact
printed; the empty list also unbinds TalkBack. Bisect, keycodes and limits:
[the G09 note](devices/tcl-55p71b-g09-key-repeat.md).

A pause keeps both at the price of the first step. On a Xiaomi MOEU0 (Google TV 14) the policy steps once per press
while a helper filters, and no helper sees the volume keys, so `consume_volume_keys` cannot repeat them. The remote's
Netflix (289), Prime Video (290) and YouTube (296), Apps and the gear are global keys that never reach a window:
`mitv.service` starts Netflix and Prime Video by package, and its gear handler is empty. `"volume_pauses_filter":
true` (the same bool in `config.xml`) makes every volume change stop the filtering for 4 s. The first press steps
once, a press or hold within those 4 s repeats as usual, and the buttons work again when the pause ends.

Where launcherx is what answers the buttons, keeping it enabled and redirecting its home screen keeps both. On a
Sony BRAVIA VU3 (`BRAVIA_VU3_4K`, Google TV 14, `UKR1.240726.001`) the Netflix, YouTube, Prime Video and YouTube
Music buttons are global keys (`BUTTON_4`, `BUTTON_14`, `BUTTON_2`, `BUTTON_15`) routed to
Expand Down
1 change: 1 addition & 0 deletions docs/switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ rule sit on its wanted list until a screenshot arrives.
| `gamepad_passthrough` | <img src="switches/gamepad_passthrough.jpg" width="320" alt="gamepad_passthrough on a TV"> |
| `home_ignore_channel_keys` | <img src="switches/home_ignore_channel_keys.png" width="320" alt="home_ignore_channel_keys on a TV"> |
| `consume_volume_keys` | <img src="switches/consume_volume_keys.jpg" width="320" alt="consume_volume_keys on a TV"> |
| `volume_pauses_filter` | <img src="switches/volume_pauses_filter.jpg" width="320" alt="volume_pauses_filter on a TV"> |
| `wifi_status_warnings` | <img src="switches/wifi_status_warnings.jpg" width="320" alt="wifi_status_warnings on a TV"> |
| `top_bar_apps` | <img src="switches/top_bar_apps.jpg" width="320" alt="top_bar_apps on a TV"> |
| `show_greeting` | <img src="switches/show_greeting.jpg" width="320" alt="show_greeting on a TV"> |
Expand Down
Binary file added docs/switches/volume_pauses_filter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ on that firmware, grouped and commented. These are starting points: comment out
| `xiaomi-tv-a43-2025/` | Xiaomi TV A43 2025 (MiTV-MOOQ1) | Google TV 11 | Mi analytics, PatchWall stubs, Mi cast, preloaded streaming, Google TV launcher |
| `xiaomi-mitv-afmu0/` | Xiaomi TV Box S 3rd gen (MiTV-AFMU0, twilight, Amlogic), streaming box | Google TV 14 | Mi analytics, PatchWall/Mi Home tab, Mi connect/SmartShare; voice and phone remote off by choice; setup wizard off so launcherx can go; second unit: Privacy Sandbox ad services off, remote's YouTube/Prime Video/Apps buttons mapped back in config.json |
| `xiaomi-mitv-mteq0/` | Xiaomi TV EA Pro 55" (MiTV-MTEQ0, baymax, MediaTek), China firmware 23.6.26.4028 | Android TV 10 (CN build) | No Google apps; Mi analytics/push/cloud control, shop and pay, XiaoAI, casting and preloads off; HOME allowlist (the `com.ts.launcher` build works); standby force-stops third-party apps; ADB closes at boot; Wi-Fi country falls back to CN (5 GHz ch. 100-144 hidden, `service_region`); no CONFIRM_INSTALL so split-APK installs hang; Sogou is the only IME; [firmware notes](../docs/devices/xiaomi-mitv-mteq0-baymax.md) |
| `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-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 (296 on a 55" unit), Apps 284 and Xiaomi TV+ 192 noted; volume_pauses_filter gives the volume ramp with the buttons; config.json maps the gear and YouTube |
| `xiaomi-mitv-mffu0/` | Xiaomi TV S Pro 100 Mini LED 2025 (小米电视 S Pro 100, L100MB-SP, MiTV-MFFU0, finch, MediaTek), China firmware OS3.0.115.0.UFFMATV, 4 GB RAM | Android 14 (HyperOS 3, CN build) | First HyperOS 3 Xiaomi: Mi analytics, MiSight log upload, TV QS reporting, cloud control, video-daily promos with the Mi ad SDK, shopping, web promos, Pictorial, AI translate; alarms, health, game SDK off by choice; **keep TV Manager** (XiaoAI's "open an app" voice command runs through its provider) and Home only for a launcher built under an allowlisted package name (`-PlauncherApplicationId=com.thunderstone.idun.dev`, stock launcher disabled; the standard ID and `set-home-activity` do not work); ADB closes on every reboot; CEC set-top-box black-screen note; voice, casting, Mi Home, OTA, Hyper screensaver kept; debloat only |
| `xiaomi-mitv-ayfr0/` | Xiaomi TV Stick 4K (MiTV-AYFR0, soul, Amlogic), streaming stick | Android TV 14 | Mi analytics, PatchWall, auto-installs (`mibox3` + `soc.amlogic`), update service; `setupwraith` does not claim HOME (kept enabled, Wi-Fi stays intact); remote Home key and CEC One Touch Play verified |
| `xiaomi-mitv-afmu1/` | Xiaomi TV Stick 4K 2nd gen (MiTV-AFMU1, twilight, adastra, Amlogic), streaming stick | Google TV 14 | Two units on the same build (UKG3.250826.001), one list: Mi analytics, PatchWall/Mi Home tab, Amlogic auto-installs, preloaded apps off; voice, phone remote and Miracast kept on one unit and off on the other; setup wizard off (re-join Wi-Fi first) so launcherx can go; the second unit also took the Privacy Sandbox ad services and the firmware updater (com.xiaomi.mitv.updateservice DISABLED, so no firmware updates); provisional, one day each |
Expand Down
10 changes: 10 additions & 0 deletions examples/xiaomi-mitv-moeu0/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"live_tv_package": "com.mitv.livetv",
"settings_key_keycodes": [176],
"settings_key_panel": true,
"volume_pauses_filter": true,
"app_keys": [
"190:com.google.android.youtube.tv",
"296:com.google.android.youtube.tv"
]
}
6 changes: 6 additions & 0 deletions examples/xiaomi-mitv-moeu0/debloat.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
# Down on the real remote stepped once per press (as on the MiTV-MOSR4, #944). key_filter false in
# config.xml brought the ramp back with both services still bound, at the cost of the Settings (176),
# YouTube (190) and TV+ (192) buttons, which then do nothing; their tiles and the Settings pill remain.
# volume_pauses_filter true in config.xml keeps both: a volume change stops key filtering for 4 s, so
# the first press steps once, a hold within those 4 s ramps, and the buttons work again once the pause
# ends. Checked on a 55" MiTV-MOEU0 (Xiaomi TV S Mini LED 55 2025, firmware V816.0.25.12.1, then V816.0.26.10.12.1) whose remote
# sends different codes: YouTube 296 (KEYCODE_VIDEO_APP_8), Netflix 289 and Prime Video 290
# (KEYCODE_VIDEO_APP_1/2, global keys mitv.service starts Netflix and Prime from, free for app_keys
# when those apps are removed), Apps 284, gear 176. config.json in this folder maps both YouTube codes.
#
# Sideloading works, including apps that claim HOME, so the launcher half is possible.
# HOME: setupwraith/.RecoveryActivity claims Home next to launcherx, so it is disabled below. On this
Expand Down
2 changes: 1 addition & 1 deletion examples/xiaomi-mitv-moeu0/tv.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"tv": "Xiaomi TV S Mini LED 75 2025 (MiTV-MOEU0, ladybird, MediaTek)",
"os": "Google TV 14",
"brand": "Xiaomi",
"notes": "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"
"notes": "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 (296 on a 55\" unit), Apps 284 and Xiaomi TV+ 192 noted; volume_pauses_filter gives the volume ramp with the buttons; config.json maps the gear and YouTube"
}
9 changes: 7 additions & 2 deletions launcher/app/src/main/java/com/gathin/tvhome/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ static boolean settingsChanged(long seen, long current) {
final boolean gamepadPassthrough;
/** With keyFilter on, the key services swallow Volume Up/Down and repeat a held one themselves (consume_volume_keys). */
final boolean consumeVolumeKeys;
/** With keyFilter on, a volume change stops the key services filtering for a few seconds (volume_pauses_filter). */
final boolean volumePausesFilter;
/** Channel Up/Down do nothing while Home is in front, instead of opening the TV's Live TV app. */
final boolean homeIgnoreChannelKeys;
/** Stock launcher whose home screen is sent back here if the firmware revives it (config.xml stock_home_redirect). */
Expand Down Expand Up @@ -295,7 +297,7 @@ private Config(List<String> dockApps, boolean fillDock, List<String> hiddenApps,
List<RaceWeekend.Race> raceCalendar, boolean cinematicHome, boolean ambientGlow, boolean appGlow, boolean fadeRows, int homeIdleClockMinutes,
boolean keyFilter, boolean dockGlass, String glowStyle, boolean wallpaperCaptions,
boolean handleViewInputs, boolean tileMenu, boolean dockEvenPadding, boolean dockBand, boolean cinematicDots, boolean cinematicWhiteFocus, boolean wifiStatusColor, int cinematicScrim, int cinematicArt,
Map<String, Set<String>> wallpaperMoods, boolean gamepadPassthrough, boolean consumeVolumeKeys, boolean homeIgnoreChannelKeys, boolean powerOnHome, boolean homeLongPressHome, boolean wifiStatusWarnings, List<String> topBarApps, boolean showGreeting, boolean headerWhite, boolean clockWallpaper, int clockWallpaperDim, boolean clockGlow, String glassStyle, boolean singleRow, boolean dockBandAll, boolean lunarDate, int lunarUtcOffset, boolean dateBelowClock, boolean vpnPill, String locale, boolean powerOnHomeAlways, boolean inputsConnectedOnly, boolean inputsCecStateOnly,
Map<String, Set<String>> wallpaperMoods, boolean gamepadPassthrough, boolean consumeVolumeKeys, boolean volumePausesFilter, boolean homeIgnoreChannelKeys, boolean powerOnHome, boolean homeLongPressHome, boolean wifiStatusWarnings, List<String> topBarApps, boolean showGreeting, boolean headerWhite, boolean clockWallpaper, int clockWallpaperDim, boolean clockGlow, String glassStyle, boolean singleRow, boolean dockBandAll, boolean lunarDate, int lunarUtcOffset, boolean dateBelowClock, boolean vpnPill, String locale, boolean powerOnHomeAlways, boolean inputsConnectedOnly, boolean inputsCecStateOnly,
int openOnStartNetworkWait, boolean openOnStartOnWake, boolean iconOnlyVpn, String clockFormat, boolean backOpensClock, boolean glowSnap,
boolean settingsKeyClearTask) {
this.handleViewInputs = handleViewInputs;
Expand Down Expand Up @@ -395,6 +397,7 @@ private Config(List<String> dockApps, boolean fillDock, List<String> hiddenApps,
this.wallpaperMoods = wallpaperMoods;
this.gamepadPassthrough = gamepadPassthrough;
this.consumeVolumeKeys = consumeVolumeKeys;
this.volumePausesFilter = volumePausesFilter;
this.homeIgnoreChannelKeys = homeIgnoreChannelKeys;
this.powerOnHome = powerOnHome;
this.homeLongPressHome = homeLongPressHome;
Expand Down Expand Up @@ -516,6 +519,7 @@ static Config load(Context ctx) {
boolean keyFilter = res.getBoolean(R.bool.key_filter);
boolean gamepadPassthrough = res.getBoolean(R.bool.gamepad_passthrough);
boolean consumeVolumeKeys = res.getBoolean(R.bool.consume_volume_keys);
boolean volumePausesFilter = res.getBoolean(R.bool.volume_pauses_filter);
boolean homeIgnoreChannelKeys = res.getBoolean(R.bool.home_ignore_channel_keys);
boolean powerOnHome = res.getBoolean(R.bool.power_on_home);
boolean homeLongPressHome = res.getBoolean(R.bool.home_long_press_home);
Expand Down Expand Up @@ -632,6 +636,7 @@ static Config load(Context ctx) {
keyFilter = bool(json, "key_filter", keyFilter);
gamepadPassthrough = bool(json, "gamepad_passthrough", gamepadPassthrough);
consumeVolumeKeys = bool(json, "consume_volume_keys", consumeVolumeKeys);
volumePausesFilter = bool(json, "volume_pauses_filter", volumePausesFilter);
homeIgnoreChannelKeys = bool(json, "home_ignore_channel_keys", homeIgnoreChannelKeys);
powerOnHome = bool(json, "power_on_home", powerOnHome);
homeLongPressHome = bool(json, "home_long_press_home", homeLongPressHome);
Expand Down Expand Up @@ -723,7 +728,7 @@ static Config load(Context ctx) {
json == null ? Collections.emptyList() : raceCalendar(json), cinematic, ambientGlow, appGlow, fadeRows,
Math.max(0, idleClockMinutes), keyFilter, dockGlass, glowStyle, wallpaperCaptions, handleViewInputs, tileMenu, dockEvenPadding,
dockBand, cinematicDots, cinematicWhiteFocus, wifiStatusColor, cinematicScrim, cinematicArt, wallpaperMoods, gamepadPassthrough,
consumeVolumeKeys, homeIgnoreChannelKeys, powerOnHome, homeLongPressHome, wifiStatusWarnings, topBarApps, showGreeting, headerWhite, clockWallpaper,
consumeVolumeKeys, volumePausesFilter, homeIgnoreChannelKeys, powerOnHome, homeLongPressHome, wifiStatusWarnings, topBarApps, showGreeting, headerWhite, clockWallpaper,
Math.max(0, Math.min(90, clockWallpaperDim)), clockGlow, glassStyle, singleRow && dockRow, dockBandAll, lunar, lunarOffset,
dateBelowClock, vpnPill, locale, powerOnHomeAlways, inputsConnectedOnly, inputsCecStateOnly,
Math.max(0, Math.min(600, openOnStartNetworkWait)), openOnStartOnWake, iconOnlyVpn, clockFormat, backOpensClock, glowSnap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public class HomeKeyService extends AccessibilityService {
private BroadcastReceiver screenOnReceiver;
/** Stops key filtering while a game controller is connected; null unless gamepad_passthrough and key_filter, or before the unlock. */
private GamepadPassthrough gamepads;
/** Pauses key filtering after a volume change; null unless volume_pauses_filter and key_filter. */
private VolumePause volumePause;
/** Screen-off receiver while guard_off_in_standby is set; null otherwise. */
private BroadcastReceiver standbyExit;
/** disableSelf is how this service leaves for standby, not the user turning it off. */
Expand Down Expand Up @@ -150,6 +152,7 @@ public void onDestroy() {
standbyExit = null;
}
if (gamepads != null) gamepads.stop();
if (volumePause != null) volumePause.stop();
if (consumeVolumeKeys) VolumeKeys.stop();
super.onDestroy();
}
Expand Down Expand Up @@ -238,6 +241,12 @@ private void loadConfig() {
gamepads = new GamepadPassthrough(this);
gamepads.start();
}
// volume_pauses_filter, off by default. Only with key_filter on and gamepad_passthrough off: a pause's end turns
// filtering back on, also for a gamepad that gamepad_passthrough let through.
if (config.volumePausesFilter && config.keyFilter && !config.gamepadPassthrough && volumePause == null) {
volumePause = new VolumePause(this);
volumePause.start();
}
}

/** A tap's target: the "front:" entry while the button's own app is already in front, else the usual one. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class InputKeyService extends AccessibilityService {
private boolean consumeVolumeKeys;
/** Stops key filtering while a game controller is connected; null unless gamepad_passthrough and key_filter. */
private GamepadPassthrough gamepads;
/** Pauses key filtering after a volume change; null unless volume_pauses_filter and key_filter. */
private VolumePause volumePause;

@Override
protected void onServiceConnected() {
Expand All @@ -52,6 +54,12 @@ protected void onServiceConnected() {
gamepads = new GamepadPassthrough(this);
gamepads.start();
}
// volume_pauses_filter, off by default. Only with key_filter on and gamepad_passthrough off: a pause's end turns
// filtering back on, also for a gamepad that gamepad_passthrough let through.
if (config.volumePausesFilter && config.keyFilter && !config.gamepadPassthrough && volumePause == null) {
volumePause = new VolumePause(this);
volumePause.start();
}
}

private boolean isInputKey(int keyCode) {
Expand Down Expand Up @@ -90,6 +98,7 @@ public boolean onUnbind(Intent intent) {
public void onDestroy() {
RemoteKeys.release(this);
if (gamepads != null) gamepads.stop();
if (volumePause != null) volumePause.stop();
if (consumeVolumeKeys) VolumeKeys.stop();
super.onDestroy();
}
Expand Down
62 changes: 62 additions & 0 deletions launcher/app/src/main/java/com/gathin/tvhome/VolumePause.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.gathin.tvhome;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;

/**
* Stops HomeKeyService and InputKeyService filtering keys for PAUSE_MS after each volume change. On a Xiaomi MOEU0
* (Google TV 14) the window policy changes volume once per press while any service filters keys, and a held volume
* key never repeats; the helpers never see the volume keys, so consume_volume_keys cannot repeat them either. With
* this on, the first press steps once, a press or hold within PAUSE_MS repeats as usual, and the remote buttons the
* helpers handle work again once the pause ends. Off by default (volume_pauses_filter). Started only with key_filter
* on too: with it off the services never filter, and this must not turn filtering back on. Not with
* gamepad_passthrough either, for the same reason while a gamepad is connected.
*/
final class VolumePause extends BroadcastReceiver {
static final long PAUSE_MS = 4000;

private final AccessibilityService service;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable resume = () -> setFiltering(true);

VolumePause(AccessibilityService service) {
this.service = service;
}

void start() {
IntentFilter volume = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
// A system broadcast: not exported is enough, and Android 14 wants the flag stated.
if (Build.VERSION.SDK_INT >= 33) service.registerReceiver(this, volume, Context.RECEIVER_NOT_EXPORTED);
else service.registerReceiver(this, volume);
}

void stop() {
handler.removeCallbacks(resume);
service.unregisterReceiver(this);
}

@Override
public void onReceive(Context context, Intent intent) {
setFiltering(false);
handler.removeCallbacks(resume);
handler.postDelayed(resume, PAUSE_MS);
}

private void setFiltering(boolean filter) {
AccessibilityServiceInfo info = service.getServiceInfo();
if (info == null) return;
boolean filtering = (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0;
if (filter == filtering) return;
// Without the flag no more keys come, the UP of a button held right now included.
if (!filter) RemoteKeys.release(service);
info.flags ^= AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS;
service.setServiceInfo(info);
}
}
Loading
Loading