From 9f951bba444b0a054ad0c6cfbb88c72dcc1eb1d9 Mon Sep 17 00:00:00 2001 From: Mahmoud Khalifa <32329050+M-khalifa@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:57:32 -0600 Subject: [PATCH] Launcher: InputKeyService opens an app_keys front: entry the way HomeKeyService does --- docs/launcher.md | 4 ++-- .../java/com/gathin/tvhome/HomeKeyService.java | 10 ++++------ .../java/com/gathin/tvhome/InputKeyService.java | 11 +++++++++-- .../main/java/com/gathin/tvhome/RemoteKeys.java | 8 ++++++++ .../java/com/gathin/tvhome/RemoteKeysTest.java | 17 +++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/launcher.md b/docs/launcher.md index 0ebeb83cfa..e2fcf1b365 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -120,8 +120,8 @@ tools/tv.py 'settings put secure screensaver_components com.gathin.tvhome/.Clock - A `hold:` prefix on an `app_keys` entry gives that button a second app: a normal press opens the unprefixed app, holding for half a second opens the hold one (by keycode or `hold:scan:...`). An entry can name an activity (`package/.Activity`), such as an add-on's quick bar, instead of the app's launcher entry. - A `front:` prefix sets what a tap opens while that button's own app is already in front (`HomeKeyService` - only). + A `front:` prefix sets what a tap opens while that button's own app is already in front. It needs + `HomeKeyService` bound (`HOME_KEY_SERVICE=1`); `InputKeyService` follows it then. - In place of an app, an `app_keys` entry can name an action: `back`, `home`, `recents`, `mute`, `sleep` (standby, Android 9 and later), `screensaver`, `settings`, `wifi`, `library` (Home with the App library open, for a remote's Apps button; needs `app_library`) or `ignore` (the button does nothing), so a spare diff --git a/launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java b/launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java index a7c9d776b1..4a8f43c8b2 100644 --- a/launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java +++ b/launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java @@ -77,10 +77,9 @@ public class HomeKeyService extends AccessibilityService { private long powerOnUntil; /** home_long_press_home: this service takes every Home press, held ones included. */ private boolean homeLongPressHome; - /** app_keys "front:" entries, and the package of the activity in front, from window changes while there are any. */ + /** app_keys "front:" entries; the package in front goes to RemoteKeys.frontPackage, for both helpers. */ private Map frontAppKeys = Collections.emptyMap(); private Map frontAppScancodes = Collections.emptyMap(); - private String frontPackage = ""; /** power_on_home_always: Home at every power-on, not only over Live TV. */ private boolean powerOnAlways; /** power_on_home: re-arms after the screen turns on; null unless the key is on. */ @@ -139,6 +138,7 @@ static void applyStandbyOptIn() { @Override public void onDestroy() { if (running == this) running = null; + RemoteKeys.frontPackage = ""; RemoteKeys.release(this); stopWaitingForUnlock(); if (screenOnReceiver != null) unregisterReceiver(screenOnReceiver); @@ -242,9 +242,7 @@ private void loadConfig() { /** A tap's target: the "front:" entry while the button's own app is already in front, else the usual one. */ private String inFront(Map keys, Map front, int code) { - String target = keys.get(code); - String alt = front.get(code); - return alt != null && RemoteKeys.inFront(target, frontPackage) ? alt : target; + return RemoteKeys.tap(keys.get(code), front.get(code)); } private boolean isSettingsKey(KeyEvent event) { @@ -314,7 +312,7 @@ public void onAccessibilityEvent(AccessibilityEvent event) { try { ComponentName window = new ComponentName(event.getPackageName().toString(), event.getClassName().toString()); getPackageManager().getActivityInfo(window, 0); - frontPackage = window.getPackageName(); + RemoteKeys.frontPackage = window.getPackageName(); } catch (PackageManager.NameNotFoundException notAnActivity) { // a dialog, panel or overlay: the app under it is still in front } diff --git a/launcher/app/src/main/java/com/gathin/tvhome/InputKeyService.java b/launcher/app/src/main/java/com/gathin/tvhome/InputKeyService.java index 7f8eac1102..e4c4a26f57 100644 --- a/launcher/app/src/main/java/com/gathin/tvhome/InputKeyService.java +++ b/launcher/app/src/main/java/com/gathin/tvhome/InputKeyService.java @@ -25,6 +25,9 @@ public class InputKeyService extends AccessibilityService { private Map appScancodes = Collections.emptyMap(); private Map holdAppKeys = Collections.emptyMap(); private Map holdAppScancodes = Collections.emptyMap(); + /** app_keys "front:" entries, chosen the way HomeKeyService chooses them (RemoteKeys.tap). */ + private Map frontAppKeys = Collections.emptyMap(); + private Map frontAppScancodes = Collections.emptyMap(); /** restore_key_services was on when the config was read: this helper is noted for ServiceRepair. */ private boolean repairRecord; /** consume_volume_keys: Volume Up/Down go to VolumeKeys instead of on to the firmware. */ @@ -47,6 +50,8 @@ protected void onServiceConnected() { RemoteKeys.applyKeyFilter(this, config.keyFilter); holdAppKeys = RemoteKeys.parseHoldAppKeys(config.appKeys); holdAppScancodes = RemoteKeys.parseHoldAppScancodes(config.appKeys); + frontAppKeys = RemoteKeys.parseFrontAppKeys(config.appKeys); + frontAppScancodes = RemoteKeys.parseFrontAppScancodes(config.appKeys); // gamepad_passthrough, off by default. Only with key_filter on: off, the service never filters keys. if (config.gamepadPassthrough && config.keyFilter && gamepads == null) { gamepads = new GamepadPassthrough(this); @@ -64,9 +69,11 @@ private boolean isInputKey(int keyCode) { protected boolean onKeyEvent(KeyEvent event) { if (consumeVolumeKeys && VolumeKeys.handle(this, event)) return true; // A scancode entry is more specific than any keycode, so it goes first. - if (RemoteKeys.handle(this, event, appScancodes.get(event.getScanCode()), + if (RemoteKeys.handle(this, event, RemoteKeys.tap(appScancodes.get(event.getScanCode()), + frontAppScancodes.get(event.getScanCode())), holdAppScancodes.get(event.getScanCode()))) return true; - if (RemoteKeys.handle(this, event, appKeys.get(event.getKeyCode()), + if (RemoteKeys.handle(this, event, RemoteKeys.tap(appKeys.get(event.getKeyCode()), + frontAppKeys.get(event.getKeyCode())), holdAppKeys.get(event.getKeyCode()))) return true; if (!isInputKey(event.getKeyCode())) return false; if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled()) { diff --git a/launcher/app/src/main/java/com/gathin/tvhome/RemoteKeys.java b/launcher/app/src/main/java/com/gathin/tvhome/RemoteKeys.java index 2f6fec37b6..0be97cb05e 100644 --- a/launcher/app/src/main/java/com/gathin/tvhome/RemoteKeys.java +++ b/launcher/app/src/main/java/com/gathin/tvhome/RemoteKeys.java @@ -179,6 +179,14 @@ static boolean inFront(String target, String front) { return (slash < 0 ? target : target.substring(0, slash)).equals(front); } + /** The app in front, from HomeKeyService's window changes while app_keys has "front:" entries; "" otherwise. */ + static String frontPackage = ""; + + /** A tap's target for both helpers: its "front:" entry while the button's own app is in front, else the usual one. */ + static String tap(String target, String alt) { + return alt != null && inFront(target, frontPackage) ? alt : target; + } + /** Created on first use: building a Handler while the class loads breaks the JVM tests. */ private static Presses presses; diff --git a/launcher/app/src/test/java/com/gathin/tvhome/RemoteKeysTest.java b/launcher/app/src/test/java/com/gathin/tvhome/RemoteKeysTest.java index 5b734d94e2..ac5e15d50d 100644 --- a/launcher/app/src/test/java/com/gathin/tvhome/RemoteKeysTest.java +++ b/launcher/app/src/test/java/com/gathin/tvhome/RemoteKeysTest.java @@ -36,6 +36,23 @@ private static Map parseHold(String... entries) { assertFalse(RemoteKeys.inFront("input:com.vt.source.external/.HdmiInputService/HW2", "com.vt.source.external")); } + // Both helpers pick a tap's target here, so InputKeyService cannot open the usual app over a "front:" entry. + @Test public void tapPicksTheFrontEntryOnlyWhileTheButtonsAppIsInFront() { + String before = RemoteKeys.frontPackage; + try { + RemoteKeys.frontPackage = "com.vt.tv.tvcenter"; + assertEquals("guide:com.vt.tv.tvcenter", RemoteKeys.tap("com.vt.tv.tvcenter", "guide:com.vt.tv.tvcenter")); + assertEquals("com.vt.tv.tvcenter", RemoteKeys.tap("com.vt.tv.tvcenter", null)); + RemoteKeys.frontPackage = "com.gathin.tvhome"; + assertEquals("com.vt.tv.tvcenter", RemoteKeys.tap("com.vt.tv.tvcenter", "guide:com.vt.tv.tvcenter")); + RemoteKeys.frontPackage = ""; + assertEquals("com.vt.tv.tvcenter", RemoteKeys.tap("com.vt.tv.tvcenter", "guide:com.vt.tv.tvcenter")); + assertNull(RemoteKeys.tap(null, "guide:com.vt.tv.tvcenter")); + } finally { + RemoteKeys.frontPackage = before; + } + } + @Test public void frontEntriesAreReadApartFromTheRest() { List entries = Arrays.asList("172:com.vt.tv.tvcenter", "front:172:guide:com.vt.tv.tvcenter", "front:scan:490:com.wbd.hbomax", "hold:172:a.b");