From 3c1636a9584e5ccaecd9371bdf92520ec39cd04e Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Mon, 4 May 2026 20:41:48 +0500 Subject: [PATCH] Fix JoyCon mappings on Android --- .../main/java/org/libsdl/app/SDLActivity.java | 8 +- .../org/libsdl/app/SDLControllerManager.java | 4 +- src/core/android/SDL_android.c | 16 +- src/joystick/SDL_gamepad.c | 185 ++++++++++-------- src/joystick/android/SDL_sysjoystick.c | 40 +++- src/joystick/android/SDL_sysjoystick_c.h | 4 +- 6 files changed, 159 insertions(+), 98 deletions(-) diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index c5777dccc4..0491ccab20 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -1478,14 +1478,16 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and // SOURCE_JOYSTICK, while its key events arrive from the keyboard source // So, retrieve the device itself and check all of its sources - if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { + // + // Echo events (event.getRepeatCount() > 0) should be ignored + if (SDLControllerManager.isDeviceSDLJoystick(deviceId) && event.getRepeatCount() == 0) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode, event.getScanCode())) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode, event.getScanCode())) { return true; } } diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java index 03db25a467..bcdf33233a 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -39,8 +39,8 @@ public class SDLControllerManager static native void nativeRemoveJoystick(int device_id); static native void nativeAddHaptic(int device_id, String name); static native void nativeRemoveHaptic(int device_id); - static public native boolean onNativePadDown(int device_id, int keycode); - static public native boolean onNativePadUp(int device_id, int keycode); + static public native boolean onNativePadDown(int device_id, int keycode, int scancode); + static public native boolean onNativePadUp(int device_id, int keycode, int scancode); static native void onNativeJoy(int device_id, int axis, float value); static native void onNativeHat(int device_id, int hat_id, diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index 1781f703a9..3e24056bd5 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -298,11 +298,11 @@ JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)( JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode); + jint device_id, jint keycode, jint scancode); JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode); + jint device_id, jint keycode, jint scancode); JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( JNIEnv *env, jclass jcls, @@ -336,8 +336,8 @@ JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( static JNINativeMethod SDLControllerManager_tab[] = { { "nativeSetupJNI", "()V", SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI) }, - { "onNativePadDown", "(II)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown) }, - { "onNativePadUp", "(II)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) }, + { "onNativePadDown", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown) }, + { "onNativePadUp", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) }, { "onNativeJoy", "(IIF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy) }, { "onNativeHat", "(IIII)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat) }, { "onNativeJoySensor", "(IIJFFF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor) }, @@ -1159,10 +1159,10 @@ SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice)(JNIEnv *env, jclass jcls, jboo // Paddown JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode) + jint device_id, jint keycode, jint scancode) { #ifdef SDL_JOYSTICK_ANDROID - return Android_OnPadDown(device_id, keycode); + return Android_OnPadDown(device_id, keycode, scancode); #else return false; #endif // SDL_JOYSTICK_ANDROID @@ -1171,10 +1171,10 @@ JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( // Padup JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode) + jint device_id, jint keycode, jint scancode) { #ifdef SDL_JOYSTICK_ANDROID - return Android_OnPadUp(device_id, keycode); + return Android_OnPadUp(device_id, keycode, scancode); #else return false; #endif // SDL_JOYSTICK_ANDROID diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 72d43a5552..71419ed3cd 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -713,91 +713,116 @@ static GamepadMapping_t *SDL_CreateMappingForAndroidGamepad(SDL_GUID guid) char mapping_string[1024]; int button_mask; int axis_mask; + Uint16 vendor, product; + + SDL_strlcpy(mapping_string, "none,", sizeof(mapping_string)); - button_mask = SDL_Swap16LE(*(Uint16 *)(&guid.data[sizeof(guid.data) - 4])); - axis_mask = SDL_Swap16LE(*(Uint16 *)(&guid.data[sizeof(guid.data) - 2])); - if (!button_mask && !axis_mask) { - // Accelerometer, shouldn't have a gamepad mapping - return NULL; - } - if (!(button_mask & face_button_mask)) { - // We don't know what buttons or axes are supported, don't make up a mapping - return NULL; - } + SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL, NULL); + if (vendor == USB_VENDOR_NINTENDO) { + if (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT || product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT) { + // FIXME: Should we have a separate hint for non-HIDAPI JoyCon handling? + // Android doesn't report JoyCon SL/SR presses for some reason, so no horizontal triggers/vertical paddles + if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false)) { + // Vertical mode + if (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT) { + SDL_strlcat(mapping_string, "Nintendo Switch Joy-Con (L),back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:b15,leftx:a0,lefty:a1,misc1:b18,", sizeof(mapping_string)); + } else { + SDL_strlcat(mapping_string, "Nintendo Switch Joy-Con (R),a:b0,b:b1,guide:b5,rightshoulder:b10,rightstick:b8,righttrigger:b16,rightx:a0,righty:a1,start:b6,x:b3,y:b2,", sizeof(mapping_string)); + } + } else { + // Mini gamepad mode + if (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT) { + SDL_strlcat(mapping_string, "Nintendo Switch Joy-Con (L),a:b13,b:b12,guide:b18,leftstick:b7,leftx:a1,lefty:a0~,start:b4,x:b11,y:b14,paddle2:b9,paddle4:b15,", sizeof(mapping_string)); + } else { + SDL_strlcat(mapping_string, "Nintendo Switch Joy-Con (R),a:b1,b:b2,guide:b5,leftstick:b8,leftx:a1~,lefty:a0,start:b6,x:b0,y:b3,paddle1:b10,paddle3:b16,", sizeof(mapping_string)); + } + } + } + } else { + button_mask = SDL_Swap16LE(*(Uint16 *)(&guid.data[sizeof(guid.data) - 4])); + axis_mask = SDL_Swap16LE(*(Uint16 *)(&guid.data[sizeof(guid.data) - 2])); + if (!button_mask && !axis_mask) { + // Accelerometer, shouldn't have a gamepad mapping + return NULL; + } + if (!(button_mask & face_button_mask)) { + // We don't know what buttons or axes are supported, don't make up a mapping + return NULL; + } - SDL_strlcpy(mapping_string, "none,*,", sizeof(mapping_string)); + SDL_strlcpy(mapping_string, "*,", sizeof(mapping_string)); - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_SOUTH)) { - SDL_strlcat(mapping_string, "a:b0,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_EAST)) { - SDL_strlcat(mapping_string, "b:b1,", sizeof(mapping_string)); - } else if (button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) { - // Use the back button as "B" for easy UI navigation with TV remotes - SDL_strlcat(mapping_string, "b:b4,", sizeof(mapping_string)); - button_mask &= ~(1 << SDL_GAMEPAD_BUTTON_BACK); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_WEST)) { - SDL_strlcat(mapping_string, "x:b2,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_NORTH)) { - SDL_strlcat(mapping_string, "y:b3,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) { - SDL_strlcat(mapping_string, "back:b4,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_GUIDE)) { - // The guide button generally isn't functional (or acts as a home button) on most Android gamepads before Android 11 - if (SDL_GetAndroidSDKVersion() >= 30 /* Android 11 */) { - SDL_strlcat(mapping_string, "guide:b5,", sizeof(mapping_string)); + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_SOUTH)) { + SDL_strlcat(mapping_string, "a:b0,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_EAST)) { + SDL_strlcat(mapping_string, "b:b1,", sizeof(mapping_string)); + } else if (button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) { + // Use the back button as "B" for easy UI navigation with TV remotes + SDL_strlcat(mapping_string, "b:b4,", sizeof(mapping_string)); + button_mask &= ~(1 << SDL_GAMEPAD_BUTTON_BACK); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_WEST)) { + SDL_strlcat(mapping_string, "x:b2,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_NORTH)) { + SDL_strlcat(mapping_string, "y:b3,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) { + SDL_strlcat(mapping_string, "back:b4,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_GUIDE)) { + // The guide button generally isn't functional (or acts as a home button) on most Android gamepads before Android 11 + if (SDL_GetAndroidSDKVersion() >= 30 /* Android 11 */) { + SDL_strlcat(mapping_string, "guide:b5,", sizeof(mapping_string)); + } + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { + SDL_strlcat(mapping_string, "start:b6,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK)) { + SDL_strlcat(mapping_string, "leftstick:b7,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK)) { + SDL_strlcat(mapping_string, "rightstick:b8,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) { + SDL_strlcat(mapping_string, "leftshoulder:b9,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) { + SDL_strlcat(mapping_string, "rightshoulder:b10,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_UP)) { + SDL_strlcat(mapping_string, "dpup:b11,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { + SDL_strlcat(mapping_string, "dpdown:b12,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { + SDL_strlcat(mapping_string, "dpleft:b13,", sizeof(mapping_string)); + } + if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { + SDL_strlcat(mapping_string, "dpright:b14,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFTX)) { + SDL_strlcat(mapping_string, "leftx:a0,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFTY)) { + SDL_strlcat(mapping_string, "lefty:a1,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHTX)) { + SDL_strlcat(mapping_string, "rightx:a2,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHTY)) { + SDL_strlcat(mapping_string, "righty:a3,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) { + SDL_strlcat(mapping_string, "lefttrigger:a4,", sizeof(mapping_string)); + } + if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) { + SDL_strlcat(mapping_string, "righttrigger:a5,", sizeof(mapping_string)); } } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { - SDL_strlcat(mapping_string, "start:b6,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK)) { - SDL_strlcat(mapping_string, "leftstick:b7,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK)) { - SDL_strlcat(mapping_string, "rightstick:b8,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) { - SDL_strlcat(mapping_string, "leftshoulder:b9,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) { - SDL_strlcat(mapping_string, "rightshoulder:b10,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_UP)) { - SDL_strlcat(mapping_string, "dpup:b11,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { - SDL_strlcat(mapping_string, "dpdown:b12,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { - SDL_strlcat(mapping_string, "dpleft:b13,", sizeof(mapping_string)); - } - if (button_mask & (1 << SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { - SDL_strlcat(mapping_string, "dpright:b14,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFTX)) { - SDL_strlcat(mapping_string, "leftx:a0,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFTY)) { - SDL_strlcat(mapping_string, "lefty:a1,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHTX)) { - SDL_strlcat(mapping_string, "rightx:a2,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHTY)) { - SDL_strlcat(mapping_string, "righty:a3,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) { - SDL_strlcat(mapping_string, "lefttrigger:a4,", sizeof(mapping_string)); - } - if (axis_mask & (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) { - SDL_strlcat(mapping_string, "righttrigger:a5,", sizeof(mapping_string)); - } - return SDL_PrivateAddMappingForGUID(guid, mapping_string, &existing, SDL_GAMEPAD_MAPPING_PRIORITY_DEFAULT); } #endif // SDL_PLATFORM_ANDROID diff --git a/src/joystick/android/SDL_sysjoystick.c b/src/joystick/android/SDL_sysjoystick.c index 671445e5bc..265c32e391 100644 --- a/src/joystick/android/SDL_sysjoystick.c +++ b/src/joystick/android/SDL_sysjoystick.c @@ -29,8 +29,10 @@ #include "../../events/SDL_keyboard_c.h" #include "../../core/android/SDL_android.h" #include "../hidapi/SDL_hidapijoystick_c.h" +#include "../usb_ids.h" #include "android/keycodes.h" +#include // As of platform android-14, android/keycodes.h is missing these defines #ifndef AKEYCODE_BUTTON_1 @@ -170,6 +172,31 @@ static int keycode_to_SDL(int keycode) return button; } +static int scancode_to_SDL(int scancode) +{ + int button = 0; + switch (scancode) { + // D-Pad buttons on the left JoyCon + case BTN_DPAD_UP: + button = SDL_GAMEPAD_BUTTON_DPAD_UP; + break; + case BTN_DPAD_DOWN: + button = SDL_GAMEPAD_BUTTON_DPAD_DOWN; + break; + case BTN_DPAD_LEFT: + button = SDL_GAMEPAD_BUTTON_DPAD_LEFT; + break; + case BTN_DPAD_RIGHT: + button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT; + break; + + default: + return -1; + } + SDL_assert(button < ANDROID_MAX_NBUTTONS); + return button; +} + static SDL_Scancode button_to_scancode(int button) { switch (button) { @@ -195,11 +222,14 @@ static SDL_Scancode button_to_scancode(int button) return SDL_SCANCODE_UNKNOWN; } -bool Android_OnPadDown(int device_id, int keycode) +bool Android_OnPadDown(int device_id, int keycode, int scancode) { Uint64 timestamp = SDL_GetTicksNS(); SDL_joylist_item *item; int button = keycode_to_SDL(keycode); + if (button < 0) { + button = scancode_to_SDL(scancode); + } if (button >= 0) { SDL_LockJoysticks(); item = JoystickByDeviceId(device_id); @@ -215,11 +245,14 @@ bool Android_OnPadDown(int device_id, int keycode) return false; } -bool Android_OnPadUp(int device_id, int keycode) +bool Android_OnPadUp(int device_id, int keycode, int scancode) { Uint64 timestamp = SDL_GetTicksNS(); SDL_joylist_item *item; int button = keycode_to_SDL(keycode); + if (button < 0) { + button = scancode_to_SDL(scancode); + } if (button >= 0) { SDL_LockJoysticks(); item = JoystickByDeviceId(device_id); @@ -367,8 +400,9 @@ void Android_AddJoystick(int device_id, const char *name, const char *desc, int SDL_Log("Joystick: %s, descriptor %s, vendor = 0x%.4x, product = 0x%.4x, %d axes, %d hats", name, desc, vendor_id, product_id, naxes, nhats); #endif - if (nhats > 0) { + if (nhats > 0 || (vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT)) { // Hat is translated into DPAD buttons + // D-Pad on the left JoyCon is a special case, it's not recognized via keycodes or hats, only scancodes button_mask |= ((1 << SDL_GAMEPAD_BUTTON_DPAD_UP) | (1 << SDL_GAMEPAD_BUTTON_DPAD_DOWN) | (1 << SDL_GAMEPAD_BUTTON_DPAD_LEFT) | diff --git a/src/joystick/android/SDL_sysjoystick_c.h b/src/joystick/android/SDL_sysjoystick_c.h index cd00380f75..f99abb5e61 100644 --- a/src/joystick/android/SDL_sysjoystick_c.h +++ b/src/joystick/android/SDL_sysjoystick_c.h @@ -28,8 +28,8 @@ #include "../SDL_sysjoystick.h" -extern bool Android_OnPadDown(int device_id, int keycode); -extern bool Android_OnPadUp(int device_id, int keycode); +extern bool Android_OnPadDown(int device_id, int keycode, int scancode); +extern bool Android_OnPadUp(int device_id, int keycode, int scancode); extern bool Android_OnJoy(int device_id, int axisnum, float value); extern bool Android_OnHat(int device_id, int hat_id, int x, int y); extern void Android_OnJoySensor(int device_id, int sensor_type, Uint64 sensor_timestamp, float x, float y, float z);