Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ rule sit on its wanted list until a screenshot arrives.
| `locale` | <img src="switches/locale.jpg" width="320" alt="locale set to en on a TV whose system language is Vietnamese"> |
| `power_on_home` | <img src="switches/power_on_home.jpg" width="320" alt="power_on_home on a TV"> |
| `home_long_press_home` | <img src="switches/home_long_press_home.jpg" width="320" alt="home_long_press_home on a TV"> |
| `home_hold_panel` | <img src="switches/home_hold_panel.jpg" width="320" alt="home_hold_panel on a Chromecast with Google TV 4K: Home held on the remote opened the quick panel under the Settings pill"> |
| `vpn_pill` | <img src="switches/vpn_pill.jpg" width="320" alt="vpn_pill on a TV"> <img src="switches/vpn_pill_rest.jpg" width="320" alt="vpn_pill at rest: the pill outlined in red, focus on a tile"> |
| `icon_only_vpn` | <img src="switches/icon_only_vpn.jpg" width="320" alt="icon_only_vpn with no VPN up: an open lock on the plain round button, between Wi-Fi and Settings"> <img src="switches/icon_only_vpn_on.jpg" width="320" alt="icon_only_vpn with a VPN up: the closed lock in a green outline, focus on a tile"> |
| `power_on_home_always` | <img src="switches/power_on_home_always.jpg" width="320" alt="power_on_home_always on a TV"> |
Expand Down
Binary file added docs/switches/home_hold_panel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions launcher/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<!-- Android 11+ package visibility: lets the launcher list the TV apps it shows as tiles, and the
apps that declare a module card (docs/specs/card.md). -->
<queries>
<!-- home_hold_panel looks for a dashboard that answers a held Home before it takes over. -->
<intent>
<action android:name="android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL" />
</intent>
<!-- The standard settings handlers, so package visibility doesn't hide them and send the pills to a vendor fallback by mistake. -->
<intent>
<action android:name="android.settings.SETTINGS" />
Expand Down
8 changes: 7 additions & 1 deletion launcher/app/src/main/java/com/gathin/tvhome/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ static boolean settingsChanged(long seen, long current) {
final boolean powerOnHome;
/** Every Home press opens this launcher, a held one too, instead of Android's long-press action (home_long_press_home). */
final boolean homeLongPressHome;
/** home_hold_panel: a held Home opens the quick panel where no dashboard answers it. Set after construction. */
boolean homeHoldPanel;
/** Home at every power-on, whatever the firmware put in front: an input, Live TV or an app (power_on_home_always). */
final boolean powerOnHomeAlways;
/** The Inputs menu lists only inputs reporting connected; the rest fold behind a More inputs row (config.xml / theme inputs_connected_only). */
Expand Down Expand Up @@ -510,6 +512,7 @@ static Config load(Context ctx) {
boolean consumeVolumeKeys = res.getBoolean(R.bool.consume_volume_keys);
boolean powerOnHome = res.getBoolean(R.bool.power_on_home);
boolean homeLongPressHome = res.getBoolean(R.bool.home_long_press_home);
boolean homeHoldPanel = res.getBoolean(R.bool.home_hold_panel);
boolean powerOnHomeAlways = res.getBoolean(R.bool.power_on_home_always);
boolean inputsConnectedOnly = res.getBoolean(R.bool.inputs_connected_only);
boolean inputsCecStateOnly = res.getBoolean(R.bool.inputs_cec_state_only);
Expand Down Expand Up @@ -624,6 +627,7 @@ static Config load(Context ctx) {
consumeVolumeKeys = bool(json, "consume_volume_keys", consumeVolumeKeys);
powerOnHome = bool(json, "power_on_home", powerOnHome);
homeLongPressHome = bool(json, "home_long_press_home", homeLongPressHome);
homeHoldPanel = bool(json, "home_hold_panel", homeHoldPanel);
powerOnHomeAlways = bool(json, "power_on_home_always", powerOnHomeAlways);
inputsConnectedOnly = bool(json, "inputs_connected_only", inputsConnectedOnly);
inputsCecStateOnly = bool(json, "inputs_cec_state_only", inputsCecStateOnly);
Expand Down Expand Up @@ -701,7 +705,7 @@ static Config load(Context ctx) {
Festival festival = json == null ? Festival.NONE : Festival.parse(festivalName(json),
string(json, "festival_until", ""), string(json, "greeting", ""), string(json, "accent_color", ""));

return new Config(dock, fillDock, hidden, libraryApps, libraryExcludedApps, liveTv, liveTvAction, liveTvInputId, packageMap(launchActions), packageMap(appLabels),
Config config = new Config(dock, fillDock, hidden, libraryApps, libraryExcludedApps, liveTv, liveTvAction, liveTvInputId, packageMap(launchActions), packageMap(appLabels),
packageMap(appBanners), panelService, panelAction, panelExtra, coocaaSources, inputAction, inputSources, inputUriScheme, Math.max(1, columns),
Math.max(0, rowsBelow), animations, advancedEffects, shamsi, greetingName, moduleCards, reducedMotion, bootToLiveTv,
openOnStartChoice, dockTitle, dockRow, topMenu, launcherSettingsButton, reorderButton, showNetworkPill, showInputsPill,
Expand All @@ -716,6 +720,8 @@ static Config load(Context ctx) {
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);
config.homeHoldPanel = homeHoldPanel;
return config;
}

/**
Expand Down
29 changes: 29 additions & 0 deletions launcher/app/src/main/java/com/gathin/tvhome/HomeKeyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public class HomeKeyService extends AccessibilityService {
private long powerOnUntil;
/** home_long_press_home: this service takes every Home press, held ones included. */
private boolean homeLongPressHome;
/** home_hold_panel: a held Home opens the quick panel, on firmware where no dashboard answers it. */
private boolean homeHoldPanel;
/** app_keys "front:" entries, and the package of the activity in front, from window changes while there are any. */
private Map<Integer, String> frontAppKeys = Collections.emptyMap();
private Map<Integer, String> frontAppScancodes = Collections.emptyMap();
Expand Down Expand Up @@ -138,6 +140,7 @@ static void applyStandbyOptIn() {
@Override
public void onDestroy() {
if (running == this) running = null;
handler.removeCallbacks(homeHeld);
RemoteKeys.release(this);
stopWaitingForUnlock();
if (screenOnReceiver != null) unregisterReceiver(screenOnReceiver);
Expand Down Expand Up @@ -196,6 +199,8 @@ private void loadConfig() {
appScancodes = RemoteKeys.parseAppScancodes(config.appKeys);
RemoteKeys.applyKeyFilter(this, config.keyFilter);
homeLongPressHome = config.homeLongPressHome;
homeHoldPanel = holdOpensPanel(config.homeHoldPanel, !getPackageManager().queryIntentActivities(
new Intent(DASHBOARD_ACTION), 0).isEmpty());
frontAppKeys = RemoteKeys.parseFrontAppKeys(config.appKeys);
frontAppScancodes = RemoteKeys.parseFrontAppScancodes(config.appKeys);
boolean watchFront = !frontAppKeys.isEmpty() || !frontAppScancodes.isEmpty();
Expand Down Expand Up @@ -283,6 +288,14 @@ protected boolean onKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled()) openHome();
return true;
}
// home_hold_panel: timed here, because Android makes the repeats of a held key after this filter. The press
// still goes on to Android, which opens Home on a short one and consumes a long one.
if (homeHoldPanel && event.getKeyCode() == KeyEvent.KEYCODE_HOME) {
handler.removeCallbacks(homeHeld);
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
handler.postDelayed(homeHeld, ViewConfiguration.getLongPressTimeout() + 50L);
}
}
if (event.getKeyCode() == KeyEvent.KEYCODE_HOME && !stockHome.isEmpty()) {
redirectHomeKey(event);
return false;
Expand Down Expand Up @@ -398,6 +411,22 @@ static boolean firstBindAfterBoot(int boot, int lastBoot, long uptimeMs) {
private static final long HOME_KEY_DELAY_MS = 250;
private final Handler handler = new Handler(Looper.getMainLooper());

/** What Android sends a held Home to on Android TV: the stock launcher's dashboard. */
static final String DASHBOARD_ACTION = "android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL";

/** The switch takes over only where nothing answers a held Home, so a TV that still has its dashboard keeps it. */
static boolean holdOpensPanel(boolean switchOn, boolean dashboardThere) {
return switchOn && !dashboardThere;
}

private final Runnable homeHeld = () -> {
try {
startActivity(new Intent(this, QuickSettingsActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException | SecurityException ignored) {
// nothing to open: the press already went to Android
}
};

/** stock_home_redirect on the Home key: when the stock home is already on screen (it came up at boot, before
* Android bound this service), Home brings it up again with no window change, so the redirect above never
* fires and the TV stays on the stock home until another app opens (#1116). A short press opens this
Expand Down
8 changes: 8 additions & 0 deletions launcher/app/src/main/res/values/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@
still step). A theme's config.json can set it too. Off by default. -->
<bool name="home_long_press_home">false</bool>

<!-- Holding Home opens the launcher's quick panel (QuickSettingsActivity) where nothing else answers a held
Home. Android sends that press to android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL, which on Google TV
only the stock launcher's dashboard handles (launcherx on a Chromecast with Google TV 4K, Google TV 14), so
with that launcher disabled a held Home did nothing. HomeKeyService (HOME_KEY_SERVICE=1) times the hold
itself, since the accessibility filter never sees Android's key repeats, and leaves the press to Android.
Does nothing while the dashboard is still there. A theme's config.json can set it too. Off by default. -->
<bool name="home_hold_panel">false</bool>

<!-- Show the Solar Hijri (Shamsi) date used in Iran and Afghanistan, e.g. "27 Mehr 1405", above the
Gregorian date. -->
<bool name="shamsi_date">false</bool>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gathin.tvhome;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class HomeHoldPanelTest {

@Test
public void opensOnlyWithTheSwitchOnAndNoDashboard() {
assertTrue(HomeKeyService.holdOpensPanel(true, false));
assertFalse(HomeKeyService.holdOpensPanel(true, true)); // the TV's own dashboard keeps the press
assertFalse(HomeKeyService.holdOpensPanel(false, false)); // off by default: nothing changes
}
}
1 change: 1 addition & 0 deletions themes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ A new key needs one there in the same pull request.
| `stock_home_redirect` | string | Stock launcher package; if the firmware turns it back on (XGIMI), its home screen is sent straight back here (`HOME_KEY_SERVICE=1`). The Home-key half (a stock home already on screen when Home is pressed, #1116) needs `key_filter` on: with `key_filter: false` it stays on the stock home until another app opens (#2779) |
| `power_on_home` | bool | For firmware that opens the Live TV app (`live_tv_package`) by itself at every power-on (Changhong / Homwee): until the first key press after boot or screen-on, that Live TV window is sent back to Home (`HOME_KEY_SERVICE=1`). Ignored with `boot_to_live_tv`, or with Live TV as the Open on start app. Off by default |
| `home_long_press_home` | bool | Every Home press opens Home, a held one too. Android turns a Home press held past 400 ms into ALL_APPS for the system-image launcher, which nothing answers once the stock launcher is disabled, so a firm press on a slow remote did nothing (Philips TPM191E, Android 12, where `long_press_timeout` does not move that limit). Needs `key_filter` (`HOME_KEY_SERVICE=1`), so held keys such as Volume Up/Down stop repeating, confirmed on that Philips; single presses still step. Off by default |
| `home_hold_panel` | bool | Holding Home opens the launcher's quick panel where nothing else answers a held Home. Android sends that press to `TOGGLE_NOTIFICATION_HANDLER_PANEL`, which on Google TV only the stock launcher's dashboard handles, so once that launcher is disabled a held Home did nothing (Chromecast with Google TV 4K, Google TV 14). Needs `HOME_KEY_SERVICE=1`; the press itself still goes to Android, and a TV whose dashboard is still there keeps it. Off by default |
| `power_on_home_always` | bool | Home at every power-on, whatever the TV put in front: the last HDMI input, Live TV or an app, for TVs that come back to the last source (Philips). Unlike `power_on_home`, it does not wait for a window to show: the Home screen comes up about a second after the screen turns on (`HOME_KEY_SERVICE=1`). A key pressed before that wins. Ignored with `boot_to_live_tv`, or with an Open on start app. Off by default |
| `inputs_connected_only` | bool | The Inputs list shows only HDMI ports and CEC devices that report connected; empty ports, the AV jacks and devices asleep in standby fold behind a "More inputs" row that expands them. Some TVs (TCL) report an empty HDMI port as connected-standby, the same state a sleeping device has, so a device in standby is folded too. An HDMI row whose state the TV cannot be trusted with stays (`false` by default) |
| `inputs_cec_state_only` | bool | Only CEC device rows in the Inputs list show a connection state ("Steam Deck · HDMI 1", Connected); bare HDMI ports show none. For TVs that put port states on the wrong port: TCL UnionTV (Android 9) reports its empty HDMI 1 as connected and a device on HDMI 2 nowhere, while its CEC device list stays right. A device without CEC shows no state (`false` by default) |
Expand Down
Loading