Added controller support

This commit is contained in:
izzy2lost
2025-08-25 13:56:13 -04:00
parent 26151a6259
commit 30191559c3
11 changed files with 965 additions and 23 deletions
@@ -0,0 +1,119 @@
package com.izzy2lost.psx2;
import android.content.Context;
import android.view.InputDevice;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Controller configuration and detection utility
* Based on AetherSX2's controller support
*/
public class ControllerConfig {
private static final String TAG = "ControllerConfig";
/**
* Get list of connected controllers
*/
public static List<ControllerInfo> getConnectedControllers() {
List<ControllerInfo> controllers = new ArrayList<>();
int[] deviceIds = InputDevice.getDeviceIds();
for (int deviceId : deviceIds) {
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null && isController(device)) {
controllers.add(new ControllerInfo(deviceId, device.getName(), getControllerType(device)));
}
}
return controllers;
}
/**
* Check if a device is a controller
*/
private static boolean isController(InputDevice device) {
int sources = device.getSources();
return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
}
/**
* Get controller type description
*/
private static String getControllerType(InputDevice device) {
String name = device.getName().toLowerCase();
if (name.contains("xbox")) {
return "Xbox Controller";
} else if (name.contains("playstation") || name.contains("ps4") || name.contains("ps5") || name.contains("dualshock") || name.contains("dualsense")) {
return "PlayStation Controller";
} else if (name.contains("nintendo") || name.contains("pro controller")) {
return "Nintendo Controller";
} else if (name.contains("steam")) {
return "Steam Controller";
} else {
return "Generic Controller";
}
}
/**
* Log controller information for debugging
*/
public static void logControllerInfo(Context context) {
List<ControllerInfo> controllers = getConnectedControllers();
Log.i(TAG, "=== Connected Controllers ===");
if (controllers.isEmpty()) {
Log.i(TAG, "No controllers detected");
} else {
for (ControllerInfo controller : controllers) {
Log.i(TAG, "Controller: " + controller.name + " (ID: " + controller.deviceId + ", Type: " + controller.type + ")");
}
}
Log.i(TAG, "============================");
}
/**
* Controller information holder
*/
public static class ControllerInfo {
public final int deviceId;
public final String name;
public final String type;
public ControllerInfo(int deviceId, String name, String type) {
this.deviceId = deviceId;
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name + " (" + type + ")";
}
}
/**
* Get button mapping description for user reference
*/
public static String getButtonMappingDescription() {
return "Controller Button Mapping:\n\n" +
"• A Button → Cross (X)\n" +
"• B Button → Circle (O)\n" +
"• X Button → Square (□)\n" +
"• Y Button → Triangle (△)\n" +
"• L1/LB → L1\n" +
"• R1/RB → R1\n" +
"• L2/LT → L2\n" +
"• R2/RT → R2\n" +
"• Left Stick Click → L3\n" +
"• Right Stick Click → R3\n" +
"• Select/Back → Select\n" +
"• Start/Menu → Start\n" +
"• D-Pad → D-Pad\n" +
"• Left Stick → Left Analog\n" +
"• Right Stick → Right Analog";
}
}
@@ -0,0 +1,323 @@
package com.izzy2lost.psx2;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.util.Log;
import android.util.SparseArray;
/**
* Controller input handler based on AetherSX2's PAD implementation
* Maps Android controller inputs to PS2 controller buttons
*/
public class ControllerInputHandler {
private static final String TAG = "ControllerInput";
// PS2 Controller button constants (using Android KeyEvent codes that MainActivity expects)
public static final int PAD_L2 = KeyEvent.KEYCODE_BUTTON_L2;
public static final int PAD_R2 = KeyEvent.KEYCODE_BUTTON_R2;
public static final int PAD_L1 = KeyEvent.KEYCODE_BUTTON_L1;
public static final int PAD_R1 = KeyEvent.KEYCODE_BUTTON_R1;
public static final int PAD_TRIANGLE = KeyEvent.KEYCODE_BUTTON_Y;
public static final int PAD_CIRCLE = KeyEvent.KEYCODE_BUTTON_B;
public static final int PAD_CROSS = KeyEvent.KEYCODE_BUTTON_A;
public static final int PAD_SQUARE = KeyEvent.KEYCODE_BUTTON_X;
public static final int PAD_SELECT = KeyEvent.KEYCODE_BUTTON_SELECT;
public static final int PAD_L3 = KeyEvent.KEYCODE_BUTTON_THUMBL;
public static final int PAD_R3 = KeyEvent.KEYCODE_BUTTON_THUMBR;
public static final int PAD_START = KeyEvent.KEYCODE_BUTTON_START;
public static final int PAD_UP = KeyEvent.KEYCODE_DPAD_UP;
public static final int PAD_RIGHT = KeyEvent.KEYCODE_DPAD_RIGHT;
public static final int PAD_DOWN = KeyEvent.KEYCODE_DPAD_DOWN;
public static final int PAD_LEFT = KeyEvent.KEYCODE_DPAD_LEFT;
// Analog stick mapping (using MainActivity's custom codes)
public static final int PAD_L_UP = 110;
public static final int PAD_L_RIGHT = 111;
public static final int PAD_L_DOWN = 112;
public static final int PAD_L_LEFT = 113;
public static final int PAD_R_UP = 120;
public static final int PAD_R_RIGHT = 121;
public static final int PAD_R_DOWN = 122;
public static final int PAD_R_LEFT = 123;
// Analog stick deadzone (matching AetherSX2's default)
private static final float ANALOG_DEADZONE = 0.15f;
// Button combo detection
private boolean mSelectPressed = false;
private boolean mStartPressed = false;
private long mComboDetectionTime = 0;
private static final long COMBO_TIMEOUT_MS = 500; // 500ms window for combo
// Key mapping from Android KeyEvent to PS2 buttons
private static final SparseArray<Integer> sKeyMapping = new SparseArray<>();
// Motion axis mapping from Android MotionEvent to PS2 analog inputs
private static final SparseArray<Integer> sAxisMapping = new SparseArray<>();
static {
// Initialize key mappings (Android KeyEvent -> PS2 button)
// Since our constants now match Android keycodes, we can map directly
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_A, PAD_CROSS); // A -> Cross (X)
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_B, PAD_CIRCLE); // B -> Circle
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_X, PAD_SQUARE); // X -> Square
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_Y, PAD_TRIANGLE); // Y -> Triangle
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_L1, PAD_L1);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_R1, PAD_R1);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_L2, PAD_L2);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_R2, PAD_R2);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_THUMBL, PAD_L3);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_THUMBR, PAD_R3);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_SELECT, PAD_SELECT);
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_START, PAD_START);
// D-Pad
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_UP, PAD_UP);
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_DOWN, PAD_DOWN);
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_LEFT, PAD_LEFT);
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_RIGHT, PAD_RIGHT);
// Initialize axis mappings for analog sticks
// Map to our custom analog codes that MainActivity expects
sAxisMapping.put(MotionEvent.AXIS_X, PAD_L_LEFT); // Left stick X (negative = left)
sAxisMapping.put(MotionEvent.AXIS_Y, PAD_L_UP); // Left stick Y (negative = up)
sAxisMapping.put(MotionEvent.AXIS_Z, PAD_R_LEFT); // Right stick X (negative = left)
sAxisMapping.put(MotionEvent.AXIS_RZ, PAD_R_UP); // Right stick Y (negative = up)
sAxisMapping.put(MotionEvent.AXIS_LTRIGGER, PAD_L2); // Left trigger
sAxisMapping.put(MotionEvent.AXIS_RTRIGGER, PAD_R2); // Right trigger
}
public interface ControllerInputListener {
void onControllerButtonPressed(int controllerId, int button, boolean pressed);
void onControllerAnalogInput(int controllerId, int axis, float value);
void onControllerCombo(int controllerId, String comboName);
}
private ControllerInputListener mListener;
public ControllerInputHandler(ControllerInputListener listener) {
mListener = listener;
}
/**
* Handle key events from controllers
*/
public boolean handleKeyEvent(KeyEvent event) {
if (!isFromController(event)) {
return false;
}
int keyCode = event.getKeyCode();
Integer ps2Button = sKeyMapping.get(keyCode);
if (ps2Button != null) {
int controllerId = event.getDeviceId();
boolean pressed = (event.getAction() == KeyEvent.ACTION_DOWN);
// Check for Select+Start combo
if (handleButtonCombo(controllerId, ps2Button, pressed)) {
return true; // Combo detected, don't send individual button presses
}
Log.d(TAG, "Controller " + controllerId + " button " + ps2Button + " " + (pressed ? "pressed" : "released"));
if (mListener != null) {
mListener.onControllerButtonPressed(controllerId, ps2Button, pressed);
}
return true;
}
return false;
}
/**
* Handle button combinations (like Select+Start)
*/
private boolean handleButtonCombo(int controllerId, int button, boolean pressed) {
long currentTime = System.currentTimeMillis();
// Track Select and Start button states
if (button == PAD_SELECT) {
mSelectPressed = pressed;
if (pressed) mComboDetectionTime = currentTime;
} else if (button == PAD_START) {
mStartPressed = pressed;
if (pressed) mComboDetectionTime = currentTime;
}
// Check if both buttons are pressed within the timeout window
if (mSelectPressed && mStartPressed &&
(currentTime - mComboDetectionTime) < COMBO_TIMEOUT_MS) {
Log.d(TAG, "Select+Start combo detected!");
// Reset combo state
mSelectPressed = false;
mStartPressed = false;
if (mListener != null) {
mListener.onControllerCombo(controllerId, "select_start");
}
return true; // Combo handled, don't process individual buttons
}
// Reset combo state if timeout exceeded
if ((currentTime - mComboDetectionTime) > COMBO_TIMEOUT_MS) {
mSelectPressed = false;
mStartPressed = false;
}
return false; // No combo, process button normally
}
/**
* Handle motion events from controllers (analog sticks, triggers)
*/
public boolean handleMotionEvent(MotionEvent event) {
if (!isFromController(event)) {
return false;
}
int controllerId = event.getDeviceId();
// Handle left stick X axis
float leftX = event.getAxisValue(MotionEvent.AXIS_X);
leftX = applyDeadzone(leftX, ANALOG_DEADZONE);
if (mListener != null) {
// Only send the direction that's actually being pressed
if (leftX < 0) {
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, -leftX); // Left direction (positive value)
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, 0); // Clear right
} else if (leftX > 0) {
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, leftX); // Right direction
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, 0); // Clear left
} else {
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, 0); // Clear both
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, 0);
}
}
// Handle left stick Y axis
float leftY = event.getAxisValue(MotionEvent.AXIS_Y);
leftY = applyDeadzone(leftY, ANALOG_DEADZONE);
if (mListener != null) {
if (leftY < 0) {
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, -leftY); // Up direction (positive value)
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, 0); // Clear down
} else if (leftY > 0) {
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, leftY); // Down direction
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, 0); // Clear up
} else {
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, 0); // Clear both
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, 0);
}
}
// Handle right stick X axis
float rightX = event.getAxisValue(MotionEvent.AXIS_Z);
rightX = applyDeadzone(rightX, ANALOG_DEADZONE);
if (mListener != null) {
if (rightX < 0) {
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, -rightX); // Left direction (positive value)
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, 0); // Clear right
} else if (rightX > 0) {
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, rightX); // Right direction
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, 0); // Clear left
} else {
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, 0); // Clear both
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, 0);
}
}
// Handle right stick Y axis
float rightY = event.getAxisValue(MotionEvent.AXIS_RZ);
rightY = applyDeadzone(rightY, ANALOG_DEADZONE);
if (mListener != null) {
if (rightY < 0) {
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, -rightY); // Up direction (positive value)
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, 0); // Clear down
} else if (rightY > 0) {
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, rightY); // Down direction
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, 0); // Clear up
} else {
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, 0); // Clear both
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, 0);
}
}
// Handle triggers
float leftTrigger = event.getAxisValue(MotionEvent.AXIS_LTRIGGER);
if (mListener != null) {
mListener.onControllerAnalogInput(controllerId, PAD_L2, leftTrigger);
}
float rightTrigger = event.getAxisValue(MotionEvent.AXIS_RTRIGGER);
if (mListener != null) {
mListener.onControllerAnalogInput(controllerId, PAD_R2, rightTrigger);
}
return true;
}
/**
* Check if the input event is from a controller
*/
private boolean isFromController(android.view.InputEvent event) {
InputDevice device = event.getDevice();
if (device == null) {
return false;
}
int sources = device.getSources();
return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
}
/**
* Apply deadzone to analog input
*/
private float applyDeadzone(float value, float deadzone) {
if (Math.abs(value) < deadzone) {
return 0.0f;
}
// Scale the remaining range
float sign = Math.signum(value);
float scaledValue = (Math.abs(value) - deadzone) / (1.0f - deadzone);
return sign * scaledValue;
}
/**
* Check if the PS2 input is an analog stick axis
*/
private boolean isAnalogStickAxis(int ps2Input) {
return ps2Input >= PAD_L_UP && ps2Input <= PAD_R_LEFT;
}
/**
* Convert Android input value to PS2 value range
*/
private int convertToPS2Value(int ps2Input, float value) {
if (ps2Input == PAD_L2 || ps2Input == PAD_R2) {
// Triggers: 0-255 range
return Math.round(value * 255.0f);
} else if (isAnalogStickAxis(ps2Input)) {
// Analog sticks: -32768 to 32767 range
return Math.round(value * 32767.0f);
} else {
// Digital buttons: 0 or 255
return value > 0.5f ? 255 : 0;
}
}
/**
* Get controller name for debugging
*/
public static String getControllerName(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
return device != null ? device.getName() : "Unknown Controller";
}
}
@@ -0,0 +1,165 @@
package com.izzy2lost.psx2;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.List;
/**
* Dialog for testing controller input
*/
public class ControllerTestDialogFragment extends DialogFragment implements ControllerInputHandler.ControllerInputListener {
private TextView mControllerListText;
private TextView mInputLogText;
private StringBuilder mInputLog = new StringBuilder();
private ControllerInputHandler mControllerHandler;
public static ControllerTestDialogFragment newInstance() {
return new ControllerTestDialogFragment();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_controller_test, null);
mControllerListText = view.findViewById(R.id.tv_controller_list);
mInputLogText = view.findViewById(R.id.tv_input_log);
// Initialize controller handler for this dialog
mControllerHandler = new ControllerInputHandler(this);
// Update controller list
updateControllerList();
return new MaterialAlertDialogBuilder(requireContext())
.setTitle("Controller Test")
.setView(view)
.setPositiveButton("Close", null)
.create();
}
private void updateControllerList() {
List<ControllerConfig.ControllerInfo> controllers = ControllerConfig.getConnectedControllers();
if (controllers.isEmpty()) {
mControllerListText.setText("No controllers detected.\n\nMake sure your controller is connected and try pressing a button.");
} else {
StringBuilder sb = new StringBuilder("Connected Controllers:\n\n");
for (ControllerConfig.ControllerInfo controller : controllers) {
sb.append("").append(controller.toString()).append("\n");
}
sb.append("\n").append(ControllerConfig.getButtonMappingDescription());
mControllerListText.setText(sb.toString());
}
}
@Override
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
String buttonName = getButtonName(button);
String action = pressed ? "PRESSED" : "RELEASED";
String logEntry = String.format("Controller %d: %s %s\n", controllerId, buttonName, action);
mInputLog.append(logEntry);
// Keep only last 10 entries
String[] lines = mInputLog.toString().split("\n");
if (lines.length > 10) {
mInputLog = new StringBuilder();
for (int i = lines.length - 10; i < lines.length; i++) {
mInputLog.append(lines[i]).append("\n");
}
}
if (mInputLogText != null) {
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
}
}
@Override
public void onControllerAnalogInput(int controllerId, int axis, float value) {
if (Math.abs(value) > 0.1f) { // Only log significant analog input
String axisName = getAxisName(axis);
String logEntry = String.format("Controller %d: %s %.2f\n", controllerId, axisName, value);
mInputLog.append(logEntry);
// Keep only last 10 entries
String[] lines = mInputLog.toString().split("\n");
if (lines.length > 10) {
mInputLog = new StringBuilder();
for (int i = lines.length - 10; i < lines.length; i++) {
mInputLog.append(lines[i]).append("\n");
}
}
if (mInputLogText != null) {
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
}
}
}
private String getButtonName(int button) {
switch (button) {
case ControllerInputHandler.PAD_CROSS: return "Cross (X)";
case ControllerInputHandler.PAD_CIRCLE: return "Circle (O)";
case ControllerInputHandler.PAD_SQUARE: return "Square (□)";
case ControllerInputHandler.PAD_TRIANGLE: return "Triangle (△)";
case ControllerInputHandler.PAD_L1: return "L1";
case ControllerInputHandler.PAD_R1: return "R1";
case ControllerInputHandler.PAD_L2: return "L2";
case ControllerInputHandler.PAD_R2: return "R2";
case ControllerInputHandler.PAD_L3: return "L3";
case ControllerInputHandler.PAD_R3: return "R3";
case ControllerInputHandler.PAD_SELECT: return "Select";
case ControllerInputHandler.PAD_START: return "Start";
case ControllerInputHandler.PAD_UP: return "D-Pad Up";
case ControllerInputHandler.PAD_DOWN: return "D-Pad Down";
case ControllerInputHandler.PAD_LEFT: return "D-Pad Left";
case ControllerInputHandler.PAD_RIGHT: return "D-Pad Right";
default: return "Button " + button;
}
}
private String getAxisName(int axis) {
switch (axis) {
case ControllerInputHandler.PAD_L_UP: return "Left Stick Up";
case ControllerInputHandler.PAD_L_DOWN: return "Left Stick Down";
case ControllerInputHandler.PAD_L_LEFT: return "Left Stick Left";
case ControllerInputHandler.PAD_L_RIGHT: return "Left Stick Right";
case ControllerInputHandler.PAD_R_UP: return "Right Stick Up";
case ControllerInputHandler.PAD_R_DOWN: return "Right Stick Down";
case ControllerInputHandler.PAD_R_LEFT: return "Right Stick Left";
case ControllerInputHandler.PAD_R_RIGHT: return "Right Stick Right";
default: return "Axis " + axis;
}
}
@Override
public void onControllerCombo(int controllerId, String comboName) {
String logEntry = String.format("Controller %d: COMBO %s\n", controllerId, comboName);
mInputLog.append(logEntry);
// Keep only last 10 entries
String[] lines = mInputLog.toString().split("\n");
if (lines.length > 10) {
mInputLog = new StringBuilder();
for (int i = lines.length - 10; i < lines.length; i++) {
mInputLog.append(lines[i]).append("\n");
}
}
if (mInputLogText != null) {
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
}
}
}
@@ -50,10 +50,11 @@ import org.json.JSONException;
import androidx.fragment.app.FragmentManager;
import java.util.List;
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener {
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener, ControllerInputHandler.ControllerInputListener {
private String m_szGamefile = "";
private HIDDeviceManager mHIDDeviceManager;
private ControllerInputHandler mControllerInputHandler;
private Thread mEmulationThread = null;
private boolean mHudVisible = false;
@@ -345,6 +346,12 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
Initialize();
// Initialize controller input handler
mControllerInputHandler = new ControllerInputHandler(this);
// Log connected controllers for debugging
ControllerConfig.logControllerInfo(this);
makeButtonTouch();
setSurfaceView(new SDLSurface(this));
@@ -1153,6 +1160,8 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
SettingsDialogFragment.loadAndApplySettings(this);
mHIDDeviceManager = HIDDeviceManager.acquire(this);
// Initialize HID device manager for USB and Bluetooth controllers
mHIDDeviceManager.initialize(true, true);
// Apply renderer setting again after a delay to override any automatic detection
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
@@ -1217,38 +1226,40 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (SDLControllerManager.isDeviceSDLJoystick(event.getDeviceId())) {
SDLControllerManager.handleJoystickMotionEvent(event);
// Use only our controller handler - disable SDL fallback to avoid conflicts
if (mControllerInputHandler != null && mControllerInputHandler.handleMotionEvent(event)) {
return true;
}
// Skip SDL controller manager to avoid mapping conflicts
return super.onGenericMotionEvent(event);
}
@Override
public boolean onKeyDown(int p_keyCode, KeyEvent p_event) {
if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
if (p_event.getRepeatCount() == 0) {
SDLControllerManager.onNativePadDown(p_event.getDeviceId(), p_keyCode);
return true;
}
// Use only our controller handler - disable SDL fallback to avoid conflicts
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
return true;
}
else {
if (p_keyCode == KeyEvent.KEYCODE_BACK) {
showExitDialog();
return true;
}
// Handle back button for non-gamepad sources
if (p_keyCode == KeyEvent.KEYCODE_BACK) {
showExitDialog();
return true;
}
// Skip SDL controller manager to avoid mapping conflicts
return super.onKeyDown(p_keyCode, p_event);
}
@Override
public boolean onKeyUp(int p_keyCode, KeyEvent p_event) {
if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
if (p_event.getRepeatCount() == 0) {
SDLControllerManager.onNativePadUp(p_event.getDeviceId(), p_keyCode);
return true;
}
// Use only our controller handler - disable SDL fallback to avoid conflicts
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
return true;
}
// Skip SDL controller manager to avoid mapping conflicts
return super.onKeyUp(p_keyCode, p_event);
}
@@ -1343,4 +1354,104 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
.setNegativeButton("Cancel", null)
.show();
}
// ControllerInputHandler.ControllerInputListener implementation
@Override
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
android.util.Log.d("Controller", "Controller " + controllerId + " button " + button + " (" + getButtonName(button) + ") " + (pressed ? "pressed" : "released"));
// Send controller input to native code using existing setPadButton method
// Range 255 for pressed, 0 for released (matching PS2 button range)
int range = pressed ? 255 : 0;
NativeApp.setPadButton(button, range, pressed);
}
private String getButtonName(int button) {
switch (button) {
case KeyEvent.KEYCODE_BUTTON_A: return "Cross";
case KeyEvent.KEYCODE_BUTTON_B: return "Circle";
case KeyEvent.KEYCODE_BUTTON_X: return "Square";
case KeyEvent.KEYCODE_BUTTON_Y: return "Triangle";
case KeyEvent.KEYCODE_BUTTON_L1: return "L1";
case KeyEvent.KEYCODE_BUTTON_R1: return "R1";
case KeyEvent.KEYCODE_BUTTON_L2: return "L2";
case KeyEvent.KEYCODE_BUTTON_R2: return "R2";
case KeyEvent.KEYCODE_BUTTON_SELECT: return "Select";
case KeyEvent.KEYCODE_BUTTON_START: return "Start";
case KeyEvent.KEYCODE_BUTTON_THUMBL: return "L3";
case KeyEvent.KEYCODE_BUTTON_THUMBR: return "R3";
case KeyEvent.KEYCODE_DPAD_UP: return "D-Up";
case KeyEvent.KEYCODE_DPAD_DOWN: return "D-Down";
case KeyEvent.KEYCODE_DPAD_LEFT: return "D-Left";
case KeyEvent.KEYCODE_DPAD_RIGHT: return "D-Right";
default: return "Unknown(" + button + ")";
}
}
@Override
public void onControllerAnalogInput(int controllerId, int axis, float value) {
android.util.Log.d("Controller", "Controller " + controllerId + " axis " + axis + " (" + getAxisName(axis) + ") value " + value);
// Send analog input to native code using setPadButton
handleAnalogInput(axis, value);
}
@Override
public void onControllerCombo(int controllerId, String comboName) {
android.util.Log.d("Controller", "Controller " + controllerId + " combo: " + comboName);
if ("select_start".equals(comboName)) {
// Show quick actions dialog
runOnUiThread(() -> {
QuickActionsDialogFragment dialog = new QuickActionsDialogFragment();
dialog.show(getSupportFragmentManager(), "quick_actions");
});
}
}
private String getAxisName(int axis) {
switch (axis) {
case 110: return "L-Up";
case 111: return "L-Right";
case 112: return "L-Down";
case 113: return "L-Left";
case 120: return "R-Up";
case 121: return "R-Right";
case 122: return "R-Down";
case 123: return "R-Left";
case KeyEvent.KEYCODE_BUTTON_L2: return "L2-Trigger";
case KeyEvent.KEYCODE_BUTTON_R2: return "R2-Trigger";
default: return "Unknown(" + axis + ")";
}
}
private void handleAnalogInput(int axis, float value) {
// Convert analog input to button presses for the native interface
// This matches how AetherSX2 handles analog input
// For analog sticks, only send positive values (negative values are handled by opposite direction)
int intensity = Math.max(0, Math.round(Math.abs(value) * 255));
boolean pressed = Math.abs(value) > 0.1f;
switch (axis) {
case ControllerInputHandler.PAD_L_UP:
case ControllerInputHandler.PAD_L_DOWN:
case ControllerInputHandler.PAD_L_LEFT:
case ControllerInputHandler.PAD_L_RIGHT:
case ControllerInputHandler.PAD_R_UP:
case ControllerInputHandler.PAD_R_DOWN:
case ControllerInputHandler.PAD_R_LEFT:
case ControllerInputHandler.PAD_R_RIGHT:
// Analog stick directions
NativeApp.setPadButton(axis, intensity, pressed);
break;
case ControllerInputHandler.PAD_L2:
case ControllerInputHandler.PAD_R2:
// Triggers: 0-255 range
NativeApp.setPadButton(axis, Math.round(value * 255), value > 0.1f);
break;
}
}
}
@@ -0,0 +1,101 @@
package com.izzy2lost.psx2;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class QuickActionsDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_quick_actions, null, false);
MaterialButton btnExitToMenu = view.findViewById(R.id.btn_exit_to_menu);
MaterialButton btnRestartGame = view.findViewById(R.id.btn_restart_game);
MaterialButton btnQuitApp = view.findViewById(R.id.btn_quit_app);
MaterialButton btnCancel = view.findViewById(R.id.btn_cancel);
// Exit to Menu - not implemented yet, show placeholder
if (btnExitToMenu != null) {
btnExitToMenu.setOnClickListener(v -> {
new MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setTitle("Exit to Menu")
.setMessage("This feature is not implemented yet. Would you like to quit the app instead?")
.setNegativeButton("Cancel", null)
.setPositiveButton("Quit App", (d, w) -> {
quitApp();
dismissAllowingStateLoss();
})
.show();
});
}
// Restart Game - use existing reboot functionality
if (btnRestartGame != null) {
btnRestartGame.setOnClickListener(v -> {
new MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setTitle("Restart Game")
.setMessage("Restart the current game?")
.setNegativeButton("Cancel", null)
.setPositiveButton("Restart", (d, w) -> {
if (requireActivity() instanceof MainActivity) {
((MainActivity) requireActivity()).rebootEmu();
} else {
NativeApp.shutdown();
}
dismissAllowingStateLoss();
})
.show();
});
}
// Quit App - use existing power functionality
if (btnQuitApp != null) {
btnQuitApp.setOnClickListener(v -> {
new MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setTitle("Quit App")
.setMessage("Quit PSX2?")
.setNegativeButton("Cancel", null)
.setPositiveButton("Quit", (d, w) -> {
quitApp();
dismissAllowingStateLoss();
})
.show();
});
}
// Cancel button
if (btnCancel != null) {
btnCancel.setOnClickListener(v -> dismissAllowingStateLoss());
}
return new MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setView(view)
.create();
}
private void quitApp() {
// Stop emulator first
NativeApp.shutdown();
// Quit the whole app/activity task
if (getActivity() != null) {
getActivity().finishAffinity();
getActivity().finishAndRemoveTask();
}
// As a fallback ensure process exit
System.exit(0);
}
}
@@ -86,6 +86,7 @@ public class SettingsDialogFragment extends DialogFragment {
Switch swDevHud = view.findViewById(R.id.sw_dev_hud);
View btnPower = view.findViewById(R.id.btn_power);
View btnReboot = view.findViewById(R.id.btn_reboot);
View btnTestController = view.findViewById(R.id.btn_test_controller);
// Brand tints for checked/activated states to replace aqua
int brand = ContextCompat.getColor(ctx, R.color.brand_primary);
@@ -180,6 +181,13 @@ public class SettingsDialogFragment extends DialogFragment {
});
}
if (btnTestController != null) {
btnTestController.setOnClickListener(v -> {
ControllerTestDialogFragment controllerDialog = new ControllerTestDialogFragment();
controllerDialog.show(getParentFragmentManager(), "controller_test");
});
}
// Populate scale spinner (1x..8x)
ArrayAdapter<CharSequence> scaleAdapter = ArrayAdapter.createFromResource(ctx,
R.array.scale_entries, android.R.layout.simple_spinner_item);
+4 -5
View File
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>
<path
android:fillColor="@android:color/white"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_controller_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Detecting controllers..."
android:textSize="14sp"
android:paddingBottom="16dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/dividerHorizontal"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tv_input_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Input Log:\n\nPress any button or move analog sticks to test..."
android:textSize="12sp"
android:fontFamily="monospace"
android:background="?android:attr/selectableItemBackground"
android:padding="8dp"
android:minHeight="200dp"/>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quick Actions"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:textColor="?android:attr/textColorPrimary" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_exit_to_menu"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Exit to Menu"
android:textAllCaps="false"
android:layout_marginBottom="8dp"
android:drawableStart="@drawable/ic_home"
android:drawablePadding="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_restart_game"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restart Game"
android:textAllCaps="false"
android:layout_marginBottom="8dp"
android:drawableStart="@drawable/ic_reboot"
android:drawablePadding="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_quit_app"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Quit App"
android:textAllCaps="false"
android:layout_marginBottom="16dp"
android:drawableStart="@drawable/ic_power"
android:drawablePadding="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cancel"
android:textAllCaps="false" />
</LinearLayout>
@@ -214,5 +214,24 @@
android:text="Dev: HUD overlay"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Controller"
android:textStyle="bold"
android:paddingBottom="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_test_controller"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Controller Input"
android:textAllCaps="false"/>
</LinearLayout>
</ScrollView>