Android: Rewrite input binding to be more flexible

Supports vibration, non-gamepad controllers, etc.

You will need to rebind your controllers.
This commit is contained in:
Connor McLaughlin 2021-03-14 16:19:19 +10:00
parent ee171465ea
commit 1839bfab3b
12 changed files with 542 additions and 374 deletions

View file

@ -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<std::mutex> 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<int> AndroidControllerInterface::GetControllerIndex(const std::string_view& device)
{
std::unique_lock<std::mutex> lock(m_controllers_mutex);
for (u32 i = 0; i < static_cast<u32>(m_device_names.size()); i++)
{
if (device == m_device_names[i])
return static_cast<int>(i);
}
return std::nullopt;
}
bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
AxisCallback callback)
{
std::unique_lock<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(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<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(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<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(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<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(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<std::string> device_names)
{
Log_DevPrintf("controller %u axis %u %f", index, static_cast<u32>(axis), value);
DebugAssert(index < NUM_CONTROLLERS);
std::unique_lock<std::mutex> 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<u32>(axis), value))
return true;
void AndroidControllerInterface::SetDeviceRumble(u32 index, bool has_vibrator)
{
std::unique_lock<std::mutex> lock(m_controllers_mutex);
if (index >= m_controllers.size())
return;
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
m_controllers[index].has_rumble = has_vibrator;
}
void AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value)
{
std::unique_lock<std::mutex> 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())
{
const AxisCallback& cb = am_iter->second[AxisSide::Full];
if (cb)
{
cb(value);
return true;
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<u32>(axis)][BoolToUInt8(!positive)];
const ButtonCallback& button_cb =
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(positive)];
const auto bm_iter = cd.axis_button_mapping.find(axis);
if (bm_iter != cd.axis_button_mapping.end())
{
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 true;
return;
}
else if (other_button_cb)
{
other_button_cb(false);
return true;
return;
}
else
{
return false;
}
}
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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(controller_index) >= m_controllers.size())
return false;
return m_controllers[static_cast<u32>(controller_index)].has_rumble ? NUM_RUMBLE_MOTORS : 0;
}
void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
u32 num_motors)
{
std::unique_lock<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(controller_index) >= m_controllers.size())
return;
const float small_motor = strengths[0];
const float large_motor = strengths[1];
static_cast<AndroidHostInterface*>(m_host_interface)
->SetControllerVibration(static_cast<u32>(controller_index), small_motor, large_motor);
}
bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
{
if (static_cast<u32>(controller_index) >= NUM_CONTROLLERS)
std::unique_lock<std::mutex> lock(m_controllers_mutex);
if (static_cast<u32>(controller_index) >= m_controllers.size())
return false;
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);

View file

@ -3,6 +3,7 @@
#include "frontend-common/controller_interface.h"
#include <array>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
@ -12,6 +13,8 @@ public:
AndroidControllerInterface();
~AndroidControllerInterface() override;
ALWAYS_INLINE u32 GetControllerCount() const { return static_cast<u32>(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<int> 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<std::string> 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<std::array<AxisCallback, 3>, NUM_AXISES> axis_mapping;
std::array<ButtonCallback, NUM_BUTTONS> button_mapping;
std::array<std::array<ButtonCallback, 2>, NUM_AXISES> axis_button_mapping;
std::array<AxisCallback, NUM_BUTTONS> button_axis_mapping;
std::map<u32, std::array<AxisCallback, 3>> axis_mapping;
std::map<u32, ButtonCallback> button_mapping;
std::map<u32, std::array<ButtonCallback, 2>> axis_button_mapping;
std::map<u32, AxisCallback> button_axis_mapping;
bool has_rumble = false;
};
using ControllerDataArray = std::array<ControllerData, NUM_CONTROLLERS>;
ControllerDataArray m_controllers;
std::vector<std::string> m_device_names;
std::vector<ControllerData> m_controllers;
std::mutex m_controllers_mutex;
std::mutex m_event_intercept_mutex;
Hook::Callback m_event_intercept_callback;

View file

@ -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<std::string> device_names;
jobjectArray const java_names = reinterpret_cast<jobjectArray>(
env->CallObjectMethod(m_emulation_activity_object, s_EmulationActivity_method_getInputDeviceNames));
if (java_names)
{
const u32 count = static_cast<u32>(env->GetArrayLength(java_names));
for (u32 i = 0; i < count; i++)
{
device_names.push_back(
AndroidHelpers::JStringToString(env, reinterpret_cast<jstring>(env->GetObjectArrayElement(java_names, i))));
}
env->DeleteLocalRef(java_names);
}
if (m_controller_interface)
{
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(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<jint>(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<AndroidControllerInterface*>(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<jint>(controller_index), static_cast<jfloat>(small_motor),
static_cast<jfloat>(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, "<init>", "(ILjava/lang/String;Z)V")) == nullptr ||
(s_GameListEntry_constructor = env->GetMethodID(
s_GameListEntry_class, "<init>",
@ -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<ControllerType> type =
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
if (!type)
return 0;
return static_cast<jint>(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<u32>(controller_index),
static_cast<u32>(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<u32>(controller_index),
static_cast<u32>(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<u32>(controller_index),
static_cast<u32>(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);

View file

@ -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();

View file

@ -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);

View file

@ -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<Integer, float[]> 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<Integer, float[]> 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<InputDevice.MotionRange> 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;

View file

@ -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,8 +73,17 @@ public class ControllerBindingPreference extends Preference {
}
private static int getIconForHotkey(String hotkeyDisplayName) {
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) {
this(context, attrs, 0);
@ -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();
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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;
List<InputDevice.MotionRange> 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();
}
public int deviceId;
public int deviceAxisOrButton;
public int controllerIndex;
public int buttonMapping;
// device.getVibrator() always returns null, but might return a "null vibrator".
final Vibrator potentialVibrator = device.getVibrator();
if (potentialVibrator != null && potentialVibrator.hasVibrator())
vibrator = potentialVibrator;
}
}
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;
private InputDeviceData[] mInputDevices = null;
private boolean mHasAnyGamepads = false;
public boolean hasAnyGamePads() {
return mHasAnyGamepads;
}
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 synchronized void updateInputDevices() {
mInputDevices = null;
mHasAnyGamepads = false;
public int deviceId;
public int deviceAxisOrButton;
public InputDevice.MotionRange deviceMotionRange;
public int controllerIndex;
public int axisMapping;
public int positiveButton;
public int negativeButton;
}
private ArrayList<ButtonMapping> mControllerKeyMapping;
private ArrayList<AxisMapping> 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)
final ArrayList<InputDeviceData> inputDeviceIds = new ArrayList<>();
for (int deviceId : InputDevice.getDeviceIds()) {
final InputDevice device = InputDevice.getDevice(deviceId);
if (device == null || !isBindableDevice(device))
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));
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));
}
result = true;
if (inputDeviceIds.isEmpty())
return;
mInputDevices = new InputDeviceData[inputDeviceIds.size()];
inputDeviceIds.toArray(mInputDevices);
}
return result;
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 (mapping.axisMapping >= 0) {
AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, 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));
}
}
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<InputDevice.MotionRange> 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))
if (data.axisValues[i] == emuValue)
continue;
InputDevice device = InputDevice.getDevice(deviceId);
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
int controllerIndex = 0;
/*Log.d("EmulationSurfaceView",
String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));*/
for (int keyCode : buttonKeyCodes) {
addControllerKeyMapping(deviceId, keyCode, controllerIndex);
data.axisValues[i] = emuValue;
AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue);
}
if (motionRanges != null) {
for (int axisCode : axisCodes) {
addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex);
return true;
}
}
}
return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M0,15h2L2,9L0,9v6zM3,17h2L5,7L3,7v10zM22,9v6h2L24,9h-2zM19,17h2L21,7h-2v10zM16.5,3h-9C6.67,3 6,3.67 6,4.5v15c0,0.83 0.67,1.5 1.5,1.5h9c0.83,0 1.5,-0.67 1.5,-1.5v-15c0,-0.83 -0.67,-1.5 -1.5,-1.5zM16,19L8,19L8,5h8v14z"/>
</vector>

View file

@ -266,4 +266,5 @@
<string name="settings_achievements_disclaimer">DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress.</string>
<string name="settings_achievements_confirm_logout_title">Confirm Logout</string>
<string name="settings_achievements_confirm_logout_message">After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost.</string>
<string name="controller_binding_device_for_vibration">Device for Vibration</string>
</resources>