diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index 982147306..51d37c29f 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -2,18 +2,15 @@ package com.flycast.emulator.emu; import android.content.Context; import android.content.res.Configuration; -import android.os.Build; import android.os.Handler; -import android.os.VibrationEffect; -import android.os.Vibrator; import android.view.InputDevice; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; -import com.flycast.emulator.Emulator; import com.flycast.emulator.periph.InputDeviceManager; import com.flycast.emulator.periph.VJoy; +import com.flycast.emulator.periph.VibratorThread; public class VirtualJoystickDelegate { private VibratorThread vibratorThread; @@ -41,19 +38,15 @@ public class VirtualJoystickDelegate { this.view = view; this.context = view.getContext(); - vibratorThread = new VibratorThread(context); - vibratorThread.start(); + vibratorThread = VibratorThread.getInstance(); readCustomVjoyValues(); scaleGestureDetector = new ScaleGestureDetector(context, new OscOnScaleGestureListener()); } public void stop() { - vibratorThread.stopVibrator(); - try { - vibratorThread.join(); - } catch (InterruptedException e) { - } + vibratorThread.stopThread(); + vibratorThread = null; } public void readCustomVjoyValues() { @@ -231,7 +224,7 @@ public class VirtualJoystickDelegate { // Not for analog if (vjoy[j][5] == 0) if (!editVjoyMode) { - vibratorThread.vibrate(); + vibratorThread.click(); } vjoy[j][5] = 2; } @@ -406,56 +399,4 @@ public class VirtualJoystickDelegate { selectedVjoyElement = -1; } } - - private class VibratorThread extends Thread - { - private Vibrator vibrator; - private boolean vibrate = false; - private boolean stopping = false; - - VibratorThread(Context context) { - vibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); - } - - @Override - public void run() { - while (!stopping) { - boolean doVibrate; - synchronized (this) { - doVibrate = false; - try { - this.wait(); - } catch (InterruptedException e) { - } - if (vibrate) { - doVibrate = true; - vibrate = false; - } - } - if (doVibrate) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(Emulator.vibrationDuration, VibrationEffect.DEFAULT_AMPLITUDE)); - } else { - vibrator.vibrate(Emulator.vibrationDuration); - } - } - } - } - - public void stopVibrator() { - synchronized (this) { - stopping = true; - notify(); - } - } - - public void vibrate() { - if (Emulator.vibrationDuration > 0) { - synchronized (this) { - vibrate = true; - notify(); - } - } - } - } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java index 305597141..f9a2bd95f 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java @@ -10,7 +10,10 @@ import android.view.InputDevice; import com.flycast.emulator.Emulator; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + import org.apache.commons.lang3.ArrayUtils; public final class InputDeviceManager implements InputManager.InputDeviceListener { @@ -21,6 +24,13 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene private InputManager inputManager; private int maple_port = 0; + private static class VibrationParams { + float power; + float inclination; + long stopTime; + } + private Map vibParams = new HashMap<>(); + public InputDeviceManager() { init(); @@ -30,7 +40,8 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene { maple_port = 0; if (applicationContext.getPackageManager().hasSystemFeature("android.hardware.touchscreen")) - joystickAdded(VIRTUAL_GAMEPAD_ID, "Virtual Gamepad", 0, "virtual_gamepad_uid", new int[0], new int[0]); + joystickAdded(VIRTUAL_GAMEPAD_ID, "Virtual Gamepad", 0, "virtual_gamepad_uid", + new int[0], new int[0], getVibrator(VIRTUAL_GAMEPAD_ID) != null); int[] ids = InputDevice.getDeviceIds(); for (int id : ids) onInputDeviceAdded(id); @@ -65,7 +76,8 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene fullAxes.add(range.getAxis()); } joystickAdded(i, device.getName(), port, device.getDescriptor(), - ArrayUtils.toPrimitive(fullAxes.toArray(new Integer[0])), ArrayUtils.toPrimitive(halfAxes.toArray(new Integer[0]))); + ArrayUtils.toPrimitive(fullAxes.toArray(new Integer[0])), ArrayUtils.toPrimitive(halfAxes.toArray(new Integer[0])), + getVibrator(i) != null); } } @@ -80,34 +92,107 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene public void onInputDeviceChanged(int i) { } - // Called from native code - private boolean rumble(int i, float power, float inclination, int duration_ms) { - Vibrator vibrator; + private Vibrator getVibrator(int i) { if (i == VIRTUAL_GAMEPAD_ID) { - vibrator = (Vibrator)Emulator.getAppContext().getSystemService(Context.VIBRATOR_SERVICE); + return (Vibrator)Emulator.getAppContext().getSystemService(Context.VIBRATOR_SERVICE); } else { InputDevice device = InputDevice.getDevice(i); if (device == null) - return false; - vibrator = device.getVibrator(); - if (!vibrator.hasVibrator()) - return false; + return null; + Vibrator vibrator = device.getVibrator(); + return vibrator.hasVibrator() ? vibrator : null; } + } - if (power == 0) { - vibrator.cancel(); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(duration_ms, VibrationEffect.DEFAULT_AMPLITUDE)); - } else { - vibrator.vibrate(duration_ms); + // Called from native code + private boolean rumble(int i, float power, float inclination, int duration_ms) + { + Vibrator vibrator = getVibrator(i); + if (vibrator == null) + return false; + + VibrationParams params; + synchronized (this) { + params = vibParams.get(i); + if (params == null) { + params = new VibrationParams(); + vibParams.put(i, params); } } + if (power == 0) { + if (params.power != 0) + vibrator.cancel(); + } else { + if (inclination > 0) { + params.inclination = inclination * power; + params.stopTime = System.currentTimeMillis() + duration_ms; + } + else { + params.inclination = 0; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate(VibrationEffect.createOneShot(duration_ms, (int)(power * 255))); + else + vibrator.vibrate(duration_ms); + if (inclination > 0) + VibratorThread.getInstance().setVibrating(); + } + params.power = power; return true; } + public boolean updateRumble() + { + List ids; + synchronized (this) { + ids = new ArrayList(vibParams.keySet()); + } + boolean active = false; + for (int id : ids) { + if (updateRumble(id)) + active = true; + } + return active; + } + + private boolean updateRumble(int i) + { + Vibrator vibrator = getVibrator(i); + VibrationParams params; + synchronized (this) { + params = vibParams.get(i); + } + if (vibrator == null || params == null || params.power == 0 || params.inclination == 0) + return false; + long remTime = params.stopTime - System.currentTimeMillis(); + if (remTime <= 0) { + params.power = 0; + params.inclination = 0; + vibrator.cancel(); + return false; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate(VibrationEffect.createOneShot(remTime, (int)(params.inclination * remTime * 255))); + else + vibrator.vibrate(remTime); + return true; + } + + public void stopRumble() + { + List ids; + synchronized (this) { + ids = new ArrayList(vibParams.keySet()); + } + for (int id : ids) { + Vibrator vibrator = getVibrator(id); + if (vibrator != null) + vibrator.cancel(); + } + } + public static InputDeviceManager getInstance() { return INSTANCE; } @@ -118,7 +203,7 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene public native boolean joystickAxisEvent(int id, int button, int value); public native void mouseEvent(int xpos, int ypos, int buttons); public native void mouseScrollEvent(int scrollValue); - private native void joystickAdded(int id, String name, int maple_port, String uniqueId, int[] fullAxes, int[] halfAxes); + private native void joystickAdded(int id, String name, int maple_port, String uniqueId, int[] fullAxes, int[] halfAxes, boolean rumbleEnabled); private native void joystickRemoved(int id); public native boolean keyboardEvent(int key, boolean pressed); public native void keyboardText(int c); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VibratorThread.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VibratorThread.java new file mode 100644 index 000000000..b7e7b1051 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VibratorThread.java @@ -0,0 +1,152 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.periph; + +import android.content.Context; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.view.InputDevice; + +import androidx.annotation.RequiresApi; + +import com.flycast.emulator.Emulator; + +public class VibratorThread extends Thread { + private boolean stopping = false; + private boolean click = false; + private long nextRumbleUpdate = 0; + @RequiresApi(Build.VERSION_CODES.O) + private VibrationEffect clickEffect = null; + int clickDuration = 0; + private static VibratorThread INSTANCE = null; + + public static VibratorThread getInstance() { + synchronized (VibratorThread.class) { + if (INSTANCE == null) + INSTANCE = new VibratorThread(); + } + return INSTANCE; + } + + private VibratorThread() { + start(); + } + + private Vibrator getVibrator(int i) + { + if (i == InputDeviceManager.VIRTUAL_GAMEPAD_ID) { + return (Vibrator) Emulator.getAppContext().getSystemService(Context.VIBRATOR_SERVICE); + } + else { + InputDevice device = InputDevice.getDevice(i); + if (device == null) + return null; + Vibrator vibrator = device.getVibrator(); + return vibrator.hasVibrator() ? vibrator : null; + } + } + + @Override + public void run() + { + while (!stopping) + { + boolean doClick = false; + synchronized (this) { + try { + if (nextRumbleUpdate != 0) { + long waitTime = nextRumbleUpdate - System.currentTimeMillis(); + if (waitTime > 0) + this.wait(waitTime); + } + else { + this.wait(); + } + } catch (InterruptedException e) { + } + if (click) { + doClick = true; + click = false; + } + } + if (doClick) + doClick(); + if (nextRumbleUpdate != 0 && nextRumbleUpdate - System.currentTimeMillis() < 5) { + if (!InputDeviceManager.getInstance().updateRumble()) + nextRumbleUpdate = 0; + else + nextRumbleUpdate = System.currentTimeMillis() + 16667; + } + } + InputDeviceManager.getInstance().stopRumble(); + } + + public void stopThread() { + synchronized (this) { + stopping = true; + notify(); + } + try { + join(); + } catch (InterruptedException e) { + } + INSTANCE = null; + } + + public void click() { + if (Emulator.vibrationDuration > 0) { + synchronized (this) { + click = true; + notify(); + } + } + } + + private void doClick() + { + Vibrator vibrator = getVibrator(InputDeviceManager.VIRTUAL_GAMEPAD_ID); + if (vibrator == null) + return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + if (clickEffect == null || clickDuration != Emulator.vibrationDuration) + { + clickDuration = Emulator.vibrationDuration; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + clickEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK); + else + clickEffect = VibrationEffect.createOneShot(clickDuration, VibrationEffect.DEFAULT_AMPLITUDE); + } + vibrator.vibrate(clickEffect); + } else { + vibrator.vibrate(Emulator.vibrationDuration); + } + } + + public void setVibrating() + { + // FIXME possible race condition + synchronized (this) { + if (nextRumbleUpdate == 0) + nextRumbleUpdate = System.currentTimeMillis() + 16667; + notify(); + } + } +} diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index b2fce4df1..b8a261e75 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -511,7 +511,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa } extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, jint id, jstring name, - jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes) + jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) { std::string joyname = jni::String(name, false); std::string unique_id = jni::String(junique_id, false); @@ -520,6 +520,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa std::shared_ptr gamepad = std::make_shared(maple_port, id, joyname.c_str(), unique_id.c_str(), full, half); AndroidGamepadDevice::AddAndroidGamepad(gamepad); + gamepad->setRumbleEnabled(hasRumble); } extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, jint id) { diff --git a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h index c95b62057..c637e3c44 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h @@ -106,7 +106,6 @@ public: if (id == VIRTUAL_GAMEPAD_ID) { input_mapper = std::make_shared(); - rumbleEnabled = true; // hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted } else @@ -310,9 +309,14 @@ public: void rumble(float power, float inclination, u32 duration_ms) override { + power *= rumblePower / 100.f; jboolean has_vibrator = jni::env()->CallBooleanMethod(input_device_manager, input_device_manager_rumble, android_id, power, inclination, duration_ms); rumbleEnabled = has_vibrator; } + void setRumbleEnabled(bool rumbleEnabled) { + this->rumbleEnabled = rumbleEnabled; + } + bool is_virtual_gamepad() override { return android_id == VIRTUAL_GAMEPAD_ID; } bool hasHalfAxis(int axis) const { return std::find(halfAxes.begin(), halfAxes.end(), axis) != halfAxes.end(); }