Browse Source

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
Sam Lantinga 10 months ago
parent
commit
f879411627

+ 81 - 38
android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java

@@ -9,6 +9,7 @@ import android.content.Context;
 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 class SDLControllerManager
         }
 
         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 class SDLControllerManager
         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 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
                         }
                     }
 
+                    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 @@ class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
     }
 }
 
+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 @@ class SDLHapticHandler {
         }
     }
 
+    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 @@ class SDLHapticHandler {
         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 @@ class SDLHapticHandler {
         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
         }
 

+ 14 - 5
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();

+ 1 - 0
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 */

+ 3 - 24
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)

+ 15 - 2
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)

+ 2 - 1
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;