Skip to content

Commit

Permalink
Added:
Browse files Browse the repository at this point in the history
- Custom Resolution
- Absolute Touch (Tablet Mode)
- Game Menu
- And a few custom send special keys command
  • Loading branch information
MuhamadRifkii committed May 5, 2024
1 parent f54f8c8 commit 0ec169c
Show file tree
Hide file tree
Showing 18 changed files with 670 additions and 22 deletions.
54 changes: 43 additions & 11 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 All @@ -23,7 +24,6 @@
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge;
Expand Down Expand Up @@ -146,6 +146,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 MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
Expand Down Expand Up @@ -368,10 +369,10 @@ public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
}
}

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

decoderRenderer = new MediaCodecDecoderRenderer(
this,
Expand Down Expand Up @@ -613,9 +614,7 @@ public void onConfigurationChanged(Configuration newConfig) {
virtualController.show();
}

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

notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);

Expand Down Expand Up @@ -1307,7 +1306,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 @@ -2016,11 +2015,11 @@ else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMask
// TODO: Re-enable native touch when have a better solution for handling
// cancelled touches from Android gestures and 3 finger taps to activate
// the software keyboard.
/*if (!prefConfig.touchscreenTrackpad && trySendTouchEvent(view, event)) {
if (!prefConfig.touchscreenTrackpad && trySendTouchEvent(view, event)) {
// If this host supports touch events and absolute touch is enabled,
// send it directly as a touch event.
return true;
}*/
}

TouchContext context = getTouchContext(actionIndex);
if (context == null) {
Expand Down Expand Up @@ -2646,6 +2645,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 @@ -2660,6 +2664,10 @@ 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 @@ -2673,4 +2681,28 @@ 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;

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_f11_alt),
() -> sendKeys(new short[]{KeyboardTranslator.VK_MENU, KeyboardTranslator.VK_RETURN})),
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_send_keys_insert),
() -> sendKeys(new short[]{KeyboardTranslator.VK_INSERT})),
new MenuOption(getString(R.string.game_menu_send_move_window),
() -> sendKeys(new short[]{KeyboardTranslator.VK_LSHIFT, KeyboardTranslator.VK_LWIN, KeyboardTranslator.VK_RIGHT})),
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 @@ -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 @@ -2371,7 +2374,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 @@ -2864,7 +2867,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 @@ -2917,6 +2920,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),
() -> 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();
}
Loading

0 comments on commit 0ec169c

Please sign in to comment.