From f879411627b0c880c98486e1cec8eb1633e419bd Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 5 Jun 2024 09:47:25 -0700
Subject: [PATCH] Added support for gamepad rumble on Android
Tested with the DualSense controller over Bluetooth on Android 12
Fixes https://github.com/libsdl-org/SDL/issues/7847
---
.../org/libsdl/app/SDLControllerManager.java | 119 ++++++++++++------
src/core/android/SDL_android.c | 19 ++-
src/core/android/SDL_android.h | 1 +
src/haptic/android/SDL_syshaptic.c | 27 +---
src/joystick/android/SDL_sysjoystick.c | 17 ++-
src/joystick/android/SDL_sysjoystick_c.h | 3 +-
6 files changed, 116 insertions(+), 70 deletions(-)
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 f0e5540dbe2f8..3a16832c58bcc 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
@@ -9,6 +9,7 @@
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.VibratorManager;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
@@ -24,7 +25,7 @@ public class SDLControllerManager
public static native int nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
int button_mask,
- int naxes, int axis_mask, int nhats);
+ int naxes, int axis_mask, int nhats, boolean can_rumble);
public static native int nativeRemoveJoystick(int device_id);
public static native int nativeAddHaptic(int device_id, String name);
public static native int nativeRemoveHaptic(int device_id);
@@ -50,7 +51,9 @@ public static void initialize() {
}
if (mHapticHandler == null) {
- if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
+ if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
+ mHapticHandler = new SDLHapticHandler_API31();
+ } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
@@ -84,6 +87,13 @@ public static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
+ /**
+ * This method is called by SDL using JNI.
+ */
+ public static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
+ mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length);
+ }
+
/**
* This method is called by SDL using JNI.
*/
@@ -233,10 +243,19 @@ public void pollInputDevices() {
}
}
+ boolean can_rumble = false;
+ if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
+ VibratorManager manager = joystickDevice.getVibratorManager();
+ int[] vibrators = manager.getVibratorIds();
+ if (vibrators.length > 0) {
+ can_rumble = true;
+ }
+ }
+
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice),
- getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2);
+ getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble);
}
}
}
@@ -470,12 +489,63 @@ public int getButtonMask(InputDevice joystickDevice) {
}
}
+class SDLHapticHandler_API31 extends SDLHapticHandler {
+ @Override
+ public void run(int device_id, float intensity, int length) {
+ SDLHaptic haptic = getHaptic(device_id);
+ if (haptic != null) {
+ vibrate(haptic.vib, intensity, length);
+ }
+ }
+
+ @Override
+ public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
+ InputDevice device = InputDevice.getDevice(device_id);
+ if (device == null) {
+ return;
+ }
+
+ VibratorManager manager = device.getVibratorManager();
+ int[] vibrators = manager.getVibratorIds();
+ if (vibrators.length >= 2) {
+ vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
+ vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
+ } else if (vibrators.length == 1) {
+ float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
+ vibrate(manager.getVibrator(vibrators[0]), intensity, length);
+ }
+ }
+
+ private void vibrate(Vibrator vibrator, float intensity, int length) {
+ if (intensity == 0.0f) {
+ vibrator.cancel();
+ return;
+ }
+
+ int value = Math.round(intensity * 255);
+ if (value > 255) {
+ value = 255;
+ }
+ if (value < 1) {
+ vibrator.cancel();
+ return;
+ }
+ try {
+ vibrator.vibrate(VibrationEffect.createOneShot(length, value));
+ }
+ catch (Exception e) {
+ // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
+ // something went horribly wrong with the Android 8.0 APIs.
+ vibrator.vibrate(length);
+ }
+ }
+}
+
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
- Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
if (intensity == 0.0f) {
stop(device_id);
return;
@@ -523,6 +593,10 @@ public void run(int device_id, float intensity, int length) {
}
}
+ public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
+ // Not supported in older APIs
+ }
+
public void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
@@ -535,30 +609,6 @@ public void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
- int[] deviceIds = InputDevice.getDeviceIds();
- // It helps processing the device ids in reverse order
- // For example, in the case of the XBox 360 wireless dongle,
- // so the first controller seen by SDL matches what the receiver
- // considers to be the first controller
-
- for (int i = deviceIds.length - 1; i > -1; i--) {
- SDLHaptic haptic = getHaptic(deviceIds[i]);
- if (haptic == null) {
- InputDevice device = InputDevice.getDevice(deviceIds[i]);
- Vibrator vib = device.getVibrator();
- if (vib != null) {
- if (vib.hasVibrator()) {
- haptic = new SDLHaptic();
- haptic.device_id = deviceIds[i];
- haptic.name = device.getName();
- haptic.vib = vib;
- mHaptics.add(haptic);
- SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
- }
- }
- }
- }
-
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
@@ -581,18 +631,11 @@ public void pollHapticDevices() {
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
- int i;
- for (i = 0; i < deviceIds.length; i++) {
- if (device_id == deviceIds[i]) break;
- }
-
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
- if (i == deviceIds.length) {
- if (removedDevices == null) {
- removedDevices = new ArrayList<Integer>();
- }
- removedDevices.add(device_id);
+ if (removedDevices == null) {
+ removedDevices = new ArrayList<Integer>();
}
+ removedDevices.add(device_id);
} // else: don't remove the vibrator if it is still present
}
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index 484aec94f1b87..70d509654577c 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -274,7 +274,7 @@ JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)(
JNIEXPORT jint JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick)(
JNIEnv *env, jclass jcls,
jint device_id, jstring device_name, jstring device_desc, jint vendor_id, jint product_id,
- jint button_mask, jint naxes, jint axis_mask, jint nhats);
+ jint button_mask, jint naxes, jint axis_mask, jint nhats, jboolean can_rumble);
JNIEXPORT jint JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)(
JNIEnv *env, jclass jcls,
@@ -294,7 +294,7 @@ static JNINativeMethod SDLControllerManager_tab[] = {
{ "onNativePadUp", "(II)I", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) },
{ "onNativeJoy", "(IIF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy) },
{ "onNativeHat", "(IIII)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat) },
- { "nativeAddJoystick", "(ILjava/lang/String;Ljava/lang/String;IIIIII)I", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick) },
+ { "nativeAddJoystick", "(ILjava/lang/String;Ljava/lang/String;IIIIIIZ)I", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick) },
{ "nativeRemoveJoystick", "(I)I", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick) },
{ "nativeAddHaptic", "(ILjava/lang/String;)I", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic) },
{ "nativeRemoveHaptic", "(I)I", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic) }
@@ -378,6 +378,7 @@ static jclass mControllerManagerClass;
static jmethodID midPollInputDevices;
static jmethodID midPollHapticDevices;
static jmethodID midHapticRun;
+static jmethodID midHapticRumble;
static jmethodID midHapticStop;
/* Accelerometer data storage */
@@ -746,10 +747,12 @@ JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)(JNIEnv *env
"pollHapticDevices", "()V");
midHapticRun = (*env)->GetStaticMethodID(env, mControllerManagerClass,
"hapticRun", "(IFI)V");
+ midHapticRumble = (*env)->GetStaticMethodID(env, mControllerManagerClass,
+ "hapticRumble", "(IFFI)V");
midHapticStop = (*env)->GetStaticMethodID(env, mControllerManagerClass,
"hapticStop", "(I)V");
- if (!midPollInputDevices || !midPollHapticDevices || !midHapticRun || !midHapticStop) {
+ if (!midPollInputDevices || !midPollHapticDevices || !midHapticRun || !midHapticRumble || !midHapticStop) {
__android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLControllerManager.java?");
}
@@ -1069,13 +1072,13 @@ JNIEXPORT jint JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick)(
JNIEnv *env, jclass jcls,
jint device_id, jstring device_name, jstring device_desc,
jint vendor_id, jint product_id,
- jint button_mask, jint naxes, jint axis_mask, jint nhats)
+ jint button_mask, jint naxes, jint axis_mask, jint nhats, jboolean can_rumble)
{
int retval;
const char *name = (*env)->GetStringUTFChars(env, device_name, NULL);
const char *desc = (*env)->GetStringUTFChars(env, device_desc, NULL);
- retval = Android_AddJoystick(device_id, name, desc, vendor_id, product_id, button_mask, naxes, axis_mask, nhats);
+ retval = Android_AddJoystick(device_id, name, desc, vendor_id, product_id, button_mask, naxes, axis_mask, nhats, can_rumble);
(*env)->ReleaseStringUTFChars(env, device_name, name);
(*env)->ReleaseStringUTFChars(env, device_desc, desc);
@@ -2201,6 +2204,12 @@ void Android_JNI_HapticRun(int device_id, float intensity, int length)
(*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRun, device_id, intensity, length);
}
+void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length)
+{
+ JNIEnv *env = Android_JNI_GetEnv();
+ (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRumble, device_id, low_frequency_intensity, high_frequency_intensity, length);
+}
+
void Android_JNI_HapticStop(int device_id)
{
JNIEnv *env = Android_JNI_GetEnv();
diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h
index 71704203ede1a..069199e9910a6 100644
--- a/src/core/android/SDL_android.h
+++ b/src/core/android/SDL_android.h
@@ -91,6 +91,7 @@ void Android_JNI_PollInputDevices(void);
/* Haptic support */
void Android_JNI_PollHapticDevices(void);
void Android_JNI_HapticRun(int device_id, float intensity, int length);
+void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length);
void Android_JNI_HapticStop(int device_id);
/* Video */
diff --git a/src/haptic/android/SDL_syshaptic.c b/src/haptic/android/SDL_syshaptic.c
index 3f35004b43a29..0ecb0a0512b2f 100644
--- a/src/haptic/android/SDL_syshaptic.c
+++ b/src/haptic/android/SDL_syshaptic.c
@@ -25,8 +25,6 @@
#include "SDL_syshaptic_c.h"
#include "../SDL_syshaptic.h"
#include "../../core/android/SDL_android.h"
-#include "../../joystick/SDL_sysjoystick.h" /* For the real SDL_Joystick */
-#include "../../joystick/android/SDL_sysjoystick_c.h" /* For joystick hwdata */
typedef struct SDL_hapticlist_item
{
@@ -78,18 +76,6 @@ static SDL_hapticlist_item *HapticByInstanceID(SDL_HapticID instance_id)
return NULL;
}
-static SDL_hapticlist_item *HapticByDevId(int device_id)
-{
- SDL_hapticlist_item *item;
- for (item = SDL_hapticlist; item; item = item->next) {
- if (device_id == item->device_id) {
- /*SDL_Log("=+=+=+=+=+= HapticByDevId id [%d]", device_id);*/
- return item;
- }
- }
- return NULL;
-}
-
SDL_HapticID SDL_SYS_HapticInstanceID(int index)
{
SDL_hapticlist_item *item = HapticByOrder(index);
@@ -142,11 +128,6 @@ static SDL_hapticlist_item *OpenHapticByInstanceID(SDL_Haptic *haptic, SDL_Hapti
return OpenHaptic(haptic, HapticByInstanceID(instance_id));
}
-static SDL_hapticlist_item *OpenHapticByDevId(SDL_Haptic *haptic, int device_id)
-{
- return OpenHaptic(haptic, HapticByDevId(device_id));
-}
-
int SDL_SYS_HapticOpen(SDL_Haptic *haptic)
{
return OpenHapticByInstanceID(haptic, haptic->instance_id) == NULL ? -1 : 0;
@@ -159,19 +140,17 @@ int SDL_SYS_HapticMouse(void)
int SDL_SYS_JoystickIsHaptic(SDL_Joystick *joystick)
{
- SDL_hapticlist_item *item;
- item = HapticByDevId(((joystick_hwdata *)joystick->hwdata)->device_id);
- return (item) ? 1 : 0;
+ return 0;
}
int SDL_SYS_HapticOpenFromJoystick(SDL_Haptic *haptic, SDL_Joystick *joystick)
{
- return OpenHapticByDevId(haptic, ((joystick_hwdata *)joystick->hwdata)->device_id) == NULL ? -1 : 0;
+ return SDL_Unsupported();
}
int SDL_SYS_JoystickSameHaptic(SDL_Haptic *haptic, SDL_Joystick *joystick)
{
- return ((SDL_hapticlist_item *)haptic->hwdata)->device_id == ((joystick_hwdata *)joystick->hwdata)->device_id ? 1 : 0;
+ return 0;
}
void SDL_SYS_HapticClose(SDL_Haptic *haptic)
diff --git a/src/joystick/android/SDL_sysjoystick.c b/src/joystick/android/SDL_sysjoystick.c
index 117077424ed2d..73ab60409c739 100644
--- a/src/joystick/android/SDL_sysjoystick.c
+++ b/src/joystick/android/SDL_sysjoystick.c
@@ -301,7 +301,7 @@ int Android_OnHat(int device_id, int hat_id, int x, int y)
return -1;
}
-int Android_AddJoystick(int device_id, const char *name, const char *desc, int vendor_id, int product_id, int button_mask, int naxes, int axis_mask, int nhats)
+int Android_AddJoystick(int device_id, const char *name, const char *desc, int vendor_id, int product_id, int button_mask, int naxes, int axis_mask, int nhats, SDL_bool can_rumble)
{
SDL_joylist_item *item;
SDL_JoystickGUID guid;
@@ -372,6 +372,7 @@ int Android_AddJoystick(int device_id, const char *name, const char *desc, int v
}
item->naxes = naxes;
item->nhats = nhats;
+ item->can_rumble = can_rumble;
item->device_instance = SDL_GetNextObjectID();
if (!SDL_joylist_tail) {
SDL_joylist = SDL_joylist_tail = item;
@@ -578,12 +579,24 @@ static int ANDROID_JoystickOpen(SDL_Joystick *joystick, int device_index)
joystick->nbuttons = item->nbuttons;
joystick->naxes = item->naxes;
+ if (item->can_rumble) {
+ SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, SDL_TRUE);
+ }
+
return 0;
}
static int ANDROID_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
{
- return SDL_Unsupported();
+ SDL_joylist_item *item = (SDL_joylist_item *)joystick->hwdata;
+ if (!item->can_rumble) {
+ return SDL_Unsupported();
+ }
+
+ float low_frequency_intensity = (float)low_frequency_rumble / SDL_MAX_UINT16;
+ float high_frequency_intensity = (float)high_frequency_rumble / SDL_MAX_UINT16;
+ Android_JNI_HapticRumble(item->device_id, low_frequency_intensity, high_frequency_intensity, 5000);
+ return 0;
}
static int ANDROID_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
diff --git a/src/joystick/android/SDL_sysjoystick_c.h b/src/joystick/android/SDL_sysjoystick_c.h
index 06424887a6a99..3639911999115 100644
--- a/src/joystick/android/SDL_sysjoystick_c.h
+++ b/src/joystick/android/SDL_sysjoystick_c.h
@@ -32,7 +32,7 @@ extern int Android_OnPadDown(int device_id, int keycode);
extern int Android_OnPadUp(int device_id, int keycode);
extern int Android_OnJoy(int device_id, int axisnum, float value);
extern int Android_OnHat(int device_id, int hat_id, int x, int y);
-extern int Android_AddJoystick(int device_id, const char *name, const char *desc, int vendor_id, int product_id, int button_mask, int naxes, int axis_mask, int nhats);
+extern int Android_AddJoystick(int device_id, const char *name, const char *desc, int vendor_id, int product_id, int button_mask, int naxes, int axis_mask, int nhats, SDL_bool can_rumble);
extern int Android_RemoveJoystick(int device_id);
/* A linked list of available joysticks */
@@ -45,6 +45,7 @@ typedef struct SDL_joylist_item
SDL_Joystick *joystick;
int nbuttons, naxes, nhats;
int dpad_state;
+ SDL_bool can_rumble;
struct SDL_joylist_item *next;
} SDL_joylist_item;