diff --git a/android/app/src/cpp/android_controller_interface.cpp b/android/app/src/cpp/android_controller_interface.cpp index 6b97d0cbc..f5552b373 100644 --- a/android/app/src/cpp/android_controller_interface.cpp +++ b/android/app/src/cpp/android_controller_interface.cpp @@ -1,4 +1,5 @@ #include "android_controller_interface.h" +#include "android_host_interface.h" #include "common/assert.h" #include "common/file_system.h" #include "common/log.h" @@ -34,36 +35,45 @@ void AndroidControllerInterface::PollEvents() {} void AndroidControllerInterface::ClearBindings() { + std::unique_lock lock(m_controllers_mutex); for (ControllerData& cd : m_controllers) { - cd.axis_mapping.fill({}); - cd.button_mapping.fill({}); - cd.axis_button_mapping.fill({}); - cd.button_axis_mapping.fill({}); + cd.axis_mapping.clear(); + cd.button_mapping.clear(); + cd.axis_button_mapping.clear(); + cd.button_axis_mapping.clear(); } } +std::optional AndroidControllerInterface::GetControllerIndex(const std::string_view& device) +{ + std::unique_lock lock(m_controllers_mutex); + for (u32 i = 0; i < static_cast(m_device_names.size()); i++) + { + if (device == m_device_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) { + std::unique_lock lock(m_controllers_mutex); if (static_cast(controller_index) >= m_controllers.size()) return false; - if (axis_number < 0 || axis_number >= NUM_AXISES) - return false; - m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback); return true; } bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) { + std::unique_lock lock(m_controllers_mutex); if (static_cast(controller_index) >= m_controllers.size()) return false; - if (button_number < 0 || button_number >= NUM_BUTTONS) - return false; - m_controllers[controller_index].button_mapping[button_number] = std::move(callback); return true; } @@ -71,12 +81,10 @@ bool AndroidControllerInterface::BindControllerButton(int controller_index, int bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) { + std::unique_lock lock(m_controllers_mutex); if (static_cast(controller_index) >= m_controllers.size()) return false; - if (axis_number < 0 || axis_number >= NUM_AXISES) - return false; - m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback); return true; } @@ -84,99 +92,145 @@ bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, ButtonCallback callback) { - // Hats don't exist in XInput return false; } bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) { + std::unique_lock lock(m_controllers_mutex); if (static_cast(controller_index) >= m_controllers.size()) return false; - if (button_number < 0 || button_number >= NUM_BUTTONS) - return false; - m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback); return true; } -bool AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value) +void AndroidControllerInterface::SetDeviceNames(std::vector device_names) { - Log_DevPrintf("controller %u axis %u %f", index, static_cast(axis), value); - DebugAssert(index < NUM_CONTROLLERS); + std::unique_lock lock(m_controllers_mutex); + m_device_names = std::move(device_names); + m_controllers.resize(m_device_names.size()); +} - if (DoEventHook(Hook::Type::Axis, index, static_cast(axis), value)) - return true; +void AndroidControllerInterface::SetDeviceRumble(u32 index, bool has_vibrator) +{ + std::unique_lock lock(m_controllers_mutex); + if (index >= m_controllers.size()) + return; - const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast(axis)][AxisSide::Full]; - if (cb) + m_controllers[index].has_rumble = has_vibrator; +} + +void AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value) +{ + std::unique_lock lock(m_controllers_mutex); + if (index >= m_controllers.size()) + return; + + Log_DevPrintf("controller %u axis %u %f", index, axis, value); + if (DoEventHook(Hook::Type::Axis, index, axis, value)) + return; + + const ControllerData& cd = m_controllers[index]; + const auto am_iter = cd.axis_mapping.find(axis); + if (am_iter != cd.axis_mapping.end()) { - cb(value); - return true; + const AxisCallback& cb = am_iter->second[AxisSide::Full]; + if (cb) + { + cb(value); + return; + } } // set the other direction to false so large movements don't leave the opposite on - const bool outside_deadzone = (std::abs(value) >= m_controllers[index].deadzone); + const bool outside_deadzone = (std::abs(value) >= cd.deadzone); const bool positive = (value >= 0.0f); - const ButtonCallback& other_button_cb = - m_controllers[index].axis_button_mapping[static_cast(axis)][BoolToUInt8(!positive)]; - const ButtonCallback& button_cb = - m_controllers[index].axis_button_mapping[static_cast(axis)][BoolToUInt8(positive)]; - if (button_cb) + const auto bm_iter = cd.axis_button_mapping.find(axis); + if (bm_iter != cd.axis_button_mapping.end()) { - button_cb(outside_deadzone); - if (other_button_cb) + const ButtonCallback& other_button_cb = bm_iter->second[BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = bm_iter->second[BoolToUInt8(positive)]; + if (button_cb) + { + button_cb(outside_deadzone); + if (other_button_cb) + other_button_cb(false); + return; + } + else if (other_button_cb) + { other_button_cb(false); - return true; - } - else if (other_button_cb) - { - other_button_cb(false); - return true; - } - else - { - return false; + return; + } } } -bool AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed) +void AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed) { Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released"); - DebugAssert(index < NUM_CONTROLLERS); + + std::unique_lock lock(m_controllers_mutex); + if (index >= m_controllers.size()) + return; if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f)) - return true; + return; - const ButtonCallback& cb = m_controllers[index].button_mapping[button]; - if (cb) + const ControllerData& cd = m_controllers[index]; + const auto button_iter = cd.button_mapping.find(button); + if (button_iter != cd.button_mapping.end() && button_iter->second) { - cb(pressed); - return true; + button_iter->second(pressed); + return; } - const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button]; - if (axis_cb) + const auto axis_iter = cd.button_axis_mapping.find(button); + if (axis_iter != cd.button_axis_mapping.end() && axis_iter->second) { - axis_cb(pressed ? 1.0f : -1.0f); + axis_iter->second(pressed ? 1.0f : -1.0f); + return; } - return true; +} + +bool AndroidControllerInterface::HasButtonBinding(u32 index, u32 button) +{ + std::unique_lock lock(m_controllers_mutex); + if (index >= m_controllers.size()) + return false; + + const ControllerData& cd = m_controllers[index]; + return (cd.button_mapping.find(button) != cd.button_mapping.end() || + cd.button_axis_mapping.find(button) != cd.button_axis_mapping.end()); } u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index) { - return 0; + std::unique_lock lock(m_controllers_mutex); + if (static_cast(controller_index) >= m_controllers.size()) + return false; + + return m_controllers[static_cast(controller_index)].has_rumble ? NUM_RUMBLE_MOTORS : 0; } void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) { + std::unique_lock lock(m_controllers_mutex); + if (static_cast(controller_index) >= m_controllers.size()) + return; + + const float small_motor = strengths[0]; + const float large_motor = strengths[1]; + static_cast(m_host_interface) + ->SetControllerVibration(static_cast(controller_index), small_motor, large_motor); } bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */) { - if (static_cast(controller_index) >= NUM_CONTROLLERS) + std::unique_lock lock(m_controllers_mutex); + if (static_cast(controller_index) >= m_controllers.size()) return false; m_controllers[static_cast(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f); diff --git a/android/app/src/cpp/android_controller_interface.h b/android/app/src/cpp/android_controller_interface.h index aaa408f84..a22b81f50 100644 --- a/android/app/src/cpp/android_controller_interface.h +++ b/android/app/src/cpp/android_controller_interface.h @@ -3,6 +3,7 @@ #include "frontend-common/controller_interface.h" #include #include +#include #include #include @@ -12,6 +13,8 @@ public: AndroidControllerInterface(); ~AndroidControllerInterface() override; + ALWAYS_INLINE u32 GetControllerCount() const { return static_cast(m_controllers.size()); } + Backend GetBackend() const override; bool Initialize(CommonHostInterface* host_interface) override; void Shutdown() override; @@ -20,6 +23,7 @@ public: void ClearBindings() override; // Binding to events. If a binding for this axis/button already exists, returns false. + std::optional GetControllerIndex(const std::string_view& device) override; bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override; bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, @@ -37,30 +41,32 @@ public: void PollEvents() override; - bool HandleAxisEvent(u32 index, u32 axis, float value); - bool HandleButtonEvent(u32 index, u32 button, bool pressed); + void SetDeviceNames(std::vector device_names); + void SetDeviceRumble(u32 index, bool has_vibrator); + void HandleAxisEvent(u32 index, u32 axis, float value); + void HandleButtonEvent(u32 index, u32 button, bool pressed); + bool HasButtonBinding(u32 index, u32 button); private: enum : u32 { - NUM_CONTROLLERS = 1, - NUM_AXISES = 12, - NUM_BUTTONS = 23 + NUM_RUMBLE_MOTORS = 2 }; struct ControllerData { float deadzone = 0.25f; - std::array, NUM_AXISES> axis_mapping; - std::array button_mapping; - std::array, NUM_AXISES> axis_button_mapping; - std::array button_axis_mapping; + std::map> axis_mapping; + std::map button_mapping; + std::map> axis_button_mapping; + std::map button_axis_mapping; + bool has_rumble = false; }; - using ControllerDataArray = std::array; - - ControllerDataArray m_controllers; + std::vector m_device_names; + std::vector m_controllers; + std::mutex m_controllers_mutex; std::mutex m_event_intercept_mutex; Hook::Callback m_event_intercept_callback; diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 77305d23d..e8b6684b8 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -49,6 +49,9 @@ static jmethodID s_EmulationActivity_method_onGameTitleChanged; static jmethodID s_EmulationActivity_method_setVibration; static jmethodID s_EmulationActivity_method_getRefreshRate; static jmethodID s_EmulationActivity_method_openPauseMenu; +static jmethodID s_EmulationActivity_method_getInputDeviceNames; +static jmethodID s_EmulationActivity_method_hasInputDeviceVibration; +static jmethodID s_EmulationActivity_method_setInputDeviceVibration; static jclass s_PatchCode_class; static jmethodID s_PatchCode_constructor; static jclass s_GameListEntry_class; @@ -284,6 +287,48 @@ void AndroidHostInterface::LoadSettings(SettingsInterface& si) } } +void AndroidHostInterface::UpdateInputMap(SettingsInterface& si) +{ + if (m_emulation_activity_object) + { + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + DebugAssert(env); + + std::vector device_names; + + jobjectArray const java_names = reinterpret_cast( + env->CallObjectMethod(m_emulation_activity_object, s_EmulationActivity_method_getInputDeviceNames)); + if (java_names) + { + const u32 count = static_cast(env->GetArrayLength(java_names)); + for (u32 i = 0; i < count; i++) + { + device_names.push_back( + AndroidHelpers::JStringToString(env, reinterpret_cast(env->GetObjectArrayElement(java_names, i)))); + } + + env->DeleteLocalRef(java_names); + } + + if (m_controller_interface) + { + AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); + if (ci) + { + ci->SetDeviceNames(std::move(device_names)); + for (u32 i = 0; i < ci->GetControllerCount(); i++) + { + const bool has_vibration = env->CallBooleanMethod( + m_emulation_activity_object, s_EmulationActivity_method_hasInputDeviceVibration, static_cast(i)); + ci->SetDeviceRumble(i, has_vibration); + } + } + } + } + + CommonHostInterface::UpdateInputMap(si); +} + bool AndroidHostInterface::IsEmulationThreadPaused() const { return System::IsValid() && System::IsPaused(); @@ -461,6 +506,7 @@ void AndroidHostInterface::EmulationThreadLoop(JNIEnv* env) else System::RunFrame(); + UpdateControllerRumble(); if (m_vibration_enabled) UpdateVibration(); } @@ -720,6 +766,28 @@ void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 a }); } +bool AndroidHostInterface::HasControllerButtonBinding(u32 controller_index, u32 button) +{ + AndroidControllerInterface* ci = static_cast(m_controller_interface.get()); + if (!ci) + return false; + + return ci->HasButtonBinding(controller_index, button); +} + +void AndroidHostInterface::SetControllerVibration(u32 controller_index, float small_motor, float large_motor) +{ + if (!m_emulation_activity_object) + return; + + JNIEnv* env = AndroidHelpers::GetJNIEnv(); + DebugAssert(env); + + env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_setInputDeviceVibration, + static_cast(controller_index), static_cast(small_motor), + static_cast(large_motor)); +} + void AndroidHostInterface::SetFastForwardEnabled(bool enabled) { m_fast_forward_enabled = enabled; @@ -885,6 +953,12 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(emulation_activity_class, "getRefreshRate", "()F")) == nullptr || (s_EmulationActivity_method_openPauseMenu = env->GetMethodID(emulation_activity_class, "openPauseMenu", "()V")) == nullptr || + (s_EmulationActivity_method_getInputDeviceNames = + env->GetMethodID(s_EmulationActivity_class, "getInputDeviceNames", "()[Ljava/lang/String;")) == nullptr || + (s_EmulationActivity_method_hasInputDeviceVibration = + env->GetMethodID(s_EmulationActivity_class, "hasInputDeviceVibration", "(I)Z")) == nullptr || + (s_EmulationActivity_method_setInputDeviceVibration = + env->GetMethodID(s_EmulationActivity_class, "setInputDeviceVibration", "(IFF)V")) == nullptr || (s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "", "(ILjava/lang/String;Z)V")) == nullptr || (s_GameListEntry_constructor = env->GetMethodID( s_GameListEntry_class, "", @@ -1119,16 +1193,35 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames return name_array; } +DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerVibrationMotorCount, jobject unused, jstring controller_type) +{ + std::optional type = + Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); + if (!type) + return 0; + + return static_cast(Controller::GetVibrationMotorCount(type.value())); +} + DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index, jint button_index, jboolean pressed) { - AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(controller_index, button_index, pressed); + AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(static_cast(controller_index), + static_cast(button_index), pressed); } DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index, jint axis_index, jfloat value) { - AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(controller_index, axis_index, value); + AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(static_cast(controller_index), + static_cast(axis_index), value); +} + +DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasControllerButtonBinding, jobject obj, jint controller_index, + jint button_index) +{ + return AndroidHelpers::GetNativeClass(env, obj)->HasControllerButtonBinding(static_cast(controller_index), + static_cast(button_index)); } DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getInputProfileNames, jobject obj) @@ -1330,6 +1423,19 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj) } } +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_updateInputMap, jobject obj) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + if (hi->IsEmulationThreadRunning()) + { + hi->RunOnEmulationThread([hi]() { hi->UpdateInputMap(); }); + } + else + { + hi->UpdateInputMap(); + } +} + DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot) { AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index c23193fca..4e9dcfc00 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -21,6 +21,8 @@ class Controller; class AndroidHostInterface final : public CommonHostInterface { public: + using CommonHostInterface::UpdateInputMap; + AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory); ~AndroidHostInterface() override; @@ -57,6 +59,8 @@ public: void SetControllerAxisState(u32 index, s32 button_code, float value); void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed); void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value); + bool HasControllerButtonBinding(u32 controller_index, u32 button); + void SetControllerVibration(u32 controller_index, float small_motor, float large_motor); void SetFastForwardEnabled(bool enabled); void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback); @@ -83,6 +87,7 @@ private: void EmulationThreadLoop(JNIEnv* env); void LoadSettings(SettingsInterface& si) override; + void UpdateInputMap(SettingsInterface& si) override; void SetVibration(bool enabled); void UpdateVibration(); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index d8dc29fda..6c25986b2 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -82,10 +82,14 @@ public class AndroidHostInterface { public static native String[] getControllerAxisNames(String controllerType); + public static native int getControllerVibrationMotorCount(String controllerType); + public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed); public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value); + public native boolean hasControllerButtonBinding(int controllerIndex, int buttonIndex); + public native void toggleControllerAnalogMode(); public native String[] getInputProfileNames(); @@ -115,6 +119,7 @@ public class AndroidHostInterface { public native void saveResumeState(boolean waitForCompletion); public native void applySettings(); + public native void updateInputMap(); public native void setDisplayAlignment(int alignment); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java index bd71bf10a..047301d27 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingDialog.java @@ -1,13 +1,11 @@ package com.github.stenzek.duckstation; import android.app.AlertDialog; -import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.ArraySet; -import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; @@ -15,16 +13,20 @@ import android.view.MotionEvent; import androidx.annotation.NonNull; import java.util.HashMap; +import java.util.List; public class ControllerBindingDialog extends AlertDialog { - private boolean mIsAxis; - private String mSettingKey; + final static float DETECT_THRESHOLD = 0.25f; + private final ControllerBindingPreference.Type mType; + private final String mSettingKey; private String mCurrentBinding; + private int mUpdatedAxisCode = -1; + private final HashMap mStartingAxisValues = new HashMap<>(); - public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, boolean isAxis) { + public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, ControllerBindingPreference.Type type) { super(context); - mIsAxis = isAxis; + mType = type; mSettingKey = settingKey; mCurrentBinding = currentBinding; if (mCurrentBinding == null) @@ -42,10 +44,7 @@ public class ControllerBindingDialog extends AlertDialog { setOnKeyListener(new DialogInterface.OnKeyListener() { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - if (onKeyDown(keyCode, event)) - return true; - - return false; + return onKeyDown(keyCode, event); } }); } @@ -73,57 +72,53 @@ public class ControllerBindingDialog extends AlertDialog { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mIsAxis || !EmulationSurfaceView.isDPadOrButtonEvent(event)) + if (!EmulationSurfaceView.isBindableDevice(event.getDevice()) || !EmulationSurfaceView.isBindableKeyCode(event.getKeyCode())) { + return super.onKeyUp(keyCode, event); + } + + if (mType == ControllerBindingPreference.Type.BUTTON) + mCurrentBinding = String.format("%s/Button%d", event.getDevice().getDescriptor(), event.getKeyCode()); + else if (mType == ControllerBindingPreference.Type.VIBRATION) + mCurrentBinding = event.getDevice().getDescriptor(); + else return super.onKeyUp(keyCode, event); - int buttonIndex = EmulationSurfaceView.getButtonIndexForKeyCode(keyCode); - if (buttonIndex < 0) - return super.onKeyUp(keyCode, event); - - // TODO: Multiple controllers - final int controllerIndex = 0; - mCurrentBinding = String.format("Controller%d/Button%d", controllerIndex, buttonIndex); updateMessage(); updateBinding(); dismiss(); return true; } - private int mUpdatedAxisCode = -1; - - private void setAxisCode(int axisCode, boolean positive) { - final int axisIndex = EmulationSurfaceView.getAxisIndexForAxisCode(axisCode); - if (mUpdatedAxisCode >= 0 || axisIndex < 0) + private void setAxisCode(InputDevice device, int axisCode, boolean positive) { + if (mUpdatedAxisCode >= 0) return; mUpdatedAxisCode = axisCode; final int controllerIndex = 0; - if (mIsAxis) - mCurrentBinding = String.format("Controller%d/Axis%d", controllerIndex, axisIndex); + if (mType == ControllerBindingPreference.Type.AXIS) + mCurrentBinding = String.format("%s/Axis%d", device.getDescriptor(), axisCode); else - mCurrentBinding = String.format("Controller%d/%cAxis%d", controllerIndex, (positive) ? '+' : '-', axisIndex); + mCurrentBinding = String.format("%s/%cAxis%d", device.getDescriptor(), (positive) ? '+' : '-', axisCode); updateBinding(); updateMessage(); dismiss(); } - final static float DETECT_THRESHOLD = 0.25f; - - private HashMap mStartingAxisValues = new HashMap<>(); - private boolean doAxisDetection(MotionEvent event) { - if ((event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0) + if (!EmulationSurfaceView.isBindableDevice(event.getDevice())) return false; - final int[] axisCodes = EmulationSurfaceView.getKnownAxisCodes(); - final int deviceId = event.getDeviceId(); + final List motionEventList = event.getDevice().getMotionRanges(); + if (motionEventList == null || motionEventList.isEmpty()) + return false; + final int deviceId = event.getDeviceId(); if (!mStartingAxisValues.containsKey(deviceId)) { - final float[] axisValues = new float[axisCodes.length]; - for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) { - final int axisCode = axisCodes[axisIndex]; + final float[] axisValues = new float[motionEventList.size()]; + for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) { + final int axisCode = motionEventList.get(axisIndex).getAxis(); // these are binary, so start at zero if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y) @@ -133,13 +128,15 @@ public class ControllerBindingDialog extends AlertDialog { } mStartingAxisValues.put(deviceId, axisValues); + return false; } final float[] axisValues = mStartingAxisValues.get(deviceId); - for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) { - final float newValue = event.getAxisValue(axisCodes[axisIndex]); + for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) { + final int axisCode = motionEventList.get(axisIndex).getAxis(); + final float newValue = event.getAxisValue(axisCode); if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) { - setAxisCode(axisCodes[axisIndex], newValue >= 0.0f); + setAxisCode(event.getDevice(), axisCode, newValue >= 0.0f); break; } } @@ -149,6 +146,10 @@ public class ControllerBindingDialog extends AlertDialog { @Override public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + if (mType != ControllerBindingPreference.Type.AXIS && mType != ControllerBindingPreference.Type.BUTTON) { + return false; + } + if (doAxisDetection(event)) return true; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java index 01c4687ed..3dbf96a52 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerBindingPreference.java @@ -14,16 +14,25 @@ import androidx.preference.PreferenceViewHolder; import java.util.Set; public class ControllerBindingPreference extends Preference { - private enum Type { + public enum Type { BUTTON, AXIS, + VIBRATION + } + + private enum VisualType { + BUTTON, + AXIS, + VIBRATION, HOTKEY } private String mBindingName; + private String mDisplayName; private String mValue; private TextView mValueView; private Type mType = Type.BUTTON; + private VisualType mVisualType = VisualType.BUTTON; private static int getIconForButton(String buttonName) { if (buttonName.equals("Up")) { @@ -64,7 +73,16 @@ public class ControllerBindingPreference extends Preference { } private static int getIconForHotkey(String hotkeyDisplayName) { - return R.drawable.ic_baseline_category_24; + switch (hotkeyDisplayName) { + case "FastForward": + case "ToggleFastForward": + case "Turbo": + case "ToggleTurbo": + return R.drawable.ic_controller_fast_forward; + + default: + return R.drawable.ic_baseline_category_24; + } } public ControllerBindingPreference(Context context, AttributeSet attrs) { @@ -94,7 +112,7 @@ public class ControllerBindingPreference extends Preference { mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value)); int drawableId = R.drawable.ic_baseline_radio_button_checked_24; - switch (mType) { + switch (mVisualType) { case BUTTON: drawableId = getIconForButton(mBindingName); break; @@ -104,37 +122,55 @@ public class ControllerBindingPreference extends Preference { case HOTKEY: drawableId = getIconForHotkey(mBindingName); break; + case VIBRATION: + drawableId = R.drawable.ic_baseline_vibration_24; + break; } iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId)); - nameView.setText(mBindingName); + nameView.setText(mDisplayName); updateValue(); } @Override protected void onClick() { - ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, (mType == Type.AXIS)); + ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mType); dialog.setOnDismissListener((dismissedDialog) -> updateValue()); dialog.show(); } public void initButton(int controllerIndex, String buttonName) { mBindingName = buttonName; + mDisplayName = buttonName; mType = Type.BUTTON; + mVisualType = VisualType.BUTTON; setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName)); updateValue(); } public void initAxis(int controllerIndex, String axisName) { mBindingName = axisName; + mDisplayName = axisName; mType = Type.AXIS; + mVisualType = VisualType.AXIS; setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName)); updateValue(); } + public void initVibration(int controllerIndex) { + mBindingName = "Rumble"; + mDisplayName = getContext().getString(R.string.controller_binding_device_for_vibration); + mType = Type.VIBRATION; + mVisualType = VisualType.VIBRATION; + setKey(String.format("Controller%d/Rumble", controllerIndex)); + updateValue(); + } + public void initHotkey(HotkeyInfo hotkeyInfo) { - mBindingName = hotkeyInfo.getDisplayName(); - mType = Type.HOTKEY; + mBindingName = hotkeyInfo.getName(); + mDisplayName = hotkeyInfo.getDisplayName(); + mType = Type.BUTTON; + mVisualType = VisualType.HOTKEY; setKey(hotkeyInfo.getBindingConfigKey()); updateValue(); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerMappingActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerMappingActivity.java index 70fb3642a..20a968915 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerMappingActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerMappingActivity.java @@ -172,9 +172,10 @@ public class ControllerMappingActivity extends AppCompatActivity { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { final SharedPreferences sp = getPreferenceManager().getSharedPreferences(); final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None"; - String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType); - String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType); - String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType); + final String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType); + final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType); + final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType); + final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType); final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); if (controllerButtons != null) { @@ -193,6 +194,12 @@ public class ControllerMappingActivity extends AppCompatActivity { activity.mPreferences.add(cbp); } } + if (vibrationMotors > 0) { + final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); + cbp.initVibration(controllerIndex); + ps.addPreference(cbp); + activity.mPreferences.add(cbp); + } setPreferenceScreen(ps); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 1f7b145a6..d96263c13 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -1,7 +1,6 @@ package com.github.stenzek.duckstation; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; @@ -17,17 +16,14 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; -import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ListView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; /** @@ -157,6 +153,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde }); } + public String[] getInputDeviceNames() { + return (mContentView != null) ? mContentView.getInputDeviceNames() : null; + } + + public boolean hasInputDeviceVibration(int controllerIndex) { + return (mContentView != null) ? mContentView.hasInputDeviceVibration(controllerIndex) : null; + } + + public void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) { + if (mContentView != null) + mContentView.setInputDeviceVibration(controllerIndex, smallMotor, largeMotor); + } + private void doApplySettings() { AndroidHostInterface.getInstance().applySettings(); updateRequestedOrientation(); @@ -731,8 +740,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde Log.i("EmulationActivity", "Controller type: " + controllerType); Log.i("EmulationActivity", "View type: " + viewType); - final boolean hasAnyControllers = mContentView.initControllerMapping(controllerType); + mContentView.updateInputDevices(); + AndroidHostInterface.getInstance().updateInputMap(); + final boolean hasAnyControllers = mContentView.hasAnyGamePads(); if (controllerType.equals("none") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) { if (mTouchscreenController != null) { activityLayout.removeView(mTouchscreenController); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java index 261ad4dac..5fdc71040 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java @@ -2,6 +2,7 @@ package com.github.stenzek.duckstation; import android.content.Context; import android.content.SharedPreferences; +import android.os.Vibrator; import android.util.AttributeSet; import android.util.Log; import android.view.InputDevice; @@ -28,14 +29,43 @@ public class EmulationSurfaceView extends SurfaceView { super(context, attrs, defStyle); } - public static boolean isDPadOrButtonEvent(KeyEvent event) { - final int source = event.getSource(); - return (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || - (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || - (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK; + public static boolean isBindableDevice(InputDevice inputDevice) { + if (inputDevice == null) + return false; + + final int sources = inputDevice.getSources(); + + // Prevent binding pointer devices such as a mouse. + if ((sources & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER) + return false; + + return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) || + ((sources & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON); } - private boolean isExternalKeyCode(int keyCode) { + public static boolean isGamepadDevice(InputDevice inputDevice) { + final int sources = inputDevice.getSources(); + return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); + } + + public static boolean isBindableKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_HOME: + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_POWER: + case KeyEvent.KEYCODE_CAMERA: + case KeyEvent.KEYCODE_CALL: + case KeyEvent.KEYCODE_ENDCALL: + case KeyEvent.KEYCODE_VOICE_ASSIST: + return false; + + default: + return true; + } + } + + private static boolean isExternalKeyCode(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_HOME: @@ -55,157 +85,146 @@ public class EmulationSurfaceView extends SurfaceView { } } - private static final int[] buttonKeyCodes = new int[]{ - KeyEvent.KEYCODE_BUTTON_A, // 0/Cross - KeyEvent.KEYCODE_BUTTON_B, // 1/Circle - KeyEvent.KEYCODE_BUTTON_X, // 2/Square - KeyEvent.KEYCODE_BUTTON_Y, // 3/Triangle - KeyEvent.KEYCODE_BUTTON_SELECT, // 4/Select - KeyEvent.KEYCODE_BUTTON_MODE, // 5/Analog - KeyEvent.KEYCODE_BUTTON_START, // 6/Start - KeyEvent.KEYCODE_BUTTON_THUMBL, // 7/L3 - KeyEvent.KEYCODE_BUTTON_THUMBR, // 8/R3 - KeyEvent.KEYCODE_BUTTON_L1, // 9/L1 - KeyEvent.KEYCODE_BUTTON_R1, // 10/R1 - KeyEvent.KEYCODE_DPAD_UP, // 11/Up - KeyEvent.KEYCODE_DPAD_DOWN, // 12/Down - KeyEvent.KEYCODE_DPAD_LEFT, // 13/Left - KeyEvent.KEYCODE_DPAD_RIGHT, // 14/Right - KeyEvent.KEYCODE_BUTTON_L2, // 15 - KeyEvent.KEYCODE_BUTTON_R2, // 16 - KeyEvent.KEYCODE_BUTTON_C, // 17 - KeyEvent.KEYCODE_BUTTON_Z, // 18 - KeyEvent.KEYCODE_VOLUME_DOWN, // 19 - KeyEvent.KEYCODE_VOLUME_UP, // 20 - KeyEvent.KEYCODE_MENU, // 21 - KeyEvent.KEYCODE_CAMERA, // 22 - }; - private static final int[] axisCodes = new int[]{ - MotionEvent.AXIS_X, // 0/LeftX - MotionEvent.AXIS_Y, // 1/LeftY - MotionEvent.AXIS_Z, // 2/RightX - MotionEvent.AXIS_RZ, // 3/RightY - MotionEvent.AXIS_LTRIGGER, // 4/L2 - MotionEvent.AXIS_RTRIGGER, // 5/R2 - MotionEvent.AXIS_RX, // 6 - MotionEvent.AXIS_RY, // 7 - MotionEvent.AXIS_HAT_X, // 8 - MotionEvent.AXIS_HAT_Y, // 9 - MotionEvent.AXIS_GAS, // 10 - MotionEvent.AXIS_BRAKE, // 11 - }; + private class InputDeviceData { + private int deviceId; + private String descriptor; + private int[] axes; + private float[] axisValues; + private int controllerIndex; + private Vibrator vibrator; - public static int getButtonIndexForKeyCode(int keyCode) { - for (int buttonIndex = 0; buttonIndex < buttonKeyCodes.length; buttonIndex++) { - if (buttonKeyCodes[buttonIndex] == keyCode) - return buttonIndex; - } - - Log.e("EmulationSurfaceView", String.format("Button code %d not found", keyCode)); - return -1; - } - - public static int[] getKnownAxisCodes() { - return axisCodes; - } - - public static int getAxisIndexForAxisCode(int axisCode) { - for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) { - if (axisCodes[axisIndex] == axisCode) - return axisIndex; - } - - Log.e("EmulationSurfaceView", String.format("Axis code %d not found", axisCode)); - return -1; - } - - - private class ButtonMapping { - public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) { - this.deviceId = deviceId; - this.deviceAxisOrButton = deviceButton; + public InputDeviceData(InputDevice device, int controllerIndex) { + deviceId = device.getId(); + descriptor = device.getDescriptor(); this.controllerIndex = controllerIndex; - this.buttonMapping = button; - } - public int deviceId; - public int deviceAxisOrButton; - public int controllerIndex; - public int buttonMapping; - } - - private class AxisMapping { - public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int axis) { - this.deviceId = deviceId; - this.deviceAxisOrButton = deviceAxis; - this.deviceMotionRange = motionRange; - this.controllerIndex = controllerIndex; - this.axisMapping = axis; - this.positiveButton = -1; - this.negativeButton = -1; - } - - public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int positiveButton, int negativeButton) { - this.deviceId = deviceId; - this.deviceAxisOrButton = deviceAxis; - this.deviceMotionRange = motionRange; - this.controllerIndex = controllerIndex; - this.axisMapping = -1; - this.positiveButton = positiveButton; - this.negativeButton = negativeButton; - } - - public int deviceId; - public int deviceAxisOrButton; - public InputDevice.MotionRange deviceMotionRange; - public int controllerIndex; - public int axisMapping; - public int positiveButton; - public int negativeButton; - } - - private ArrayList mControllerKeyMapping; - private ArrayList mControllerAxisMapping; - - private boolean handleControllerKey(int deviceId, int keyCode, int repeatCount, boolean pressed) { - boolean result = false; - for (ButtonMapping mapping : mControllerKeyMapping) { - if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode) - continue; - - if (repeatCount == 0) { - AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.buttonMapping, pressed); - Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0)); + List motionRanges = device.getMotionRanges(); + if (motionRanges != null && !motionRanges.isEmpty()) { + axes = new int[motionRanges.size()]; + axisValues = new float[motionRanges.size()]; + for (int i = 0; i < motionRanges.size(); i++) + axes[i] = motionRanges.get(i).getAxis(); } - result = true; + // device.getVibrator() always returns null, but might return a "null vibrator". + final Vibrator potentialVibrator = device.getVibrator(); + if (potentialVibrator != null && potentialVibrator.hasVibrator()) + vibrator = potentialVibrator; + } + } + + private InputDeviceData[] mInputDevices = null; + private boolean mHasAnyGamepads = false; + + public boolean hasAnyGamePads() { + return mHasAnyGamepads; + } + + public synchronized void updateInputDevices() { + mInputDevices = null; + mHasAnyGamepads = false; + + final ArrayList inputDeviceIds = new ArrayList<>(); + for (int deviceId : InputDevice.getDeviceIds()) { + final InputDevice device = InputDevice.getDevice(deviceId); + if (device == null || !isBindableDevice(device)) + continue; + + if (isGamepadDevice(device)) + mHasAnyGamepads = true; + + final int controllerIndex = inputDeviceIds.size(); + Log.d("EmulationSurfaceView", String.format("Tracking device %d/%s (%s)", + controllerIndex, device.getDescriptor(), device.getName())); + inputDeviceIds.add(new InputDeviceData(device, controllerIndex)); } - return result; + if (inputDeviceIds.isEmpty()) + return; + + mInputDevices = new InputDeviceData[inputDeviceIds.size()]; + inputDeviceIds.toArray(mInputDevices); + } + + public synchronized String[] getInputDeviceNames() { + if (mInputDevices == null) + return null; + + final String[] deviceNames = new String[mInputDevices.length]; + for (int i = 0; i < mInputDevices.length; i++) { + deviceNames[i] = mInputDevices[i].descriptor; + } + + return deviceNames; + } + + public synchronized boolean hasInputDeviceVibration(int controllerIndex) { + if (mInputDevices == null || controllerIndex >= mInputDevices.length) + return false; + + return (mInputDevices[controllerIndex].vibrator != null); + } + + public synchronized void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) { + if (mInputDevices == null || controllerIndex >= mInputDevices.length) + return; + + // shouldn't get here + final InputDeviceData data = mInputDevices[controllerIndex]; + if (data.vibrator == null) + return; + + final float MINIMUM_INTENSITY = 0.1f; + if (smallMotor >= MINIMUM_INTENSITY || largeMotor >= MINIMUM_INTENSITY) + data.vibrator.vibrate(1000); + else + data.vibrator.cancel(); + } + + public InputDeviceData getDataForDeviceId(int deviceId) { + if (mInputDevices == null) + return null; + + for (InputDeviceData data : mInputDevices) { + if (data.deviceId == deviceId) + return data; + } + + return null; + } + + public int getControllerIndexForDeviceId(int deviceId) { + final InputDeviceData data = getDataForDeviceId(deviceId); + return (data != null) ? data.controllerIndex : -1; + } + + private boolean handleKeyEvent(int keyCode, KeyEvent event, boolean pressed) { + if (!isBindableDevice(event.getDevice())) + return false; + + /*Log.e("ESV", String.format("Code %d RC %d Pressed %d %s", keyCode, + event.getRepeatCount(), pressed? 1 : 0, event.toString()));*/ + + final AndroidHostInterface hi = AndroidHostInterface.getInstance(); + final int controllerIndex = getControllerIndexForDeviceId(event.getDeviceId()); + if (event.getRepeatCount() == 0 && controllerIndex >= 0) + hi.handleControllerButtonEvent(controllerIndex, keyCode, pressed); + + // We don't want to eat external button events unless it's actually bound. + if (isExternalKeyCode(keyCode)) + return (controllerIndex >= 0 && hi.hasControllerButtonBinding(controllerIndex, keyCode)); + else + return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isDPadOrButtonEvent(event)) - return false; - - if (handleControllerKey(event.getDeviceId(), keyCode, event.getRepeatCount(), true)) - return true; - - // eat non-external button events anyway - return !isExternalKeyCode(keyCode); + return handleKeyEvent(keyCode, event, true); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (!isDPadOrButtonEvent(event)) - return false; - - if (handleControllerKey(event.getDeviceId(), keyCode, 0, false)) - return true; - - // eat non-external button events anyway - return !isExternalKeyCode(keyCode); + return handleKeyEvent(keyCode, event, false); } private float clamp(float value, float min, float max) { @@ -214,19 +233,19 @@ public class EmulationSurfaceView extends SurfaceView { @Override public boolean onGenericMotionEvent(MotionEvent event) { - final int source = event.getSource(); - if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0) + if (!isBindableDevice(event.getDevice())) return false; - final int deviceId = event.getDeviceId(); - for (AxisMapping mapping : mControllerAxisMapping) { - if (mapping.deviceId != deviceId) - continue; + final InputDeviceData data = getDataForDeviceId(event.getDeviceId()); + if (data == null || data.axes == null) + return false; - final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton); + for (int i = 0; i < data.axes.length; i++) { + final int axis = data.axes[i]; + final float axisValue = event.getAxisValue(axis); float emuValue; - switch (mapping.deviceAxisOrButton) { + switch (axis) { case MotionEvent.AXIS_BRAKE: case MotionEvent.AXIS_GAS: case MotionEvent.AXIS_LTRIGGER: @@ -241,109 +260,16 @@ public class EmulationSurfaceView extends SurfaceView { break; } - Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue)); + if (data.axisValues[i] == emuValue) + continue; - if (mapping.axisMapping >= 0) { - AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, emuValue); - } + /*Log.d("EmulationSurfaceView", + String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));*/ - final float DEAD_ZONE = 0.25f; - if (mapping.negativeButton >= 0) { - AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE)); - } - if (mapping.positiveButton >= 0) { - AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.positiveButton, (emuValue >= DEAD_ZONE)); - } + data.axisValues[i] = emuValue; + AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue); } return true; } - - private boolean addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex) { - int mapping = getButtonIndexForKeyCode(keyCode); - Log.i("EmulationSurfaceView", String.format("Map %d to %d", keyCode, mapping)); - if (mapping >= 0) { - mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping)); - return true; - } - - return false; - } - - private boolean addControllerAxisMapping(int deviceId, List motionRanges, int axis, int controllerIndex) { - InputDevice.MotionRange range = null; - for (InputDevice.MotionRange curRange : motionRanges) { - if (curRange.getAxis() == axis) { - range = curRange; - break; - } - } - if (range == null) - return false; - - int mapping = getAxisIndexForAxisCode(axis); - int negativeButton = -1; - int positiveButton = -1; - - if (mapping >= 0) { - Log.i("EmulationSurfaceView", String.format("Map axis %d to %d", axis, mapping)); - mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping)); - return true; - } - - if (negativeButton >= 0 && negativeButton >= 0) { - Log.i("EmulationSurfaceView", String.format("Map axis %d to buttons %d %d", axis, negativeButton, positiveButton)); - mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveButton, negativeButton)); - return true; - } - - Log.w("EmulationSurfaceView", String.format("Axis %d was not mapped", axis)); - return false; - } - - private static boolean isJoystickDevice(int deviceId) { - if (deviceId < 0) - return false; - - final InputDevice dev = InputDevice.getDevice(deviceId); - if (dev == null) - return false; - - final int sources = dev.getSources(); - if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) - return true; - - if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) - return true; - - return (sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD; - } - - public boolean initControllerMapping(String controllerType) { - mControllerKeyMapping = new ArrayList<>(); - mControllerAxisMapping = new ArrayList<>(); - - final int[] deviceIds = InputDevice.getDeviceIds(); - for (int deviceId : deviceIds) { - if (!isJoystickDevice(deviceId)) - continue; - - InputDevice device = InputDevice.getDevice(deviceId); - List motionRanges = device.getMotionRanges(); - int controllerIndex = 0; - - for (int keyCode : buttonKeyCodes) { - addControllerKeyMapping(deviceId, keyCode, controllerIndex); - } - - if (motionRanges != null) { - for (int axisCode : axisCodes) { - addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex); - } - } - } - - return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty(); - } - } diff --git a/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml b/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml new file mode 100644 index 000000000..a9600b74e --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_vibration_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 02f42e232..cc92a4237 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -266,4 +266,5 @@ DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress. Confirm Logout After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost. + Device for Vibration