Skip to content
Open
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
105 changes: 105 additions & 0 deletions app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import com.limelight.utils.ExternalDisplayControlActivity;
import com.limelight.utils.MouseModeOption;
import com.limelight.utils.PanZoomHandler;
import com.limelight.utils.TouchpadPresentation;
import com.limelight.utils.PerformanceDataTracker;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.ShortcutHelper;
Expand All @@ -55,6 +56,7 @@

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import androidx.annotation.RequiresApi;
import android.app.AlertDialog;
import android.app.PictureInPictureParams;
import android.app.Service;
Expand Down Expand Up @@ -142,6 +144,7 @@ public class Game extends AppCompatActivity implements SurfaceHolder.Callback,
ExternalControllerView.InputCallbacks,
PerfOverlayListener, UsbDriverService.UsbDriverStateListener, View.OnKeyListener {
public static Game instance;
private TouchpadPresentation touchpadPresentation;

private int lastButtonState = 0;

Expand Down Expand Up @@ -170,6 +173,10 @@ public class Game extends AppCompatActivity implements SurfaceHolder.Callback,

private ControllerHandler controllerHandler;
private KeyboardTranslator keyboardTranslator;

public KeyboardTranslator getKeyboardTranslator() {
return keyboardTranslator;
}
private VirtualController virtualController;

private KeyBoardController keyBoardController;
Expand Down Expand Up @@ -396,6 +403,11 @@ protected void onCreate(Bundle savedInstanceState) {

onExternelDisplay = currentDisplay.getDisplayId() != Display.DEFAULT_DISPLAY;

// Initialize touchpad presentation if in touchpad mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && prefConfig.secondaryScreenTouchpad) {
initTouchpadPresentation();
}

boolean shouldInvertDecoderResolution = false;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
Expand Down Expand Up @@ -1698,13 +1710,82 @@ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
hideSystemUi(50);
}

@RequiresApi(api = Build.VERSION_CODES.R)
private void initTouchpadPresentation() {
try {
DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);

// Get the touchpad display using DISPLAY_CATEGORY_PRESENTATION
Display[] presentations = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
Display touchpadDisplay = null;

LimeLog.info("Game - Found " + presentations.length + " presentation displays");

// Find the display matching our touchpad display ID
for (Display display : presentations) {
LimeLog.info("Game - Checking presentation display: " + display.getDisplayId() + " (" + display.getName() + ")");
if (display.getDisplayId() == prefConfig.touchpadDisplayId) {
touchpadDisplay = display;
break;
}
}

if (touchpadDisplay != null) {
LimeLog.info("Game - Creating TouchpadPresentation on display: " + touchpadDisplay.getDisplayId());
touchpadPresentation = new TouchpadPresentation(this, touchpadDisplay, prefConfig);
touchpadPresentation.show();
LimeLog.info("Game - TouchpadPresentation shown successfully");
} else {
LimeLog.warning("Game - Touchpad display " + prefConfig.touchpadDisplayId + " not found in presentation displays");
}
} catch (Exception e) {
LimeLog.severe("Game - Error initializing touchpad presentation: " + e.getMessage());
e.printStackTrace();
}
}

private void dismissTouchpadPresentation() {
if (touchpadPresentation != null) {
LimeLog.info("Game - Dismissing touchpad presentation");
touchpadPresentation.dismiss();
touchpadPresentation = null;
}
}

public void onTouchpadPresentationDismissed() {
LimeLog.info("Game - Touchpad presentation was dismissed");
touchpadPresentation = null;
}

@RequiresApi(api = Build.VERSION_CODES.R)
public void toggleTouchpadPresentation() {
if (touchpadPresentation != null) {
// Hide touchpad
dismissTouchpadPresentation();
Toast.makeText(this, R.string.toast_touchpad_hidden, Toast.LENGTH_SHORT).show();
} else if (prefConfig.secondaryScreenTouchpad) {
// Show touchpad
initTouchpadPresentation();
if (touchpadPresentation != null) {
Toast.makeText(this, R.string.toast_touchpad_shown, Toast.LENGTH_SHORT).show();
}
}
}

public boolean isTouchpadModeEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && prefConfig.secondaryScreenTouchpad;
}

@Override
protected void onDestroy() {
super.onDestroy();

instance = null;
timerHandler.removeCallbacksAndMessages(null);

// Dismiss touchpad presentation if active
dismissTouchpadPresentation();

if (prefConfig.enableFullExDisplay) handleDisplayRemoved();

if (controllerHandler != null) {
Expand Down Expand Up @@ -1757,6 +1838,20 @@ protected void onPause() {
super.onPause();
}

@Override
protected void onStart() {
super.onStart();

// Recreate touchpad presentation if it was dismissed when app went to background
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
prefConfig.secondaryScreenTouchpad &&
touchpadPresentation == null &&
conn != null) {
LimeLog.info("Game - onStart: Recreating touchpad presentation");
initTouchpadPresentation();
}
}

@Override
protected void onStop() {
super.onStop();
Expand Down Expand Up @@ -2402,6 +2497,10 @@ private TouchContext getTouchContext(int actionIndex, TouchContext[] inputContex
public void toggleKeyboard() {
if (isOnExternalDisplay()) {
ExternalDisplayControlActivity.toggleKeyboard();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && prefConfig.secondaryScreenTouchpad && touchpadPresentation != null) {
// In touchpad mode, keyboard is shown on touchpad display via the keyboard button
// Don't toggle it on the game display
LimeLog.info("Touchpad mode active - keyboard should be toggled from touchpad display");
} else {
LimeLog.info("Toggling keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
Expand Down Expand Up @@ -3289,11 +3388,14 @@ private boolean handleMultiTouchGesture(MotionEvent event, int eventAction, int

if (eventAction == MotionEvent.ACTION_POINTER_DOWN) {
if (pointerCount == 3) {
LimeLog.info("Game - 3 fingers down detected");
threeFingerDownTime = event.getEventTime();
} else if (pointerCount == 4) {
LimeLog.info("Game - 4 fingers down detected");
threeFingerDownTime = 0;
fourFingerDownTime = event.getEventTime();
} else if (pointerCount == 5) {
LimeLog.info("Game - 5 fingers down detected");
threeFingerDownTime = 0;
fourFingerDownTime = 0;
fiveFingerDownTime = event.getEventTime();
Expand All @@ -3315,9 +3417,12 @@ private boolean handleMultiTouchGesture(MotionEvent event, int eventAction, int
fourFingerDownTime = 0;
break;
} else if (pointerCount == 3 && threeFingerDownTime > 0 && currentEventTime - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
LimeLog.info("Game - 3 finger tap detected, toggling keyboard");
toggleKeyboard();
threeFingerDownTime = 0;
break;
} else if (pointerCount == 3) {
LimeLog.info("Game - 3 fingers up but not a tap (time: " + (currentEventTime - threeFingerDownTime) + "ms, threshold: " + THREE_FINGER_TAP_THRESHOLD + "ms)");
}
threeFingerDownTime = 0;
fourFingerDownTime = 0;
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/limelight/GameMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private void showAdvancedMenu(GameInputDevice device) {
if (game.allowChangeMouseMode) {
options.add(new MenuOption(getString(R.string.game_menu_select_mouse_mode), true, () -> game.selectMouseMode(dialogScreenContext)));
}

options.add(new MenuOption(getString(R.string.game_menu_toggle_hud), true, game::toggleHUD));
options.add(new MenuOption(getString(R.string.game_menu_toggle_floating_button), true, game::toggleFloatingButtonVisibility));
options.add(new MenuOption(getString(R.string.game_menu_toggle_keyboard_model), true, game::toggleKeyboardController));
Expand Down Expand Up @@ -317,6 +317,10 @@ public void showMenu(GameInputDevice device) {
options.add(new MenuOption(getString(R.string.game_menu_toggle_keyboard), true,
game::toggleKeyboard));

if (game.isTouchpadModeEnabled()) {
options.add(new MenuOption(getString(R.string.game_menu_toggle_touchpad), true, game::toggleTouchpadPresentation));
}

options.add(new MenuOption(getString(game.isZoomModeEnabled() ? R.string.game_menu_disable_zoom_mode : R.string.game_menu_enable_zoom_mode), true,
game::toggleZoomMode));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.limelight.preferences;

import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Display;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.preference.ListPreference;

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

/**
* A custom ListPreference that dynamically populates with available displays.
* Shows display ID, name, and resolution to help users identify screens.
*/
public class DisplaySelectionPreference extends ListPreference {

public DisplaySelectionPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
populateDisplays();
}

public DisplaySelectionPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
populateDisplays();
}

public DisplaySelectionPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
populateDisplays();
}

public DisplaySelectionPreference(@NonNull Context context) {
super(context);
populateDisplays();
}

/**
* Populates the preference with available displays from DisplayManager.
* Each entry shows: "Display [ID]: [Name] ([Width]x[Height])"
*/
private void populateDisplays() {
DisplayManager displayManager = (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
if (displayManager == null) {
// Fallback to simple primary/secondary if DisplayManager unavailable
setEntries(new CharSequence[]{"Primary display", "Secondary display"});
setEntryValues(new CharSequence[]{"0", "1"});
return;
}

Display[] displays = displayManager.getDisplays();
List<CharSequence> entries = new ArrayList<>();
List<CharSequence> entryValues = new ArrayList<>();

for (Display display : displays) {
int displayId = display.getDisplayId();
String displayName = getDisplayName(display);
String resolution = getDisplayResolution(display);

// Format: "Display 0: Built-in Screen (2560x1600)"
String entry = String.format("Display %d: %s (%s)", displayId, displayName, resolution);

entries.add(entry);
entryValues.add(String.valueOf(displayId));
}

// If no displays found, add a default entry
if (entries.isEmpty()) {
entries.add("Primary display");
entryValues.add("0");
}

setEntries(entries.toArray(new CharSequence[0]));
setEntryValues(entryValues.toArray(new CharSequence[0]));
}

/**
* Gets a friendly name for the display.
*/
private String getDisplayName(Display display) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ has a proper display name API
return getDisplayNameR(display);
} else {
// Fallback for older Android versions
int displayId = display.getDisplayId();
if (displayId == Display.DEFAULT_DISPLAY) {
return "Built-in Screen";
} else {
return "External Display";
}
}
}

@RequiresApi(api = Build.VERSION_CODES.R)
private String getDisplayNameR(Display display) {
String name = display.getName();
int displayId = display.getDisplayId();

if (name != null && !name.isEmpty()) {
return name;
} else if (displayId == Display.DEFAULT_DISPLAY) {
return "Built-in Screen";
} else {
return "External Display";
}
}

/**
* Gets the display resolution as "WIDTHxHEIGHT"
*/
private String getDisplayResolution(Display display) {
Display.Mode mode = display.getMode();
return mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight();
}

/**
* Refresh the display list when the preference is shown.
* This ensures we have the latest display information.
*/
@Override
protected void onClick() {
populateDisplays();
super.onClick();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ public enum AnalogStickForScrolling {

private static final String CHECKBOX_SHOW_OVERLAY_ZOOM_TOGGLE_BUTTON = "checkbox_show_overlay_zoom_toggle_button";

//Use secondary screen as touchpad
private static final String CHECKBOX_SECONDARY_SCREEN_TOUCHPAD = "checkbox_secondary_screen_touchpad";
private static final String LIST_TOUCHPAD_DISPLAY = "list_touchpad_display";

//竖屏模式
private static final String CHECKBOX_AUTO_ORIENTATION = "checkbox_auto_orientation";
//屏幕特殊按键
Expand Down Expand Up @@ -208,6 +212,8 @@ public enum AnalogStickForScrolling {
private static final boolean DEFAULT_ENABLE_COMMIT_TEXT = false;
private static final String DEFAULT_ONSCREEN_KEYBOARD_ALIGN_MODE = "center";
private static final boolean DEFAULT_SHOW_OVERLAY_TOGGLE_BUTTON = false;
private static final boolean DEFAULT_SECONDARY_SCREEN_TOUCHPAD = false;
private static final int DEFAULT_TOUCHPAD_DISPLAY = 1; // Display ID 1 (usually secondary)

private static final boolean DEFAULT_REMEMBER_ZOOM_PAN = false;
private static final float DEFAULT_ZOOM_SCALE = 1.0f;
Expand Down Expand Up @@ -282,6 +288,8 @@ public enum AnalogStickForScrolling {
public boolean enableBackMenu;
public boolean enableFloatingButton;
public boolean showOverlayZoomToggleButton;
public boolean secondaryScreenTouchpad;
public int touchpadDisplayId; // Display ID for touchpad mode

//Invert video width/height
public boolean autoInvertVideoResolution;
Expand Down Expand Up @@ -938,6 +946,16 @@ else if (audioConfig.equals("51")) {
config.enableBackMenu = prefs.getBoolean(CHECKBOX_ENABLE_QUIT_DIALOG,true);
config.enableFloatingButton = prefs.getBoolean(CHECKBOX_ENABLE_FLOATING_BUTTON,DEFAULT_ENABLE_FLOATING_BUTTON);
config.showOverlayZoomToggleButton = prefs.getBoolean(CHECKBOX_SHOW_OVERLAY_ZOOM_TOGGLE_BUTTON, DEFAULT_SHOW_OVERLAY_TOGGLE_BUTTON);
config.secondaryScreenTouchpad = prefs.getBoolean(CHECKBOX_SECONDARY_SCREEN_TOUCHPAD, DEFAULT_SECONDARY_SCREEN_TOUCHPAD);
// Try to read as int first (new format), fall back to string for backwards compatibility
try {
String displayIdStr = prefs.getString(LIST_TOUCHPAD_DISPLAY, String.valueOf(DEFAULT_TOUCHPAD_DISPLAY));
config.touchpadDisplayId = Integer.parseInt(displayIdStr);
} catch (NumberFormatException e) {
// Handle legacy string values ("primary"/"secondary")
String legacyValue = prefs.getString(LIST_TOUCHPAD_DISPLAY, "secondary");
config.touchpadDisplayId = legacyValue.equals("primary") ? Display.DEFAULT_DISPLAY : 1;
}
config.autoOrientation = prefs.getBoolean(CHECKBOX_AUTO_ORIENTATION,false);
config.autoInvertVideoResolution = prefs.getBoolean(AUTO_INVERT_VIDEO_RESOLUTION_PREF_STRING, DEFAULT_AUTO_INVERT_VIDEO_RESOLUTION);
config.resolutionScaleFactor = prefs.getInt(RESOLUTION_SCALE_FACTOR_PREF_STRING, DEFAULT_RESOLUTION_SCALE_FACTOR);
Expand Down
Loading