Skip to content
Draft
49 changes: 39 additions & 10 deletions 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 @@ -142,6 +143,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private TextView notificationOverlayView;
private int requestedNotificationOverlayVisibility = View.GONE;
private TextView performanceOverlayView;
private int requestedPerformanceOverlayVisibility = View.GONE;

private ShortcutHelper shortcutHelper;

Expand Down Expand Up @@ -378,11 +380,6 @@ public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
}
}

// Check if the user has enabled performance stats overlay
if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}

decoderRenderer = new MediaCodecDecoderRenderer(
this,
prefConfig,
Expand Down Expand Up @@ -630,10 +627,7 @@ public void onConfigurationChanged(Configuration newConfig) {
virtualController.show();
}

if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}

performanceOverlayView.setVisibility(requestedPerformanceOverlayVisibility);
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);

// Update GameManager state to indicate we're out of PiP (gaming, non-interruptible)
Expand Down Expand Up @@ -1267,7 +1261,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 @@ -2280,6 +2274,11 @@ public void run() {
});
}

@Override
public boolean isPerfOverlayVisible() {
return requestedPerformanceOverlayVisibility == View.VISIBLE;
}

@Override
public void onUsbPermissionPromptStarting() {
// Disable PiP auto-enter while the USB permission prompt is on-screen. This prevents
Expand All @@ -2294,6 +2293,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 @@ -2307,4 +2311,29 @@ 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.
showGameMenu(null);
}

public void togglePerformanceOverlay() {
if (requestedPerformanceOverlayVisibility == View.VISIBLE) {
requestedPerformanceOverlayVisibility = View.GONE;
} else {
requestedPerformanceOverlayVisibility = View.VISIBLE;
}
performanceOverlayView.setVisibility(requestedPerformanceOverlayVisibility);
}
}
184 changes: 184 additions & 0 deletions app/src/main/java/com/limelight/GameMenu.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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()));

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

options.add(new MenuOption(getString(R.string.game_menu_toggle_performance_overlay), () -> game.togglePerformanceOverlay()));
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 @@ -22,7 +22,9 @@
import android.view.MotionEvent;
import android.widget.Toast;

import com.limelight.GameMenu;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.driver.AbstractController;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
Expand All @@ -36,6 +38,8 @@
import org.cgutman.shieldcontrollerextensions.SceManager;

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

public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {

Expand Down Expand Up @@ -1521,7 +1525,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 @@ -1868,7 +1872,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 @@ -1911,6 +1915,16 @@ public void run() {
}
};

@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() {
handler.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();
}
Loading