android: gamepad rumble support (untested)
Use single thread to handle rumble for virtual and physical gamepads. Use heavy click effect for vgamepad buttons when available (android 10) Handle rumble inclination param.
This commit is contained in:
parent
af7edadedb
commit
9976fe10fb
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Integer, VibrationParams> 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<Integer> ids;
|
||||
synchronized (this) {
|
||||
ids = new ArrayList<Integer>(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<Integer> ids;
|
||||
synchronized (this) {
|
||||
ids = new ArrayList<Integer>(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);
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AndroidGamepadDevice> gamepad = std::make_shared<AndroidGamepadDevice>(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)
|
||||
{
|
||||
|
|
|
@ -106,7 +106,6 @@ public:
|
|||
if (id == VIRTUAL_GAMEPAD_ID)
|
||||
{
|
||||
input_mapper = std::make_shared<IdentityInputMapping>();
|
||||
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(); }
|
||||
|
|
Loading…
Reference in New Issue