Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add game back menu #1171

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
31 changes: 30 additions & 1 deletion app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.audio.AndroidAudioRenderer;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.GameInputDevice;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
Expand Down Expand Up @@ -213,6 +214,9 @@ protected void onCreate(Bundle savedInstanceState) {
// Inflate the content
setContentView(R.layout.activity_game);

// Hack: allows use keyboard by dpad or controller
getWindow().getDecorView().findViewById(android.R.id.content).setFocusable(true);

// Start the spinner
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.conn_establishing_msg), true);
Expand Down Expand Up @@ -1353,7 +1357,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) {

@Override
public boolean handleKeyDown(KeyEvent event) {
// Pass-through virtual navigation keys
// Pass-through navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return false;
}
Expand Down Expand Up @@ -1527,6 +1531,10 @@ private TouchContext getTouchContext(int actionIndex)
@Override
public void toggleKeyboard() {
LimeLog.info("Toggling keyboard overlay");

// Hack: allows use keyboard by dpad or controller
streamView.clearFocus();

InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(0, 0);
}
Expand Down Expand Up @@ -2715,6 +2723,11 @@ public void onUsbPermissionPromptCompleted() {
updatePipAutoEnter();
}

@Override
public void showGameMenu(GameInputDevice device) {
new GameMenu(this, conn, device);
}

@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
switch (keyEvent.getAction()) {
Expand All @@ -2728,4 +2741,20 @@ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
return false;
}
}

public void disconnect() {
finish();
}

@Override
public void onBackPressed() {
// Instead of "closing" the game activity open the game menu. The user has to select
// "Disconnect" within the game menu to actually disconnect from the remote host.
//
// Use the onBackPressed instead of the onKey function, since the onKey function
// also captures events while having the on-screen keyboard open. Using onBackPressed
// ensures that Android properly handles the back key when needed and only open the game
// menu when the activity would be closed.
kmreisi marked this conversation as resolved.
Show resolved Hide resolved
showGameMenu(null);
}
}
183 changes: 183 additions & 0 deletions app/src/main/java/com/limelight/GameMenu.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.limelight;

import android.app.AlertDialog;
import android.os.Handler;
import android.widget.ArrayAdapter;

import com.limelight.binding.input.GameInputDevice;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeyboardPacket;

import java.util.ArrayList;
import java.util.List;

/**
* Provide options for ongoing Game Stream.
* <p>
* Shown on back action in game activity.
*/
public class GameMenu {

private static final long TEST_GAME_FOCUS_DELAY = 10;
private static final long KEY_UP_DELAY = 25;

public static class MenuOption {
private final String label;
private final boolean withGameFocus;
private final Runnable runnable;

public MenuOption(String label, boolean withGameFocus, Runnable runnable) {
this.label = label;
this.withGameFocus = withGameFocus;
this.runnable = runnable;
}

public MenuOption(String label, Runnable runnable) {
this(label, false, runnable);
}
}

private final Game game;
private final NvConnection conn;
private final GameInputDevice device;

public GameMenu(Game game, NvConnection conn, GameInputDevice device) {
this.game = game;
this.conn = conn;
this.device = device;

showMenu();
}

private String getString(int id) {
return game.getResources().getString(id);
}

private static byte getModifier(short key) {
switch (key) {
case KeyboardTranslator.VK_LSHIFT:
return KeyboardPacket.MODIFIER_SHIFT;
case KeyboardTranslator.VK_LCONTROL:
return KeyboardPacket.MODIFIER_CTRL;
case KeyboardTranslator.VK_LWIN:
return KeyboardPacket.MODIFIER_META;

default:
return 0;
}
}

private void sendKeys(short[] keys) {
final byte[] modifier = {(byte) 0};

for (short key : keys) {
conn.sendKeyboardInput(key, KeyboardPacket.KEY_DOWN, modifier[0], (byte) 0);

// Apply the modifier of the pressed key, e.g. CTRL first issues a CTRL event (without
// modifier) and then sends the following keys with the CTRL modifier applied
modifier[0] |= getModifier(key);
}

new Handler().postDelayed((() -> {

for (int pos = keys.length - 1; pos >= 0; pos--) {
short key = keys[pos];

// Remove the keys modifier before releasing the key
modifier[0] &= ~getModifier(key);

conn.sendKeyboardInput(key, KeyboardPacket.KEY_UP, modifier[0], (byte) 0);
}
}), KEY_UP_DELAY);
}

private void runWithGameFocus(Runnable runnable) {
// Ensure that the Game activity is still active (not finished)
if (game.isFinishing()) {
return;
}
// Check if the game window has focus again, if not try again after delay
if (!game.hasWindowFocus()) {
new Handler().postDelayed(() -> runWithGameFocus(runnable), TEST_GAME_FOCUS_DELAY);
return;
}
// Game Activity has focus, run runnable
runnable.run();
}

private void run(MenuOption option) {
if (option.runnable == null) {
return;
}

if (option.withGameFocus) {
runWithGameFocus(option.runnable);
} else {
option.runnable.run();
}
}

private void showMenuDialog(String title, MenuOption[] options) {
AlertDialog.Builder builder = new AlertDialog.Builder(game);
builder.setTitle(title);

final ArrayAdapter<String> actions =
new ArrayAdapter<String>(game, android.R.layout.simple_list_item_1);

for (MenuOption option : options) {
actions.add(option.label);
}

builder.setAdapter(actions, (dialog, which) -> {
String label = actions.getItem(which);
for (MenuOption option : options) {
if (!label.equals(option.label)) {
continue;
}

run(option);
break;
}
});

builder.show();
}

private void showSpecialKeysMenu() {
showMenuDialog(getString(R.string.game_menu_send_keys), new MenuOption[]{
new MenuOption(getString(R.string.game_menu_send_keys_esc),
() -> sendKeys(new short[]{KeyboardTranslator.VK_ESCAPE})),
new MenuOption(getString(R.string.game_menu_send_keys_f11),
() -> sendKeys(new short[]{KeyboardTranslator.VK_F11})),
new MenuOption(getString(R.string.game_menu_send_keys_ctrl_v),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LCONTROL, KeyboardTranslator.VK_V})),
new MenuOption(getString(R.string.game_menu_send_keys_win),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LWIN})),
new MenuOption(getString(R.string.game_menu_send_keys_win_d),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LWIN, KeyboardTranslator.VK_D})),
new MenuOption(getString(R.string.game_menu_send_keys_win_g),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LWIN, KeyboardTranslator.VK_G})),
new MenuOption(getString(R.string.game_menu_send_keys_shift_tab),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LSHIFT, KeyboardTranslator.VK_TAB})),
new MenuOption(getString(R.string.game_menu_cancel), null),
});
}

private void showMenu() {
List<MenuOption> options = new ArrayList<>();

options.add(new MenuOption(getString(R.string.game_menu_toggle_keyboard), true,
() -> game.toggleKeyboard()));
kmreisi marked this conversation as resolved.
Show resolved Hide resolved

if (device != null) {
options.addAll(device.getGameMenuOptions());
}

options.add(new MenuOption(getString(R.string.game_menu_send_keys), () -> showSpecialKeysMenu()));
options.add(new MenuOption(getString(R.string.game_menu_disconnect), () -> game.disconnect()));
options.add(new MenuOption(getString(R.string.game_menu_cancel), null));

showMenuDialog("Game Menu", options.toArray(new MenuOption[options.size()]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import android.view.Surface;
import android.widget.Toast;

import com.limelight.GameMenu;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.driver.AbstractController;
Expand All @@ -52,6 +53,8 @@

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;

public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {

Expand Down Expand Up @@ -2397,7 +2400,7 @@ public boolean handleButtonUp(KeyEvent event) {
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
event.getEventTime() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
prefConfig.mouseEmulation) {
context.toggleMouseEmulation();
gestures.showGameMenu(context);
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
Expand Down Expand Up @@ -2890,7 +2893,7 @@ public void deviceAdded(AbstractController controller) {
usbDeviceContexts.put(controller.getControllerId(), context);
}

class GenericControllerContext {
class GenericControllerContext implements GameInputDevice {
public int id;
public boolean external;

Expand Down Expand Up @@ -2943,6 +2946,16 @@ else if (prefConfig.analogStickForScrolling == PreferenceConfiguration.AnalogSti
}
};

@Override
public List<GameMenu.MenuOption> getGameMenuOptions() {
List<GameMenu.MenuOption> options = new ArrayList<>();
options.add(new GameMenu.MenuOption(activityContext.getString(mouseEmulationActive ?
R.string.game_menu_toggle_mouse_off : R.string.game_menu_toggle_mouse_on),
true, () -> toggleMouseEmulation()));

return options;
}

public void toggleMouseEmulation() {
mainThreadHandler.removeCallbacks(mouseEmulationRunnable);
mouseEmulationActive = !mouseEmulationActive;
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/limelight/binding/input/GameInputDevice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.limelight.binding.input;

import com.limelight.GameMenu;

import java.util.List;

/**
* Generic Input Device
*/
public interface GameInputDevice {

/**
* @return list of device specific game menu options, e.g. configure a controller's mouse mode
*/
List<GameMenu.MenuOption> getGameMenuOptions();
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public class KeyboardTranslator implements InputManager.InputDeviceListener {
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_C = 67;
public static final int VK_D = 68;
public static final int VK_G = 71;
public static final int VK_V = 86;
kmreisi marked this conversation as resolved.
Show resolved Hide resolved
public static final int VK_Z = 90;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
Expand All @@ -34,6 +38,7 @@ public class KeyboardTranslator implements InputManager.InputDeviceListener {
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_F11 = 122;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
Expand All @@ -54,6 +59,9 @@ public class KeyboardTranslator implements InputManager.InputDeviceListener {
public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
public static final int VK_LWIN = 91;
public static final int VK_LSHIFT = 160;
public static final int VK_LCONTROL = 162;

private static class KeyboardMapping {
private final InputDevice device;
Expand Down Expand Up @@ -188,7 +196,7 @@ else if (keycode >= KeyEvent.KEYCODE_F1 &&
break;

case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2;
translated = VK_LCONTROL;
break;

case KeyEvent.KEYCODE_CTRL_RIGHT:
Expand Down Expand Up @@ -225,7 +233,7 @@ else if (keycode >= KeyEvent.KEYCODE_F1 &&
break;

case KeyEvent.KEYCODE_META_LEFT:
translated = 0x5b;
translated = VK_LWIN;
break;

case KeyEvent.KEYCODE_META_RIGHT:
Expand Down Expand Up @@ -277,7 +285,7 @@ else if (keycode >= KeyEvent.KEYCODE_F1 &&
break;

case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0;
translated = VK_LSHIFT;
break;

case KeyEvent.KEYCODE_SHIFT_RIGHT:
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/limelight/ui/GameGestures.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.limelight.ui;

import com.limelight.binding.input.GameInputDevice;

public interface GameGestures {
void toggleKeyboard();

void showGameMenu(GameInputDevice device);
}
Loading