diff --git a/app/src/main/assets/resources/game_controller_db.txt b/app/src/main/assets/resources/game_controller_db.txt.disabled similarity index 100% rename from app/src/main/assets/resources/game_controller_db.txt rename to app/src/main/assets/resources/game_controller_db.txt.disabled diff --git a/app/src/main/java/com/izzy2lost/psx2/ControllerConfig.java b/app/src/main/java/com/izzy2lost/psx2/ControllerConfig.java new file mode 100644 index 0000000..b7647cd --- /dev/null +++ b/app/src/main/java/com/izzy2lost/psx2/ControllerConfig.java @@ -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 getConnectedControllers() { + List 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 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/izzy2lost/psx2/ControllerInputHandler.java b/app/src/main/java/com/izzy2lost/psx2/ControllerInputHandler.java new file mode 100644 index 0000000..dff2e44 --- /dev/null +++ b/app/src/main/java/com/izzy2lost/psx2/ControllerInputHandler.java @@ -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 sKeyMapping = new SparseArray<>(); + + // Motion axis mapping from Android MotionEvent to PS2 analog inputs + private static final SparseArray 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/izzy2lost/psx2/ControllerTestDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/ControllerTestDialogFragment.java new file mode 100644 index 0000000..045903e --- /dev/null +++ b/app/src/main/java/com/izzy2lost/psx2/ControllerTestDialogFragment.java @@ -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 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()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java index e624806..4d514ca 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -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; + } + } } diff --git a/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java new file mode 100644 index 0000000..493c185 --- /dev/null +++ b/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java index ca63229..b91f99b 100644 --- a/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java @@ -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 scaleAdapter = ArrayAdapter.createFromResource(ctx, R.array.scale_entries, android.R.layout.simple_spinner_item); diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml index 09c6fe7..5c73f86 100644 --- a/app/src/main/res/drawable/ic_home.xml +++ b/app/src/main/res/drawable/ic_home.xml @@ -1,10 +1,9 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_controller_test.xml b/app/src/main/res/layout/dialog_controller_test.xml new file mode 100644 index 0000000..97693e3 --- /dev/null +++ b/app/src/main/res/layout/dialog_controller_test.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_quick_actions.xml b/app/src/main/res/layout/dialog_quick_actions.xml new file mode 100644 index 0000000..5f37105 --- /dev/null +++ b/app/src/main/res/layout/dialog_quick_actions.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_settings.xml b/app/src/main/res/layout/dialog_settings.xml index 4e89949..7fe9e24 100644 --- a/app/src/main/res/layout/dialog_settings.xml +++ b/app/src/main/res/layout/dialog_settings.xml @@ -214,5 +214,24 @@ android:text="Dev: HUD overlay"/> + + + + + +