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

Allow toggling performance overlay while streaming #1219

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
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