From a1c5f4180743a22cc19d6755da7efa94f14cc117 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Mon, 9 Feb 2026 19:29:00 -0500 Subject: [PATCH] beginning of touch controls --- android/ON_SCREEN_CONTROLLER.md | 268 ++++++++++++ android/app/src/main/cpp/vmstate_if_android.c | 4 + .../app/src/main/cpp/xemu_settings_android.cc | 118 ++++- .../izzy2lost/x1box/ControllerInputBridge.kt | 105 +++++ .../com/izzy2lost/x1box/ControllerSettings.kt | 34 ++ .../izzy2lost/x1box/GameLibraryActivity.kt | 39 +- .../java/com/izzy2lost/x1box/MainActivity.kt | 189 +++++++- .../com/izzy2lost/x1box/OnScreenController.kt | 407 ++++++++++++++++++ .../src/main/res/layout/item_game_cover.xml | 32 +- hw/xbox/xid-gamepad.c | 8 +- ui/xemu-input.c | 26 +- 11 files changed, 1199 insertions(+), 31 deletions(-) create mode 100644 android/ON_SCREEN_CONTROLLER.md create mode 100644 android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt create mode 100644 android/app/src/main/java/com/izzy2lost/x1box/ControllerSettings.kt create mode 100644 android/app/src/main/java/com/izzy2lost/x1box/OnScreenController.kt diff --git a/android/ON_SCREEN_CONTROLLER.md b/android/ON_SCREEN_CONTROLLER.md new file mode 100644 index 0000000000..7022b64882 --- /dev/null +++ b/android/ON_SCREEN_CONTROLLER.md @@ -0,0 +1,268 @@ +# On-Screen Controller + +This implementation provides a virtual Xbox controller overlay for touchscreen devices. + +## Features + +### Controller Layout +Based on the original Xbox controller, the on-screen controller includes: + +**Face Buttons (Right Side)** +- A Button (Green) - Bottom +- B Button (Red) - Right +- X Button (Blue) - Left +- Y Button (Yellow) - Top + +**D-Pad (Left Side)** +- Up, Down, Left, Right directional buttons + +**Analog Sticks** +- Left Stick - Lower left position +- Right Stick - Lower right position +- Both sticks support 360-degree movement with dead zone +- Click-able (L3/R3 functionality) + +**Shoulder Buttons** +- Left Trigger (LT) - Top left +- Right Trigger (RT) - Top right +- Left Bumper (LB) - Below left trigger +- Right Bumper (RB) - Below right trigger + +**Center Buttons** +- Start Button - Right of center +- Back Button - Left of center + +**Original Xbox Buttons** +- Black Button - Upper right area +- White Button - Upper right area + +## Usage + +### Automatic Behavior + +The on-screen controller is **enabled by default** and will: +- ✅ Show automatically when no physical controller is connected +- ✅ Hide automatically when a physical controller is plugged in +- ✅ Reappear automatically when the physical controller is disconnected + +This provides a seamless experience where users always have a way to control the game. + +### Physical Controller Detection + +The system monitors for: +- USB game controllers +- Bluetooth game controllers +- Any device with `SOURCE_GAMEPAD` or `SOURCE_JOYSTICK` input sources + +When any physical controller is detected, the on-screen controller automatically hides to avoid cluttering the screen. + +### Manual Control + +You can still manually control the on-screen controller if needed: + +```kotlin +// In MainActivity +val mainActivity = this as MainActivity + +// Show controller +mainActivity.showOnScreenController() + +// Hide controller +mainActivity.hideOnScreenController() + +// Toggle visibility +mainActivity.toggleOnScreenController() + +// Force recheck for physical controllers +mainActivity.forceUpdateControllerVisibility() +``` + +### User Preference Override + +Users can override the automatic behavior through settings: + +Controller preferences are managed through `ControllerSettings`: + +```kotlin +val settings = ControllerSettings(context) + +// Enable/disable on-screen controller (default: true) +settings.showOnScreenController = true + +// Adjust opacity (0.0 - 1.0) +settings.controllerOpacity = 0.7f + +// Adjust scale (0.5 - 2.0) +settings.controllerScale = 1.0f + +// Enable/disable vibration feedback +settings.vibrationEnabled = true +``` + +## Implementation Details + +### Automatic Controller Detection + +The `MainActivity` implements `InputManager.InputDeviceListener` to monitor for controller connections: + +1. **On Startup**: Scans for existing connected controllers +2. **On Device Added**: Detects when a controller is plugged in (USB/Bluetooth) +3. **On Device Removed**: Detects when a controller is disconnected +4. **On Device Changed**: Handles configuration changes + +The detection logic identifies game controllers by checking for: +- `InputDevice.SOURCE_GAMEPAD` - Standard gamepad devices +- `InputDevice.SOURCE_JOYSTICK` - Joystick devices + +### Components + +1. **OnScreenController.kt** + - Custom View that renders the controller overlay + - Handles multi-touch input for simultaneous button presses + - Provides visual feedback for button states + - Manages analog stick positioning with dead zones + +2. **ControllerInputBridge.kt** + - Bridges on-screen controller events to SDL input system + - Maps virtual buttons to SDL controller buttons + - Converts analog stick positions to axis events + - Translates to Android KeyEvent codes + +3. **ControllerSettings.kt** + - Manages user preferences for controller behavior + - Persists settings using SharedPreferences + +### Input Mapping + +The controller maps to standard SDL controller inputs: + +| On-Screen Button | SDL Button | KeyEvent | +|-----------------|------------|----------| +| A | SDL_CONTROLLER_BUTTON_A | KEYCODE_BUTTON_A | +| B | SDL_CONTROLLER_BUTTON_B | KEYCODE_BUTTON_B | +| X | SDL_CONTROLLER_BUTTON_X | KEYCODE_BUTTON_X | +| Y | SDL_CONTROLLER_BUTTON_Y | KEYCODE_BUTTON_Y | +| D-Pad Up | SDL_CONTROLLER_BUTTON_DPAD_UP | KEYCODE_DPAD_UP | +| D-Pad Down | SDL_CONTROLLER_BUTTON_DPAD_DOWN | KEYCODE_DPAD_DOWN | +| D-Pad Left | SDL_CONTROLLER_BUTTON_DPAD_LEFT | KEYCODE_DPAD_LEFT | +| D-Pad Right | SDL_CONTROLLER_BUTTON_DPAD_RIGHT | KEYCODE_DPAD_RIGHT | +| Left Bumper | SDL_CONTROLLER_BUTTON_LEFTSHOULDER | KEYCODE_BUTTON_L1 | +| Right Bumper | SDL_CONTROLLER_BUTTON_RIGHTSHOULDER | KEYCODE_BUTTON_R1 | +| Left Trigger | Axis Event | KEYCODE_BUTTON_L2 | +| Right Trigger | Axis Event | KEYCODE_BUTTON_R2 | +| Start | SDL_CONTROLLER_BUTTON_START | KEYCODE_BUTTON_START | +| Back | SDL_CONTROLLER_BUTTON_BACK | KEYCODE_BUTTON_SELECT | +| Left Stick | Axis Events (X/Y) | - | +| Right Stick | Axis Events (X/Y) | - | +| L3 (Left Stick Click) | SDL_CONTROLLER_BUTTON_LEFTSTICK | KEYCODE_BUTTON_THUMBL | +| R3 (Right Stick Click) | SDL_CONTROLLER_BUTTON_RIGHTSTICK | KEYCODE_BUTTON_THUMBR | + +### Analog Stick Behavior + +- **Dead Zone**: 20% of stick radius (configurable) +- **Range**: -1.0 to 1.0 for both X and Y axes +- **Clamping**: Movement is constrained to circular boundary +- **Multi-touch**: Each stick can be controlled independently + +## Customization + +### Adjusting Button Positions + +Edit `initializeControls()` in `OnScreenController.kt` to modify button positions: + +```kotlin +// Example: Move A button +buttons[Button.A] = ButtonState( + PointF(w * 0.85f, h * 0.5f), // x, y as percentage of screen + faceButtonRadius +) +``` + +### Changing Button Colors + +Modify `getButtonColor()` and `getButtonPressedColor()` methods: + +```kotlin +private fun getButtonColor(button: Button): Int { + return when (button) { + Button.A -> Color.argb(150, 100, 200, 100) // ARGB values + // ... other buttons + } +} +``` + +### Adjusting Sizes + +Button and stick sizes are calculated as percentages of screen width: + +```kotlin +val faceButtonRadius = w * 0.04f // 4% of screen width +val stickRadius = w * 0.08f // 8% of screen width +``` + +## Future Enhancements + +Potential improvements: +- Haptic feedback on button press +- Customizable button layouts +- Save/load custom controller configurations +- Opacity and scale adjustments via UI +- Button remapping +- Portrait/landscape specific layouts +- Gesture support (swipe for quick actions) + +## Testing + +To test the on-screen controller: + +1. **Default Behavior** + - Build and run the app on a touchscreen device + - On-screen controller should be visible by default + - Test each button for visual feedback and input + +2. **Physical Controller Detection** + - Connect a USB or Bluetooth game controller + - On-screen controller should automatically hide + - Disconnect the controller + - On-screen controller should automatically reappear + +3. **Multi-touch Testing** + - Test analog sticks for smooth movement + - Test multi-touch (e.g., move both sticks simultaneously) + - Press multiple buttons at once + +4. **Game Integration** + - Verify input is received by the game/emulator + - Test all buttons map correctly to game actions + +## Troubleshooting + +**Controller not visible on startup** +- Check that no physical controller is connected +- Verify the view is added to the layout hierarchy +- Check `ControllerSettings.showOnScreenController` preference + +**Controller doesn't hide when physical controller connected** +- Verify the physical controller is recognized as a gamepad +- Check logcat for InputDevice events +- Call `forceUpdateControllerVisibility()` to manually trigger detection + +**Controller doesn't reappear when physical controller disconnected** +- Check that `InputDeviceListener` is properly registered +- Verify `onInputDeviceRemoved()` is being called +- Check for any exceptions in the listener callbacks + +**Buttons not responding** +- Ensure `ControllerInputBridge` is properly connected +- Check that SDL input system is initialized +- Verify touch events are not being intercepted by other views + +**Analog sticks not working** +- Confirm axis events are being sent to SDL +- Check dead zone settings +- Verify stick position calculations + +**Performance issues** +- Reduce controller opacity +- Optimize `onDraw()` method +- Consider using hardware acceleration diff --git a/android/app/src/main/cpp/vmstate_if_android.c b/android/app/src/main/cpp/vmstate_if_android.c index bef7e5250f..f0527d822e 100644 --- a/android/app/src/main/cpp/vmstate_if_android.c +++ b/android/app/src/main/cpp/vmstate_if_android.c @@ -76,6 +76,10 @@ __attribute__((used)) static void *xemu_android_force_smbus_storage_ref = extern void xemu_android_force_usb_hub_link(void); __attribute__((used)) static void *xemu_android_force_usb_hub_ref = (void *)&xemu_android_force_usb_hub_link; +/* Force-link the Xbox USB gamepad registration unit from the static lib. */ +extern void xemu_android_force_xid_gamepad_link(void); +__attribute__((used)) static void *xemu_android_force_xid_gamepad_ref = + (void *)&xemu_android_force_xid_gamepad_link; #define XEMU_ANDROID_ACCEL_CPU_TYPE "accel-i386-cpu" diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index ead262ae43..be1b6ef08d 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -1,6 +1,7 @@ #include "qemu/osdep.h" #include +#include #include #include #include @@ -337,14 +338,127 @@ void remove_net_nat_forward_ports(unsigned int index) bool xemu_settings_load_gamepad_mapping(const char *guid, GamepadMappings **mapping) { - (void)guid; + if (!mapping) { + return false; + } + *mapping = NULL; + if (!guid || *guid == '\0') { + return false; + } + + unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count; + for (unsigned int i = 0; i < gamepad_mappings_count; ++i) { + GamepadMappings *entry = &g_config.input.gamepad_mappings[i]; + if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) { + continue; + } + + // Preserve old behavior: global vibration off disables rumble. + if (!g_config.input.allow_vibration) { + entry->enable_rumble = false; + } + + *mapping = entry; + return false; + } + + auto apply_default_controller_mapping = [](GamepadMappings *entry) { + entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A; + entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B; + entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X; + entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y; + entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK; + entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE; + entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START; + entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK; + entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK; + entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; + entry->controller_mapping.rshoulder = + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; + entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP; + entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN; + entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT; + entry->controller_mapping.dpad_right = + SDL_CONTROLLER_BUTTON_DPAD_RIGHT; + entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX; + entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY; + entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX; + entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY; + entry->controller_mapping.axis_trigger_left = + SDL_CONTROLLER_AXIS_TRIGGERLEFT; + entry->controller_mapping.axis_trigger_right = + SDL_CONTROLLER_AXIS_TRIGGERRIGHT; + entry->controller_mapping.invert_axis_left_x = false; + entry->controller_mapping.invert_axis_left_y = false; + entry->controller_mapping.invert_axis_right_x = false; + entry->controller_mapping.invert_axis_right_y = false; + }; + + const unsigned int old_count = g_config.input.gamepad_mappings_count; + const unsigned int new_count = old_count + 1; + GamepadMappings *new_mappings = static_cast(realloc( + g_config.input.gamepad_mappings, sizeof(GamepadMappings) * new_count)); + if (!new_mappings) { + __android_log_print(ANDROID_LOG_ERROR, "xemu-android", + "Failed to allocate gamepad mapping for %s", guid); + return false; + } + + g_config.input.gamepad_mappings = new_mappings; + g_config.input.gamepad_mappings_count = new_count; + + GamepadMappings *entry = &g_config.input.gamepad_mappings[old_count]; + memset(entry, 0, sizeof(*entry)); + entry->gamepad_id = strdup(guid); + entry->enable_rumble = g_config.input.allow_vibration; + apply_default_controller_mapping(entry); + + *mapping = entry; return true; } void xemu_settings_reset_controller_mapping(const char *guid) { - (void)guid; + if (!guid || *guid == '\0') { + return; + } + + unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count; + for (unsigned int i = 0; i < gamepad_mappings_count; ++i) { + GamepadMappings *entry = &g_config.input.gamepad_mappings[i]; + if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) { + continue; + } + + entry->enable_rumble = g_config.input.allow_vibration; + entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A; + entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B; + entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X; + entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y; + entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK; + entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE; + entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START; + entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK; + entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK; + entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; + entry->controller_mapping.rshoulder = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; + entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP; + entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN; + entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT; + entry->controller_mapping.dpad_right = SDL_CONTROLLER_BUTTON_DPAD_RIGHT; + entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX; + entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY; + entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX; + entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY; + entry->controller_mapping.axis_trigger_left = SDL_CONTROLLER_AXIS_TRIGGERLEFT; + entry->controller_mapping.axis_trigger_right = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; + entry->controller_mapping.invert_axis_left_x = false; + entry->controller_mapping.invert_axis_left_y = false; + entry->controller_mapping.invert_axis_right_x = false; + entry->controller_mapping.invert_axis_right_y = false; + return; + } } void xemu_settings_reset_keyboard_mapping(void) diff --git a/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt b/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt new file mode 100644 index 0000000000..e39b9e2d85 --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt @@ -0,0 +1,105 @@ +package com.izzy2lost.x1box + +import android.view.KeyEvent +import org.libsdl.app.SDLControllerManager + +/** + * Bridge between on-screen controller and SDL input system + */ +class ControllerInputBridge : OnScreenController.ControllerListener { + + companion object { + // Virtual device ID for on-screen controller + const val VIRTUAL_DEVICE_ID = -2 + + // Axis indices for SDL + const val AXIS_LEFT_X = 0 + const val AXIS_LEFT_Y = 1 + const val AXIS_RIGHT_X = 2 + const val AXIS_RIGHT_Y = 3 + const val AXIS_LEFT_TRIGGER = 4 + const val AXIS_RIGHT_TRIGGER = 5 + } + + override fun onButtonPressed(button: OnScreenController.Button) { + try { + val keyCode = getKeyCodeForButton(button) + SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode) + } catch (e: Exception) { + android.util.Log.e("ControllerBridge", "Error on button press: ${e.message}") + } + } + + override fun onButtonReleased(button: OnScreenController.Button) { + try { + val keyCode = getKeyCodeForButton(button) + SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode) + } catch (e: Exception) { + android.util.Log.e("ControllerBridge", "Error on button release: ${e.message}") + } + } + + override fun onStickMoved(stick: OnScreenController.Stick, x: Float, y: Float) { + try { + when (stick) { + OnScreenController.Stick.LEFT -> { + SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_X, x) + SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_Y, y) + } + OnScreenController.Stick.RIGHT -> { + SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_X, x) + SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_Y, y) + } + } + } catch (e: Exception) { + android.util.Log.e("ControllerBridge", "Error on stick move: ${e.message}") + } + } + + override fun onStickPressed(stick: OnScreenController.Stick) { + try { + val keyCode = when (stick) { + OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL + OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR + } + SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode) + } catch (e: Exception) { + android.util.Log.e("ControllerBridge", "Error on stick press: ${e.message}") + } + } + + override fun onStickReleased(stick: OnScreenController.Stick) { + try { + val keyCode = when (stick) { + OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL + OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR + } + SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode) + } catch (e: Exception) { + android.util.Log.e("ControllerBridge", "Error on stick release: ${e.message}") + } + } + + private fun getKeyCodeForButton(button: OnScreenController.Button): Int { + return when (button) { + OnScreenController.Button.A -> KeyEvent.KEYCODE_BUTTON_A + OnScreenController.Button.B -> KeyEvent.KEYCODE_BUTTON_B + OnScreenController.Button.X -> KeyEvent.KEYCODE_BUTTON_X + OnScreenController.Button.Y -> KeyEvent.KEYCODE_BUTTON_Y + OnScreenController.Button.DPAD_UP -> KeyEvent.KEYCODE_DPAD_UP + OnScreenController.Button.DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN + OnScreenController.Button.DPAD_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT + OnScreenController.Button.DPAD_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT + OnScreenController.Button.LEFT_BUMPER -> KeyEvent.KEYCODE_BUTTON_L1 + OnScreenController.Button.RIGHT_BUMPER -> KeyEvent.KEYCODE_BUTTON_R1 + OnScreenController.Button.LEFT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_L2 + OnScreenController.Button.RIGHT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_R2 + OnScreenController.Button.START -> KeyEvent.KEYCODE_BUTTON_START + OnScreenController.Button.BACK -> KeyEvent.KEYCODE_BUTTON_SELECT + OnScreenController.Button.LEFT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBL + OnScreenController.Button.RIGHT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBR + OnScreenController.Button.BLACK -> KeyEvent.KEYCODE_BUTTON_L2 + OnScreenController.Button.WHITE -> KeyEvent.KEYCODE_BUTTON_R2 + } + } +} diff --git a/android/app/src/main/java/com/izzy2lost/x1box/ControllerSettings.kt b/android/app/src/main/java/com/izzy2lost/x1box/ControllerSettings.kt new file mode 100644 index 0000000000..165600bbe9 --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/x1box/ControllerSettings.kt @@ -0,0 +1,34 @@ +package com.izzy2lost.x1box + +import android.content.Context +import android.content.SharedPreferences + +class ControllerSettings(context: Context) { + private val prefs: SharedPreferences = context.getSharedPreferences( + "controller_settings", + Context.MODE_PRIVATE + ) + + companion object { + private const val KEY_SHOW_ON_SCREEN_CONTROLLER = "show_on_screen_controller" + private const val KEY_CONTROLLER_OPACITY = "controller_opacity" + private const val KEY_CONTROLLER_SCALE = "controller_scale" + private const val KEY_VIBRATION_ENABLED = "vibration_enabled" + } + + var showOnScreenController: Boolean + get() = prefs.getBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, true) + set(value) = prefs.edit().putBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, value).apply() + + var controllerOpacity: Float + get() = prefs.getFloat(KEY_CONTROLLER_OPACITY, 0.7f) + set(value) = prefs.edit().putFloat(KEY_CONTROLLER_OPACITY, value.coerceIn(0f, 1f)).apply() + + var controllerScale: Float + get() = prefs.getFloat(KEY_CONTROLLER_SCALE, 1.0f) + set(value) = prefs.edit().putFloat(KEY_CONTROLLER_SCALE, value.coerceIn(0.5f, 2.0f)).apply() + + var vibrationEnabled: Boolean + get() = prefs.getBoolean(KEY_VIBRATION_ENABLED, true) + set(value) = prefs.edit().putBoolean(KEY_VIBRATION_ENABLED, value).apply() +} diff --git a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt index 15aa843fbf..bd0721c113 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt @@ -1,6 +1,7 @@ package com.izzy2lost.x1box import android.content.Intent +import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -208,10 +209,13 @@ class GameLibraryActivity : AppCompatActivity() { private fun renderCoverGrid(games: List) { val inflater = LayoutInflater.from(this) var row: LinearLayout? = null + val columns = resolveCoverGridColumns() val spacingPx = dp(8) + val halfSpacingPx = spacingPx / 2 for ((index, game) in games.withIndex()) { - if (index % 2 == 0) { + val columnIndex = index % columns + if (columnIndex == 0) { row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL } @@ -219,7 +223,7 @@ class GameLibraryActivity : AppCompatActivity() { LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) - if (index > 0) { + if (index >= columns) { rowLp.topMargin = dp(12) } gamesGridContainer.addView(row, rowLp) @@ -227,11 +231,8 @@ class GameLibraryActivity : AppCompatActivity() { val item = inflater.inflate(R.layout.item_game_cover, row, false) val itemLp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) - if (index % 2 == 0) { - itemLp.marginEnd = spacingPx - } else { - itemLp.marginStart = spacingPx - } + itemLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx + itemLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx row!!.addView(item, itemLp) val nameText = item.findViewById(R.id.game_cover_name_text) @@ -244,11 +245,25 @@ class GameLibraryActivity : AppCompatActivity() { bindCoverArt(coverImage, game) } - if (games.size % 2 != 0) { - val filler = Space(this) - val fillerLp = LinearLayout.LayoutParams(0, 0, 1f) - fillerLp.marginStart = spacingPx - row?.addView(filler, fillerLp) + val remainder = games.size % columns + if (remainder != 0) { + for (columnIndex in remainder until columns) { + val filler = Space(this) + val fillerLp = LinearLayout.LayoutParams(0, 0, 1f) + fillerLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx + fillerLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx + row?.addView(filler, fillerLp) + } + } + } + + private fun resolveCoverGridColumns(): Int { + val widthDp = resources.configuration.screenWidthDp + val suggested = (widthDp / 180).coerceIn(2, 4) + return if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + maxOf(3, suggested) + } else { + suggested } } diff --git a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt index afb47292d0..cdefcde9c9 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt @@ -1,11 +1,198 @@ package com.izzy2lost.x1box +import android.content.Context +import android.hardware.input.InputManager +import android.os.Build import android.os.Bundle +import android.view.InputDevice +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsetsController +import android.widget.FrameLayout import org.libsdl.app.SDLActivity -class MainActivity : SDLActivity() { +class MainActivity : SDLActivity(), InputManager.InputDeviceListener { + private var onScreenController: OnScreenController? = null + private var controllerBridge: ControllerInputBridge? = null + private var isControllerVisible = false + private var inputManager: InputManager? = null + private var hasPhysicalController = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setupOnScreenController() + setupControllerDetection() + hideSystemUI() + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + hideSystemUI() + } + } + + private fun hideSystemUI() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11 (API 30) and above + window.setDecorFitsSystemWindows(false) + window.insetsController?.let { controller -> + controller.hide(WindowInsets.Type.systemBars()) + controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } else { + // Android 10 and below + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + or View.SYSTEM_UI_FLAG_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + ) + } + } + + private fun setupOnScreenController() { + // Create on-screen controller + onScreenController = OnScreenController(this).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + } + + // Create input bridge + controllerBridge = ControllerInputBridge() + onScreenController?.setControllerListener(controllerBridge!!) + + // Add to layout + mLayout?.addView(onScreenController) + + // Check for existing controllers and show/hide accordingly + updateControllerVisibility() + } + + override fun onResume() { + super.onResume() + + // Register virtual controller after SDL is initialized + // Use a delay to ensure SDL is fully ready + mLayout?.postDelayed({ + registerVirtualController() + }, 1000) + } + + private fun registerVirtualController() { + try { + // Register the virtual on-screen controller as a joystick device + // Device ID: -2, Name: "On-Screen Controller" + org.libsdl.app.SDLControllerManager.nativeAddJoystick( + -2, // device_id + "On-Screen Controller", // name + "Virtual touchscreen controller", // desc + 0x045e, // vendor_id (Microsoft) + 0x028e, // product_id (Xbox 360 Controller) + false, // is_accelerometer + 0xFFFF, // button_mask (all buttons) + 6, // naxes (left X/Y, right X/Y, left trigger, right trigger) + 0x3F, // axis_mask (6 axes) + 0, // nhats + 0 // nballs + ) + android.util.Log.d("MainActivity", "Virtual controller registered successfully") + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Failed to register virtual controller: ${e.message}") + } + } + + private fun setupControllerDetection() { + inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager + inputManager?.registerInputDeviceListener(this, null) + + // Check for already connected controllers + checkForPhysicalControllers() + } + + private fun checkForPhysicalControllers() { + val deviceIds = inputManager?.inputDeviceIds ?: return + hasPhysicalController = deviceIds.any { deviceId -> + val device = inputManager?.getInputDevice(deviceId) + isGameController(device) + } + updateControllerVisibility() + } + + private fun isGameController(device: InputDevice?): Boolean { + if (device == null) return false + + val sources = device.sources + + // Check if device is a gamepad or joystick + return ((sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || + ((sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) + } + + private fun updateControllerVisibility() { + // Show on-screen controller only if no physical controller is connected + val shouldShow = !hasPhysicalController + + if (shouldShow != isControllerVisible) { + isControllerVisible = shouldShow + onScreenController?.visibility = if (shouldShow) View.VISIBLE else View.GONE + } + } + + // InputDeviceListener callbacks + override fun onInputDeviceAdded(deviceId: Int) { + val device = inputManager?.getInputDevice(deviceId) + if (isGameController(device)) { + hasPhysicalController = true + updateControllerVisibility() + } + } + + override fun onInputDeviceRemoved(deviceId: Int) { + // Recheck all devices to see if any controllers remain + checkForPhysicalControllers() + } + + override fun onInputDeviceChanged(deviceId: Int) { + // Recheck all devices in case configuration changed + checkForPhysicalControllers() + } + + override fun onDestroy() { + // Unregister virtual controller + try { + org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2) + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Failed to unregister virtual controller: ${e.message}") + } + + inputManager?.unregisterInputDeviceListener(this) + super.onDestroy() + } + + // Manual control methods (for settings/preferences) + fun toggleOnScreenController() { + isControllerVisible = !isControllerVisible + onScreenController?.visibility = if (isControllerVisible) View.VISIBLE else View.GONE + } + + fun showOnScreenController() { + isControllerVisible = true + onScreenController?.visibility = View.VISIBLE + } + + fun hideOnScreenController() { + isControllerVisible = false + onScreenController?.visibility = View.GONE + } + + fun forceUpdateControllerVisibility() { + checkForPhysicalControllers() } override fun getLibraries(): Array = arrayOf( diff --git a/android/app/src/main/java/com/izzy2lost/x1box/OnScreenController.kt b/android/app/src/main/java/com/izzy2lost/x1box/OnScreenController.kt new file mode 100644 index 0000000000..4cdaec716a --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/x1box/OnScreenController.kt @@ -0,0 +1,407 @@ +package com.izzy2lost.x1box + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PointF +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import kotlin.math.pow +import kotlin.math.sqrt + +class OnScreenController @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val buttons = mutableMapOf() + private val sticks = mutableMapOf() + + private var controllerListener: ControllerListener? = null + + enum class Button { + A, B, X, Y, + DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT, + LEFT_TRIGGER, RIGHT_TRIGGER, + LEFT_BUMPER, RIGHT_BUMPER, + START, BACK, + LEFT_STICK_BUTTON, RIGHT_STICK_BUTTON, + BLACK, WHITE + } + + enum class Stick { + LEFT, RIGHT + } + + data class ButtonState( + val center: PointF, + val radius: Float, + var isPressed: Boolean = false + ) + + data class StickState( + val center: PointF, + val radius: Float, + val deadZone: Float = 0.2f, + var currentPos: PointF = PointF(0f, 0f), + var isPressed: Boolean = false, + var activePointerId: Int = -1 + ) + + interface ControllerListener { + fun onButtonPressed(button: Button) + fun onButtonReleased(button: Button) + fun onStickMoved(stick: Stick, x: Float, y: Float) + fun onStickPressed(stick: Stick) + fun onStickReleased(stick: Stick) + } + + init { + setBackgroundColor(Color.TRANSPARENT) + } + + fun setControllerListener(listener: ControllerListener) { + this.controllerListener = listener + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + initializeControls(w, h) + } + + private fun initializeControls(width: Int, height: Int) { + val w = width.toFloat() + val h = height.toFloat() + + // Button sizes - made D-pad smaller + val faceButtonRadius = w * 0.035f + val dpadButtonRadius = w * 0.025f + val shoulderButtonRadius = w * 0.042f + val smallButtonRadius = w * 0.022f + val stickRadius = w * 0.07f + + // Face buttons (right side) - A, B, X, Y in diamond formation + val faceButtonCenterX = w * 0.88f + val faceButtonCenterY = h * 0.55f + val faceButtonSpacing = w * 0.07f + + buttons[Button.A] = ButtonState( + PointF(faceButtonCenterX, faceButtonCenterY + faceButtonSpacing), + faceButtonRadius + ) + buttons[Button.B] = ButtonState( + PointF(faceButtonCenterX + faceButtonSpacing, faceButtonCenterY), + faceButtonRadius + ) + buttons[Button.X] = ButtonState( + PointF(faceButtonCenterX - faceButtonSpacing, faceButtonCenterY), + faceButtonRadius + ) + buttons[Button.Y] = ButtonState( + PointF(faceButtonCenterX, faceButtonCenterY - faceButtonSpacing), + faceButtonRadius + ) + + // D-Pad (bottom left corner) - smaller buttons + val dpadCenterX = w * 0.12f + val dpadCenterY = h * 0.85f + val dpadSpacing = w * 0.045f + + buttons[Button.DPAD_UP] = ButtonState( + PointF(dpadCenterX, dpadCenterY - dpadSpacing), + dpadButtonRadius + ) + buttons[Button.DPAD_DOWN] = ButtonState( + PointF(dpadCenterX, dpadCenterY + dpadSpacing), + dpadButtonRadius + ) + buttons[Button.DPAD_LEFT] = ButtonState( + PointF(dpadCenterX - dpadSpacing, dpadCenterY), + dpadButtonRadius + ) + buttons[Button.DPAD_RIGHT] = ButtonState( + PointF(dpadCenterX + dpadSpacing, dpadCenterY), + dpadButtonRadius + ) + + // Shoulder buttons (triggers and bumpers) + buttons[Button.LEFT_TRIGGER] = ButtonState( + PointF(w * 0.12f, h * 0.08f), + shoulderButtonRadius + ) + buttons[Button.RIGHT_TRIGGER] = ButtonState( + PointF(w * 0.88f, h * 0.08f), + shoulderButtonRadius + ) + buttons[Button.LEFT_BUMPER] = ButtonState( + PointF(w * 0.12f, h * 0.16f), + shoulderButtonRadius + ) + buttons[Button.RIGHT_BUMPER] = ButtonState( + PointF(w * 0.88f, h * 0.16f), + shoulderButtonRadius + ) + + // Center buttons + buttons[Button.START] = ButtonState( + PointF(w * 0.58f, h * 0.4f), + smallButtonRadius + ) + buttons[Button.BACK] = ButtonState( + PointF(w * 0.42f, h * 0.4f), + smallButtonRadius + ) + + // Analog sticks - right stick moved down + sticks[Stick.LEFT] = StickState( + PointF(w * 0.18f, h * 0.45f), + stickRadius + ) + sticks[Stick.RIGHT] = StickState( + PointF(w * 0.62f, h * 0.82f), + stickRadius + ) + + // Black and White buttons - moved to right of right analog stick + buttons[Button.WHITE] = ButtonState( + PointF(w * 0.75f, h * 0.78f), + smallButtonRadius + ) + buttons[Button.BLACK] = ButtonState( + PointF(w * 0.75f, h * 0.86f), + smallButtonRadius + ) + + // Stick buttons + buttons[Button.LEFT_STICK_BUTTON] = ButtonState( + sticks[Stick.LEFT]!!.center, + stickRadius * 0.3f + ) + buttons[Button.RIGHT_STICK_BUTTON] = ButtonState( + sticks[Stick.RIGHT]!!.center, + stickRadius * 0.3f + ) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw analog sticks + sticks.forEach { (stick, state) -> + // Outer circle + paint.style = Paint.Style.STROKE + paint.strokeWidth = 4f + paint.color = Color.argb(100, 255, 255, 255) + canvas.drawCircle(state.center.x, state.center.y, state.radius, paint) + + // Dead zone circle + paint.color = Color.argb(50, 255, 255, 255) + canvas.drawCircle(state.center.x, state.center.y, state.radius * state.deadZone, paint) + + // Stick position + val stickX = state.center.x + state.currentPos.x * state.radius + val stickY = state.center.y + state.currentPos.y * state.radius + + paint.style = Paint.Style.FILL + paint.color = if (state.isPressed) { + Color.argb(200, 100, 150, 255) + } else { + Color.argb(150, 200, 200, 200) + } + canvas.drawCircle(stickX, stickY, state.radius * 0.4f, paint) + } + + // Draw buttons + buttons.forEach { (button, state) -> + // Skip stick buttons as they're drawn with sticks + if (button == Button.LEFT_STICK_BUTTON || button == Button.RIGHT_STICK_BUTTON) { + return@forEach + } + + paint.style = Paint.Style.FILL + paint.color = when { + state.isPressed -> getButtonPressedColor(button) + else -> getButtonColor(button) + } + canvas.drawCircle(state.center.x, state.center.y, state.radius, paint) + + // Button outline + paint.style = Paint.Style.STROKE + paint.strokeWidth = 3f + paint.color = Color.argb(150, 255, 255, 255) + canvas.drawCircle(state.center.x, state.center.y, state.radius, paint) + + // Button labels + paint.style = Paint.Style.FILL + paint.color = Color.WHITE + paint.textSize = state.radius * 0.8f + paint.textAlign = Paint.Align.CENTER + val label = getButtonLabel(button) + canvas.drawText(label, state.center.x, state.center.y + state.radius * 0.3f, paint) + } + } + + private fun getButtonColor(button: Button): Int { + return when (button) { + Button.A -> Color.argb(150, 100, 200, 100) + Button.B -> Color.argb(150, 200, 100, 100) + Button.X -> Color.argb(150, 100, 150, 255) + Button.Y -> Color.argb(150, 255, 255, 100) + Button.BLACK -> Color.argb(150, 50, 50, 50) + Button.WHITE -> Color.argb(150, 220, 220, 220) + else -> Color.argb(120, 150, 150, 150) + } + } + + private fun getButtonPressedColor(button: Button): Int { + return when (button) { + Button.A -> Color.argb(255, 100, 255, 100) + Button.B -> Color.argb(255, 255, 100, 100) + Button.X -> Color.argb(255, 100, 150, 255) + Button.Y -> Color.argb(255, 255, 255, 100) + Button.BLACK -> Color.argb(255, 80, 80, 80) + Button.WHITE -> Color.argb(255, 255, 255, 255) + else -> Color.argb(200, 200, 200, 200) + } + } + + private fun getButtonLabel(button: Button): String { + return when (button) { + Button.A -> "A" + Button.B -> "B" + Button.X -> "X" + Button.Y -> "Y" + Button.DPAD_UP -> "↑" + Button.DPAD_DOWN -> "↓" + Button.DPAD_LEFT -> "←" + Button.DPAD_RIGHT -> "→" + Button.LEFT_TRIGGER -> "LT" + Button.RIGHT_TRIGGER -> "RT" + Button.LEFT_BUMPER -> "LB" + Button.RIGHT_BUMPER -> "RB" + Button.START -> "▶" + Button.BACK -> "◀" + Button.BLACK -> "BK" + Button.WHITE -> "WH" + else -> "" + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val pointerIndex = event.actionIndex + val pointerId = event.getPointerId(pointerIndex) + val x = event.getX(pointerIndex) + val y = event.getY(pointerIndex) + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + handleTouchDown(x, y, pointerId) + } + MotionEvent.ACTION_MOVE -> { + for (i in 0 until event.pointerCount) { + handleTouchMove( + event.getX(i), + event.getY(i), + event.getPointerId(i) + ) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> { + handleTouchUp(pointerId) + } + } + + invalidate() + return true + } + + private fun handleTouchDown(x: Float, y: Float, pointerId: Int) { + // Check sticks first + sticks.forEach { (stick, state) -> + if (state.activePointerId == -1 && isPointInCircle(x, y, state.center, state.radius)) { + state.activePointerId = pointerId + updateStickPosition(stick, state, x, y) + return + } + } + + // Check buttons + buttons.forEach { (button, state) -> + if (isPointInCircle(x, y, state.center, state.radius)) { + state.isPressed = true + controllerListener?.onButtonPressed(button) + return + } + } + } + + private fun handleTouchMove(x: Float, y: Float, pointerId: Int) { + sticks.forEach { (stick, state) -> + if (state.activePointerId == pointerId) { + updateStickPosition(stick, state, x, y) + } + } + } + + private fun handleTouchUp(pointerId: Int) { + // Release sticks + sticks.forEach { (stick, state) -> + if (state.activePointerId == pointerId) { + state.activePointerId = -1 + state.currentPos = PointF(0f, 0f) + controllerListener?.onStickMoved(stick, 0f, 0f) + if (state.isPressed) { + state.isPressed = false + controllerListener?.onStickReleased(stick) + } + } + } + + // Release buttons + buttons.forEach { (button, state) -> + if (state.isPressed) { + state.isPressed = false + controllerListener?.onButtonReleased(button) + } + } + } + + private fun updateStickPosition(stick: Stick, state: StickState, x: Float, y: Float) { + val dx = x - state.center.x + val dy = y - state.center.y + val distance = sqrt(dx.pow(2) + dy.pow(2)) + + if (distance > state.radius) { + // Clamp to circle boundary + state.currentPos.x = (dx / distance) + state.currentPos.y = (dy / distance) + } else { + // Normalize to -1..1 range + state.currentPos.x = dx / state.radius + state.currentPos.y = dy / state.radius + } + + // Apply dead zone + val magnitude = sqrt(state.currentPos.x.pow(2) + state.currentPos.y.pow(2)) + if (magnitude < state.deadZone) { + state.currentPos.x = 0f + state.currentPos.y = 0f + } + + controllerListener?.onStickMoved(stick, state.currentPos.x, state.currentPos.y) + } + + private fun isPointInCircle(x: Float, y: Float, center: PointF, radius: Float): Boolean { + val dx = x - center.x + val dy = y - center.y + return sqrt(dx.pow(2) + dy.pow(2)) <= radius + } + + fun setVisibility(visible: Boolean) { + visibility = if (visible) View.VISIBLE else View.GONE + } +} diff --git a/android/app/src/main/res/layout/item_game_cover.xml b/android/app/src/main/res/layout/item_game_cover.xml index 28c967d338..ea3ed70a89 100644 --- a/android/app/src/main/res/layout/item_game_cover.xml +++ b/android/app/src/main/res/layout/item_game_cover.xml @@ -12,38 +12,48 @@ app:strokeColor="@color/xemu_outline" app:strokeWidth="1dp"> - + android:scaleType="fitCenter" + android:src="@android:drawable/ic_menu_report_image" + app:layout_constraintDimensionRatio="2:3" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:textAppearance="@style/TextAppearance.Material3.TitleSmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/game_cover_image" /> - + android:textColor="@color/xemu_text_muted" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/game_cover_name_text" /> + diff --git a/hw/xbox/xid-gamepad.c b/hw/xbox/xid-gamepad.c index 65bb5b4984..319a957443 100644 --- a/hw/xbox/xid-gamepad.c +++ b/hw/xbox/xid-gamepad.c @@ -290,4 +290,10 @@ static void usb_xid_register_types(void) type_register_static(&usb_xbox_gamepad_s_info); } -type_init(usb_xid_register_types) \ No newline at end of file +type_init(usb_xid_register_types) + +#ifdef __ANDROID__ +void xemu_android_force_xid_gamepad_link(void) +{ +} +#endif diff --git a/ui/xemu-input.c b/ui/xemu-input.c index fde1ecb5da..5cfe345636 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -167,6 +167,10 @@ static void check_and_reset_in_range(int *btn, int min, int max, static void xemu_input_bindings_set_in_range(ControllerState *con) { + if (!con->controller_map) { + return; + } + #define CHECK_RESET_BUTTON(btn) \ check_and_reset_in_range(&con->controller_map->controller_mapping.btn, \ SDL_CONTROLLER_BUTTON_INVALID, \ @@ -213,7 +217,14 @@ static void xemu_input_bindings_reload_map(ControllerState *con) char guid[35] = { 0 }; SDL_JoystickGetGUIDString(con->sdl_joystick_guid, guid, sizeof(guid)); - if (!xemu_settings_load_gamepad_mapping(guid, &con->controller_map)) { + bool added_mapping = + xemu_settings_load_gamepad_mapping(guid, &con->controller_map); + if (!con->controller_map) { + fprintf(stderr, "Failed to load gamepad mapping for %s\n", guid); + return; + } + + if (!added_mapping) { return; } @@ -221,7 +232,7 @@ static void xemu_input_bindings_reload_map(ControllerState *con) // have been reallocated. Any gamepad mapping pointers for other controllers // are now invalid, and need to be reloaded. ControllerState *iter, *next; - bool is_new_mapping; + bool iter_is_new_mapping; QTAILQ_FOREACH_SAFE (iter, &available_controllers, entry, next) { if (iter == con || iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) { continue; @@ -230,9 +241,9 @@ static void xemu_input_bindings_reload_map(ControllerState *con) memset(guid, 0, sizeof(guid)); SDL_JoystickGetGUIDString(iter->sdl_joystick_guid, guid, sizeof(guid)); - is_new_mapping = + iter_is_new_mapping = xemu_settings_load_gamepad_mapping(guid, &iter->controller_map); - assert(!is_new_mapping && + assert(!iter_is_new_mapping && "Existing controller GUIDs should exist in the config"); xemu_input_bindings_set_in_range(iter); @@ -566,6 +577,9 @@ void xemu_input_update_sdl_controller_state(ControllerState *state) { state->buttons = 0; memset(state->axis, 0, sizeof(state->axis)); + if (!state->controller_map) { + return; + } #define SDL_MASK_BUTTON(state, btn, idx) \ (SDL_GameControllerGetButton( \ @@ -636,6 +650,10 @@ void xemu_input_update_rumble(ControllerState *state) return; } + if (!state->controller_map) { + return; + } + if (!state->controller_map->enable_rumble) { return; }