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/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 96af607bb..be35c71fe 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,9 +43,9 @@ android:value="com.github.stenzek.duckstation.MainActivity" /> 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..842943bc1 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 @@ -3,6 +3,7 @@ package com.github.stenzek.duckstation; import android.content.Context; import android.content.SharedPreferences; import android.util.AttributeSet; +import android.view.InputDevice; import android.widget.ImageView; import android.widget.TextView; @@ -14,16 +15,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 +74,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 +113,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,41 +123,102 @@ 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(); } + private String prettyPrintBinding(String value) { + final int index = value.indexOf('/'); + String device, binding; + if (index >= 0) { + device = value.substring(0, index); + binding = value.substring(index); + } else { + device = value; + binding = ""; + } + + String humanName = device; + int deviceIndex = -1; + + final int[] deviceIds = InputDevice.getDeviceIds(); + for (int i = 0; i < deviceIds.length; i++) { + final InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]); + if (inputDevice == null || !inputDevice.getDescriptor().equals(device)) { + continue; + } + + humanName = inputDevice.getName(); + deviceIndex = i; + break; + } + + final int MAX_LENGTH = 40; + if (humanName.length() > MAX_LENGTH) { + final StringBuilder shortenedName = new StringBuilder(); + shortenedName.append(humanName, 0, MAX_LENGTH / 2); + shortenedName.append("..."); + shortenedName.append(humanName, humanName.length() - (MAX_LENGTH / 2), + humanName.length()); + + humanName = shortenedName.toString(); + } + + if (deviceIndex < 0) + return String.format("%s[??]%s", humanName, binding); + else + return String.format("%s[%d]%s", humanName, deviceIndex, binding); + } + private void updateValue(String value) { mValue = value; if (mValueView != null) { @@ -157,7 +237,7 @@ public class ControllerBindingPreference extends Preference { for (String value : values) { if (sb.length() > 0) sb.append(", "); - sb.append(value); + sb.append(prettyPrintBinding(value)); } updateValue(sb.toString()); 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 deleted file mode 100644 index 70fb3642a..000000000 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerMappingActivity.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.github.stenzek.duckstation; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentFactory; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import java.util.ArrayList; - -public class ControllerMappingActivity extends AppCompatActivity { - - private static final int NUM_CONTROLLER_PORTS = 2; - - private ArrayList mPreferences = new ArrayList<>(); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.settings_activity); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.settings, new SettingsCollectionFragment(this)) - .commit(); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.controller_mapping_activity_title); - } - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.remove("android:support:fragments"); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_controller_mapping, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - final int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_load_profile) { - doLoadProfile(); - return true; - } else if (id == R.id.action_save_profile) { - doSaveProfile(); - return true; - } else if (id == R.id.action_clear_bindings) { - doClearBindings(); - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - private void displayError(String text) { - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_activity_error) - .setMessage(text) - .setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss())) - .create() - .show(); - } - - private void doLoadProfile() { - final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames(); - if (profileNames == null) { - displayError(getString(R.string.controller_mapping_activity_no_profiles_found)); - return; - } - - new AlertDialog.Builder(this) - .setTitle(R.string.controller_mapping_activity_select_input_profile) - .setItems(profileNames, (dialog, choice) -> { - doLoadProfile(profileNames[choice]); - dialog.dismiss(); - }) - .setNegativeButton("Cancel", ((dialog, which) -> dialog.dismiss())) - .create() - .show(); - } - - private void doLoadProfile(String profileName) { - if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) { - displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName)); - return; - } - - updateAllBindings(); - } - - private void doSaveProfile() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final EditText input = new EditText(this); - builder.setTitle(R.string.controller_mapping_activity_input_profile_name); - builder.setView(input); - builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> { - final String name = input.getText().toString(); - if (name.isEmpty()) { - displayError(getString(R.string.controller_mapping_activity_name_must_be_provided)); - return; - } - - if (!AndroidHostInterface.getInstance().saveInputProfile(name)) { - displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile)); - return; - } - - Toast.makeText(ControllerMappingActivity.this, String.format(ControllerMappingActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name), - Toast.LENGTH_LONG).show(); - }); - builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss()); - builder.create().show(); - } - - private void doClearBindings() { - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(this).edit(); - for (ControllerBindingPreference pref : mPreferences) - pref.clearBinding(prefEdit); - prefEdit.commit(); - } - - private void updateAllBindings() { - for (ControllerBindingPreference pref : mPreferences) - pref.updateValue(); - } - - public static class ControllerPortFragment extends PreferenceFragmentCompat { - private ControllerMappingActivity activity; - private int controllerIndex; - - public ControllerPortFragment(ControllerMappingActivity activity, int controllerIndex) { - this.activity = activity; - this.controllerIndex = controllerIndex; - } - - @Override - 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 PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - if (controllerButtons != null) { - for (String buttonName : controllerButtons) { - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initButton(controllerIndex, buttonName); - ps.addPreference(cbp); - activity.mPreferences.add(cbp); - } - } - if (axisButtons != null) { - for (String axisName : axisButtons) { - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initAxis(controllerIndex, axisName); - ps.addPreference(cbp); - activity.mPreferences.add(cbp); - } - } - - setPreferenceScreen(ps); - } - } - - public static class HotkeyFragment extends PreferenceFragmentCompat { - private ControllerMappingActivity activity; - private HotkeyInfo[] mHotkeyInfo; - - public HotkeyFragment(ControllerMappingActivity activity) { - this.activity = activity; - this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList(); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); - if (mHotkeyInfo != null) { - for (HotkeyInfo hotkeyInfo : mHotkeyInfo) { - final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); - cbp.initHotkey(hotkeyInfo); - ps.addPreference(cbp); - activity.mPreferences.add(cbp); - } - } - - setPreferenceScreen(ps); - } - } - - public static class SettingsCollectionFragment extends Fragment { - private ControllerMappingActivity activity; - private SettingsCollectionAdapter adapter; - private ViewPager2 viewPager; - - public SettingsCollectionFragment(ControllerMappingActivity activity) { - this.activity = activity; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_controller_mapping, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - adapter = new SettingsCollectionAdapter(activity, this); - viewPager = view.findViewById(R.id.view_pager); - viewPager.setAdapter(adapter); - - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { - if (position == NUM_CONTROLLER_PORTS) - tab.setText("Hotkeys"); - else - tab.setText(String.format("Port %d", position + 1)); - }).attach(); - } - } - - public static class SettingsCollectionAdapter extends FragmentStateAdapter { - private ControllerMappingActivity activity; - - public SettingsCollectionAdapter(@NonNull ControllerMappingActivity activity, @NonNull Fragment fragment) { - super(fragment); - this.activity = activity; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - if (position != NUM_CONTROLLER_PORTS) - return new ControllerPortFragment(activity, position + 1); - else - return new HotkeyFragment(activity); - } - - @Override - public int getItemCount() { - return NUM_CONTROLLER_PORTS + 1; - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java new file mode 100644 index 000000000..16827450c --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java @@ -0,0 +1,445 @@ +package com.github.stenzek.duckstation; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.ArrayList; +import java.util.HashMap; + +public class ControllerSettingsActivity extends AppCompatActivity { + + private static final int NUM_CONTROLLER_PORTS = 2; + public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode"; + + private ArrayList mPreferences = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings, new SettingsCollectionFragment(this)) + .commit(); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(R.string.controller_mapping_activity_title); + } + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.remove("android:support:fragments"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_controller_mapping, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + + final int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_load_profile) { + doLoadProfile(); + return true; + } else if (id == R.id.action_save_profile) { + doSaveProfile(); + return true; + } else if (id == R.id.action_clear_bindings) { + doClearBindings(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private void displayError(String text) { + new AlertDialog.Builder(this) + .setTitle(R.string.emulation_activity_error) + .setMessage(text) + .setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss())) + .create() + .show(); + } + + private void doLoadProfile() { + final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames(); + if (profileNames == null) { + displayError(getString(R.string.controller_mapping_activity_no_profiles_found)); + return; + } + + new AlertDialog.Builder(this) + .setTitle(R.string.controller_mapping_activity_select_input_profile) + .setItems(profileNames, (dialog, choice) -> { + doLoadProfile(profileNames[choice]); + dialog.dismiss(); + }) + .setNegativeButton(R.string.controller_mapping_activity_cancel, ((dialog, which) -> dialog.dismiss())) + .create() + .show(); + } + + private void doLoadProfile(String profileName) { + if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) { + displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName)); + return; + } + + updateAllBindings(); + } + + private void doSaveProfile() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + final EditText input = new EditText(this); + builder.setTitle(R.string.controller_mapping_activity_input_profile_name); + builder.setView(input); + builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> { + final String name = input.getText().toString(); + if (name.isEmpty()) { + displayError(getString(R.string.controller_mapping_activity_name_must_be_provided)); + return; + } + + if (!AndroidHostInterface.getInstance().saveInputProfile(name)) { + displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile)); + return; + } + + Toast.makeText(ControllerSettingsActivity.this, String.format(ControllerSettingsActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name), + Toast.LENGTH_LONG).show(); + }); + builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss()); + builder.create().show(); + } + + private void doClearBindings() { + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(this).edit(); + for (ControllerBindingPreference pref : mPreferences) + pref.clearBinding(prefEdit); + prefEdit.commit(); + } + + private void updateAllBindings() { + for (ControllerBindingPreference pref : mPreferences) + pref.updateValue(); + } + + public static class SettingsFragment extends PreferenceFragmentCompat { + ControllerSettingsActivity parent; + + public SettingsFragment(ControllerSettingsActivity parent) { + this.parent = parent; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.controllers_preferences, rootKey); + + final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY); + if (multitapModePreference != null) { + multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> { + parent.recreate(); + return true; + }); + } + } + } + + public static class ControllerPortFragment extends PreferenceFragmentCompat { + private ControllerSettingsActivity activity; + private int controllerIndex; + private PreferenceCategory mButtonsCategory; + private PreferenceCategory mAxisCategory; + private PreferenceCategory mSettingsCategory; + + public ControllerPortFragment(ControllerSettingsActivity activity, int controllerIndex) { + this.activity = activity; + this.controllerIndex = controllerIndex; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); + setPreferenceScreen(ps); + createPreferences(); + } + + private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) { + final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext()); + pref.setKey(key); + pref.setTitle(title); + pref.setSummary(summary); + pref.setIconSpaceReserved(false); + pref.setDefaultValue(defaultValue); + return pref; + } + + private void createPreferences() { + final PreferenceScreen ps = getPreferenceScreen(); + final SharedPreferences sp = getPreferenceManager().getSharedPreferences(); + final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None"; + final String controllerTypeKey = String.format("Controller%d/Type", controllerIndex); + final String controllerType = sp.getString(controllerTypeKey, defaultControllerType); + final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType); + final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType); + final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType); + + final ListPreference typePreference = new ListPreference(getContext()); + typePreference.setEntries(R.array.settings_controller_type_entries); + typePreference.setEntryValues(R.array.settings_controller_type_values); + typePreference.setKey(controllerTypeKey); + typePreference.setTitle(R.string.settings_controller_type); + typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + typePreference.setIconSpaceReserved(false); + typePreference.setOnPreferenceChangeListener((pref, value) -> { + removePreferences(); + createPreferences(value.toString()); + return true; + }); + ps.addPreference(typePreference); + + mButtonsCategory = new PreferenceCategory(getContext()); + mButtonsCategory.setTitle(getContext().getString(R.string.controller_settings_category_button_bindings)); + mButtonsCategory.setIconSpaceReserved(false); + ps.addPreference(mButtonsCategory); + + mAxisCategory = new PreferenceCategory(getContext()); + mAxisCategory.setTitle(getContext().getString(R.string.controller_settings_category_axis_bindings)); + mAxisCategory.setIconSpaceReserved(false); + ps.addPreference(mAxisCategory); + + mSettingsCategory = new PreferenceCategory(getContext()); + mSettingsCategory.setTitle(getContext().getString(R.string.controller_settings_category_settings)); + mSettingsCategory.setIconSpaceReserved(false); + ps.addPreference(mSettingsCategory); + + createPreferences(controllerType); + } + + private void createPreferences(String controllerType) { + final PreferenceScreen ps = getPreferenceScreen(); + final SharedPreferences sp = getPreferenceManager().getSharedPreferences(); + final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType); + final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType); + final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType); + + if (controllerButtons != null) { + for (String buttonName : controllerButtons) { + final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); + cbp.initButton(controllerIndex, buttonName); + mButtonsCategory.addPreference(cbp); + activity.mPreferences.add(cbp); + } + } + + if (axisButtons != null) { + for (String axisName : axisButtons) { + final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); + cbp.initAxis(controllerIndex, axisName); + mAxisCategory.addPreference(cbp); + activity.mPreferences.add(cbp); + } + } + + if (vibrationMotors > 0) { + final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); + cbp.initVibration(controllerIndex); + mSettingsCategory.addPreference(cbp); + activity.mPreferences.add(cbp); + } + + if (controllerType.equals("AnalogController")) { + mSettingsCategory.addPreference( + createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex), + R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_enable_analog_mode_on_reset, true)); + + mSettingsCategory.addPreference( + createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex), + R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_use_analog_sticks_for_dpad, true)); + } + } + + private void removePreferences() { + for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) { + activity.mPreferences.remove(mButtonsCategory.getPreference(i)); + } + mButtonsCategory.removeAll(); + + for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) { + activity.mPreferences.remove(mAxisCategory.getPreference(i)); + } + mAxisCategory.removeAll(); + + for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) { + activity.mPreferences.remove(mSettingsCategory.getPreference(i)); + } + mSettingsCategory.removeAll(); + } + } + + public static class HotkeyFragment extends PreferenceFragmentCompat { + private ControllerSettingsActivity activity; + private HotkeyInfo[] mHotkeyInfo; + + public HotkeyFragment(ControllerSettingsActivity activity) { + this.activity = activity; + this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext()); + if (mHotkeyInfo != null) { + final HashMap categoryMap = new HashMap<>(); + + for (HotkeyInfo hotkeyInfo : mHotkeyInfo) { + PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ? + categoryMap.get(hotkeyInfo.getCategory()) : null; + if (category == null) { + category = new PreferenceCategory(getContext()); + category.setTitle(hotkeyInfo.getCategory()); + category.setIconSpaceReserved(false); + categoryMap.put(hotkeyInfo.getCategory(), category); + ps.addPreference(category); + } + + final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null); + cbp.initHotkey(hotkeyInfo); + category.addPreference(cbp); + activity.mPreferences.add(cbp); + } + } + + setPreferenceScreen(ps); + } + } + + public static class SettingsCollectionFragment extends Fragment { + private ControllerSettingsActivity activity; + private SettingsCollectionAdapter adapter; + private ViewPager2 viewPager; + private String[] controllerPortNames; + + private static final int NUM_MAIN_CONTROLLER_PORTS = 2; + private static final int NUM_SUB_CONTROLLER_PORTS = 4; + private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[] {'A', 'B', 'C', 'D'}; + + public SettingsCollectionFragment(ControllerSettingsActivity activity) { + this.activity = activity; + + final String multitapMode = PreferenceManager.getDefaultSharedPreferences(activity).getString( + MULTITAP_MODE_SETTINGS_KEY, "Disabled"); + + final ArrayList portNames = new ArrayList<>(); + for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) { + final boolean isMultitap = (multitapMode.equals("BothPorts") || + (i == 0 && multitapMode.equals("Port1Only")) || + (i == 1 && multitapMode.equals("Port2Only"))); + + if (isMultitap) { + for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) { + portNames.add(activity.getString( + R.string.controller_settings_sub_port_format, + i + 1, SUB_CONTROLLER_PORT_NAMES[j])); + } + } else { + portNames.add(activity.getString( + R.string.controller_settings_main_port_format, + i + 1)); + } + } + + controllerPortNames = new String[portNames.size()]; + portNames.toArray(controllerPortNames); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_controller_settings, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + adapter = new SettingsCollectionAdapter(activity, this, controllerPortNames.length); + viewPager = view.findViewById(R.id.view_pager); + viewPager.setAdapter(adapter); + + TabLayout tabLayout = view.findViewById(R.id.tab_layout); + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + if (position == 0) + tab.setText(R.string.controller_settings_tab_settings); + else if (position <= controllerPortNames.length) + tab.setText(controllerPortNames[position - 1]); + else + tab.setText(R.string.controller_settings_tab_hotkeys); + }).attach(); + } + } + + public static class SettingsCollectionAdapter extends FragmentStateAdapter { + private ControllerSettingsActivity activity; + private int controllerPorts; + + public SettingsCollectionAdapter(@NonNull ControllerSettingsActivity activity, @NonNull Fragment fragment, int controllerPorts) { + super(fragment); + this.activity = activity; + this.controllerPorts = controllerPorts; + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) + return new SettingsFragment(activity); + else if (position <= controllerPorts) + return new ControllerPortFragment(activity, position); + else + return new HotkeyFragment(activity); + } + + @Override + public int getItemCount() { + return controllerPorts + 2; + } + } +} \ No newline at end of file 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/java/com/github/stenzek/duckstation/GamePropertiesActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java index 86165abca..cd0847d2f 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GamePropertiesActivity.java @@ -1,16 +1,11 @@ package com.github.stenzek.duckstation; -import android.content.SharedPreferences; import android.os.Bundle; -import android.util.Property; import android.view.LayoutInflater; -import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; import android.widget.ListAdapter; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.ListFragment; import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; @@ -28,8 +22,6 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import java.util.ArrayList; - public class GamePropertiesActivity extends AppCompatActivity { PropertyListAdapter mPropertiesListAdapter; GameListEntry mGameListEntry; @@ -183,7 +175,7 @@ public class GamePropertiesActivity extends AppCompatActivity { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_controller_mapping, container, false); + return inflater.inflate(R.layout.fragment_controller_settings, container, false); } @Override diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index 9db6d5e48..7f87cb476 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -26,8 +26,6 @@ import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentFactory; import androidx.preference.PreferenceManager; import java.io.ByteArrayOutputStream; @@ -237,8 +235,8 @@ public class MainActivity extends AppCompatActivity { Intent intent = new Intent(this, SettingsActivity.class); startActivityForResult(intent, REQUEST_SETTINGS); return true; - } else if (id == R.id.action_controller_mapping) { - Intent intent = new Intent(this, ControllerMappingActivity.class); + } else if (id == R.id.action_controller_settings) { + Intent intent = new Intent(this, ControllerSettingsActivity.class); startActivity(intent); return true; } else if (id == R.id.action_switch_view) { diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java index df106181f..16d879a7f 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java @@ -107,13 +107,10 @@ public class SettingsActivity extends AppCompatActivity { case 3: // Enhancements return new SettingsFragment(R.xml.enhancements_preferences); - case 4: // Controllers - return new SettingsFragment(R.xml.controllers_preferences); - - case 5: // Achievements + case 4: // Achievements return new AchievementSettingsFragment(); - case 6: // Advanced + case 5: // Advanced return new SettingsFragment(R.xml.advanced_preferences); default: @@ -123,7 +120,7 @@ public class SettingsActivity extends AppCompatActivity { @Override public int getItemCount() { - return 7; + return 6; } } } \ No newline at end of file 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/layout/fragment_controller_mapping.xml b/android/app/src/main/res/layout/fragment_controller_settings.xml similarity index 91% rename from android/app/src/main/res/layout/fragment_controller_mapping.xml rename to android/app/src/main/res/layout/fragment_controller_settings.xml index d60ed84ea..a47a49de4 100644 --- a/android/app/src/main/res/layout/fragment_controller_mapping.xml +++ b/android/app/src/main/res/layout/fragment_controller_settings.xml @@ -10,7 +10,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:tabTextAppearance="@style/TabTextAppearance" - app:tabMode="fixed" /> + app:tabMinWidth="150dp" + app:tabMode="scrollable" /> + android:orderInCategory="102" + android:title="@string/action_controller_mapping" + app:showAsAction="ifRoom" /> diff --git a/android/app/src/main/res/values-es/arrays.xml b/android/app/src/main/res/values-es/arrays.xml index bdf4c4e08..a5724e92e 100644 --- a/android/app/src/main/res/values-es/arrays.xml +++ b/android/app/src/main/res/values-es/arrays.xml @@ -69,8 +69,11 @@ xBR (Sin unión de bordes) + None Control Digital (Mando) Control Analógico (DualShock) + Analog Joystick + NeGcon Sin tarjeta de memoria @@ -139,7 +142,6 @@ Pantalla Audio Mejoras - Controles Achievements Avanzado diff --git a/android/app/src/main/res/values-it/arrays.xml b/android/app/src/main/res/values-it/arrays.xml index a0fb37e2d..abaadd120 100644 --- a/android/app/src/main/res/values-it/arrays.xml +++ b/android/app/src/main/res/values-it/arrays.xml @@ -69,8 +69,11 @@ xBR (No Blending Bordi) + None Controller Digitale (Gamepad) Controller Analogico (DualShock) + Analog Joystick + NeGcon No Memory Card @@ -139,7 +142,6 @@ Display Audio Miglioramenti - Controller Achievements Avanzate diff --git a/android/app/src/main/res/values-nl/arrays.xml b/android/app/src/main/res/values-nl/arrays.xml index bfb95f2a7..0d36a1d14 100644 --- a/android/app/src/main/res/values-nl/arrays.xml +++ b/android/app/src/main/res/values-nl/arrays.xml @@ -69,8 +69,11 @@ xBR (Geen Edge Blending) + None Digitale Controller (Gamepad) Analoge Controller (DualShock) + Analog Joystick + NeGcon Geen Geheugenkaart @@ -139,7 +142,6 @@ Weergave Audio Verbeteringen - Controllers Achievements Geavanceerd diff --git a/android/app/src/main/res/values-pt-rBR/arrays.xml b/android/app/src/main/res/values-pt-rBR/arrays.xml index ba286ee0d..6a43906b7 100644 --- a/android/app/src/main/res/values-pt-rBR/arrays.xml +++ b/android/app/src/main/res/values-pt-rBR/arrays.xml @@ -69,8 +69,11 @@ xBR (Sem ajustes laterais) + None Controle Digital (Gamepad) Controle Analógico (DualShock) + Analog Joystick + NeGcon Sem Cartão de Memória @@ -139,7 +142,6 @@ Vídeo Áudio Melhorias - Controles Achievements Avançado diff --git a/android/app/src/main/res/values-ru/arrays.xml b/android/app/src/main/res/values-ru/arrays.xml index 7b3d17d11..768d9e6e0 100644 --- a/android/app/src/main/res/values-ru/arrays.xml +++ b/android/app/src/main/res/values-ru/arrays.xml @@ -69,8 +69,11 @@ xBR (без сглаживания краёв) + None Цифровой Аналоговый (DualShock) + Analog Joystick + NeGcon Без карты памяти @@ -145,7 +148,6 @@ Экран Звук Улучшения - Контроллеры Достижения Расширенные diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index 450aa5559..f9573a275 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -138,12 +138,18 @@ xBRBinAlpha + None Digital Controller (Gamepad) Analog Controller (DualShock) + Analog Joystick + NeGcon + None DigitalController AnalogController + AnalogJoystick + NeGcon No Memory Card @@ -265,7 +271,6 @@ Display Audio Enhancements - Controllers Achievements Advanced @@ -467,4 +472,16 @@ false true + + Disabled + Enable on Port 1 Only + Enable on Port 2 Only + Enable on Ports 1 and 2 + + + Disabled + Port1Only + Port2Only + BothPorts + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 02f42e232..861f7bc3e 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ DuckStation Settings - Controller Mapping + Controller Settings Settings Console Region Enable TTY Output @@ -167,7 +167,7 @@ ]]> Cancel Clear - Controller Mapping + Controller Settings No profiles found. Select Input Profile Failed to load profile \'%s\' @@ -266,4 +266,14 @@ 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 + Settings + Hotkeys + Button Bindings + Axis Bindings + Settings + Touchscreen Controller + Ports + Port %d + Port %1$d%2$c diff --git a/android/app/src/main/res/xml/controllers_preferences.xml b/android/app/src/main/res/xml/controllers_preferences.xml index 77953e447..521f0409e 100644 --- a/android/app/src/main/res/xml/controllers_preferences.xml +++ b/android/app/src/main/res/xml/controllers_preferences.xml @@ -14,88 +14,72 @@ ~ limitations under the License. --> - + - - - - - - - - - + + + + + + + - - - - - - + + + + + diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 4958ec047..36c53fcba 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1538,21 +1538,9 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const return false; } - if (StringUtil::StartsWith(device, "Controller")) + std::optional controller_index; + if (m_controller_interface && (controller_index = m_controller_interface->GetControllerIndex(device))) { - if (!m_controller_interface) - { - Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str()); - return false; - } - - const std::optional controller_index = StringUtil::FromChars(device.substr(10)); - if (!controller_index || *controller_index < 0) - { - Log_WarningPrintf("Invalid controller index in button binding '%s'", binding.c_str()); - return false; - } - if (StringUtil::StartsWith(button, "Button")) { const std::optional button_index = StringUtil::FromChars(button.substr(6)); @@ -1652,21 +1640,9 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st } } - if (StringUtil::StartsWith(device, "Controller")) + std::optional controller_index; + if (m_controller_interface && (controller_index = m_controller_interface->GetControllerIndex(device))) { - if (!m_controller_interface) - { - Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str()); - return false; - } - - const std::optional controller_index = StringUtil::FromChars(device.substr(10)); - if (!controller_index || *controller_index < 0) - { - Log_WarningPrintf("Invalid controller index in axis binding '%s'", binding.c_str()); - return false; - } - if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") || StringUtil::StartsWith(axis, "-Axis")) { @@ -1723,21 +1699,9 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st bool CommonHostInterface::AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors) { - if (StringUtil::StartsWith(binding, "Controller")) + std::optional host_controller_index; + if (m_controller_interface && (host_controller_index = m_controller_interface->GetControllerIndex(binding))) { - if (!m_controller_interface) - { - Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str()); - return false; - } - - const std::optional host_controller_index = StringUtil::FromChars(binding.substr(10)); - if (!host_controller_index || *host_controller_index < 0) - { - Log_WarningPrintf("Invalid controller index in rumble binding '%s'", binding.c_str()); - return false; - } - AddControllerRumble(controller_index, num_motors, std::bind(&ControllerInterface::SetControllerRumbleStrength, m_controller_interface.get(), host_controller_index.value(), std::placeholders::_1, std::placeholders::_2)); diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index cd835188a..b61ea5974 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -356,7 +356,7 @@ protected: void RegisterHotkey(String category, String name, String display_name, InputButtonHandler handler); bool HandleHostKeyEvent(HostKeyCode code, HostKeyCode modifiers, bool pressed); bool HandleHostMouseEvent(HostMouseButton button, bool pressed); - void UpdateInputMap(SettingsInterface& si); + virtual void UpdateInputMap(SettingsInterface& si); void ClearInputMap(); void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback); diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp index d333292da..90134011f 100644 --- a/src/frontend-common/controller_interface.cpp +++ b/src/frontend-common/controller_interface.cpp @@ -22,6 +22,22 @@ void ControllerInterface::Shutdown() m_host_interface = nullptr; } +std::optional ControllerInterface::GetControllerIndex(const std::string_view& device) +{ + if (!StringUtil::StartsWith(device, "Controller")) + return std::nullopt; + + const std::optional controller_index = StringUtil::FromChars(device.substr(10)); + if (!controller_index || *controller_index < 0) + { + Log_WarningPrintf("Invalid controller index in button binding '%*s'", static_cast(device.length()), + device.data()); + return std::nullopt; + } + + return controller_index; +} + void ControllerInterface::SetHook(Hook::Callback callback) { std::unique_lock lock(m_event_intercept_mutex); diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h index feae1c2e8..f97d0f6cd 100644 --- a/src/frontend-common/controller_interface.h +++ b/src/frontend-common/controller_interface.h @@ -6,6 +6,7 @@ #include #include #include +#include #include class HostInterface; @@ -70,6 +71,7 @@ public: virtual void ClearBindings() = 0; // Binding to events. If a binding for this axis/button already exists, returns false. + virtual std::optional GetControllerIndex(const std::string_view& device); virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0; virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0; virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,