ControllerInterface/Android: Handle input events
Android doesn't let us poll inputs whenever we want. Instead, we listen to input events (activities will have to forward them to the input backend), and store the received values in atomic variables in the Input classes. This is similar in concept to how ButtonManager worked, but without its homegrown second input mapping system.
This commit is contained in:
parent
792cb62195
commit
ca508e4503
|
@ -11,7 +11,6 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -42,6 +41,7 @@ import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding;
|
|||
import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding;
|
||||
import org.dolphinemu.dolphinemu.databinding.DialogIrSensitivityBinding;
|
||||
import org.dolphinemu.dolphinemu.databinding.DialogSkylandersManagerBinding;
|
||||
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface;
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
|
||||
|
@ -61,7 +61,6 @@ import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer;
|
|||
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
||||
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider;
|
||||
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
|
||||
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
|
||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||
import org.dolphinemu.dolphinemu.utils.IniFile;
|
||||
import org.dolphinemu.dolphinemu.utils.ThemeHelper;
|
||||
|
@ -855,13 +854,15 @@ public final class EmulationActivity extends AppCompatActivity implements ThemeP
|
|||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event)
|
||||
{
|
||||
if (mMenuVisible || event.getKeyCode() == KeyEvent.KEYCODE_BACK)
|
||||
if (!mMenuVisible)
|
||||
{
|
||||
return super.dispatchKeyEvent(event);
|
||||
if (ControllerInterface.dispatchKeyEvent(event))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
return false;
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
private void toggleControls()
|
||||
|
@ -1217,13 +1218,15 @@ public final class EmulationActivity extends AppCompatActivity implements ThemeP
|
|||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event)
|
||||
{
|
||||
if (mMenuVisible)
|
||||
if (!mMenuVisible)
|
||||
{
|
||||
return false;
|
||||
if (ControllerInterface.dispatchGenericMotionEvent(event))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
return false;
|
||||
return super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
private void showSubMenu(SaveLoadStateFragment.SaveOrLoad saveOrLoad)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.input.model;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* This class interfaces with the native ControllerInterface,
|
||||
* which is where the emulator core gets inputs from.
|
||||
*/
|
||||
public final class ControllerInterface
|
||||
{
|
||||
/**
|
||||
* Activities which want to pass on inputs to native code
|
||||
* should call this in their own dispatchKeyEvent method.
|
||||
*
|
||||
* @return true if the emulator core seems to be interested in this event.
|
||||
* false if the event should be passed on to the default dispatchKeyEvent.
|
||||
*/
|
||||
public static native boolean dispatchKeyEvent(KeyEvent event);
|
||||
|
||||
/**
|
||||
* Activities which want to pass on inputs to native code
|
||||
* should call this in their own dispatchGenericMotionEvent method.
|
||||
*
|
||||
* @return true if the emulator core seems to be interested in this event.
|
||||
* false if the event should be passed on to the default dispatchGenericMotionEvent.
|
||||
*/
|
||||
public static native boolean dispatchGenericMotionEvent(MotionEvent event);
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "InputCommon/ControllerInterface/Android/Android.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
@ -15,6 +16,7 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
|
@ -31,6 +33,7 @@ jclass s_input_device_class;
|
|||
jmethodID s_input_device_get_device_ids;
|
||||
jmethodID s_input_device_get_device;
|
||||
jmethodID s_input_device_get_controller_number;
|
||||
jmethodID s_input_device_get_id;
|
||||
jmethodID s_input_device_get_motion_ranges;
|
||||
jmethodID s_input_device_get_name;
|
||||
jmethodID s_input_device_get_sources;
|
||||
|
@ -42,8 +45,21 @@ jmethodID s_motion_range_get_max;
|
|||
jmethodID s_motion_range_get_min;
|
||||
jmethodID s_motion_range_get_source;
|
||||
|
||||
jclass s_input_event_class;
|
||||
jmethodID s_input_event_get_device;
|
||||
|
||||
jclass s_key_event_class;
|
||||
jmethodID s_key_event_get_action;
|
||||
jmethodID s_key_event_get_keycode;
|
||||
|
||||
jclass s_motion_event_class;
|
||||
jmethodID s_motion_event_get_axis_value;
|
||||
jmethodID s_motion_event_get_source;
|
||||
|
||||
jintArray s_keycodes_array;
|
||||
|
||||
std::unordered_map<jint, ciface::Core::DeviceQualifier> s_device_id_to_device_qualifier;
|
||||
|
||||
constexpr int MAX_KEYCODE = AKEYCODE_PROFILE_SWITCH; // Up to date as of SDK 31
|
||||
|
||||
const std::array<std::string_view, MAX_KEYCODE + 1> KEYCODE_NAMES = {
|
||||
|
@ -379,47 +395,70 @@ std::string ConstructAxisName(int source, int axis, bool negative)
|
|||
return fmt::format("{}{}{}", ConstructAxisNamePrefix(source), axis, sign);
|
||||
}
|
||||
|
||||
std::shared_ptr<ciface::Core::Device> FindDevice(jint device_id)
|
||||
{
|
||||
const auto it = s_device_id_to_device_qualifier.find(device_id);
|
||||
if (it == s_device_id_to_device_qualifier.end())
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Could not find device ID {}", device_id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ciface::Core::DeviceQualifier& qualifier = it->second;
|
||||
std::shared_ptr<ciface::Core::Device> device = g_controller_interface.FindDevice(qualifier);
|
||||
if (!device)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Could not find device {}", qualifier.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ciface::Android
|
||||
{
|
||||
class AndroidKey final : public Core::Device::Input
|
||||
class AndroidInput : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
explicit AndroidKey(int keycode) : m_name(ConstructKeyName(keycode))
|
||||
explicit AndroidInput(std::string name) : m_name(std::move(name))
|
||||
{
|
||||
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "Created {}", m_name);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return m_name; }
|
||||
|
||||
ControlState GetState() const override
|
||||
{
|
||||
return 0; // TODO
|
||||
}
|
||||
ControlState GetState() const override { return m_state.load(std::memory_order_relaxed); }
|
||||
|
||||
void SetState(ControlState state) { m_state.store(state, std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::atomic<ControlState> m_state = 0;
|
||||
};
|
||||
|
||||
class AndroidAxis final : public Core::Device::Input
|
||||
class AndroidKey final : public AndroidInput
|
||||
{
|
||||
public:
|
||||
explicit AndroidKey(int keycode) : AndroidInput(ConstructKeyName(keycode)) {}
|
||||
};
|
||||
|
||||
class AndroidAxis final : public AndroidInput
|
||||
{
|
||||
public:
|
||||
AndroidAxis(int source, int axis, bool negative)
|
||||
: m_name(ConstructAxisName(source, axis, negative))
|
||||
: AndroidInput(ConstructAxisName(source, axis, negative)), m_negative(negative)
|
||||
{
|
||||
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "Created {}", m_name);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return m_name; };
|
||||
|
||||
ControlState GetState() const override
|
||||
{
|
||||
return 0; // TODO
|
||||
};
|
||||
return m_negative ? -AndroidInput::GetState() : AndroidInput::GetState();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
bool m_negative;
|
||||
};
|
||||
|
||||
class AndroidDevice final : public Core::Device
|
||||
|
@ -552,6 +591,7 @@ void Init()
|
|||
env->GetStaticMethodID(s_input_device_class, "getDevice", "(I)Landroid/view/InputDevice;");
|
||||
s_input_device_get_controller_number =
|
||||
env->GetMethodID(s_input_device_class, "getControllerNumber", "()I");
|
||||
s_input_device_get_id = env->GetMethodID(s_input_device_class, "getId", "()I");
|
||||
s_input_device_get_motion_ranges =
|
||||
env->GetMethodID(s_input_device_class, "getMotionRanges", "()Ljava/util/List;");
|
||||
s_input_device_get_name =
|
||||
|
@ -568,6 +608,21 @@ void Init()
|
|||
s_motion_range_get_source = env->GetMethodID(s_motion_range_class, "getSource", "()I");
|
||||
env->DeleteLocalRef(motion_range_class);
|
||||
|
||||
const jclass input_event_class = env->FindClass("android/view/InputEvent");
|
||||
s_input_event_class = reinterpret_cast<jclass>(env->NewGlobalRef(input_event_class));
|
||||
s_input_event_get_device =
|
||||
env->GetMethodID(s_input_event_class, "getDevice", "()Landroid/view/InputDevice;");
|
||||
|
||||
const jclass key_event_class = env->FindClass("android/view/KeyEvent");
|
||||
s_key_event_class = reinterpret_cast<jclass>(env->NewGlobalRef(key_event_class));
|
||||
s_key_event_get_action = env->GetMethodID(s_key_event_class, "getAction", "()I");
|
||||
s_key_event_get_keycode = env->GetMethodID(s_key_event_class, "getKeyCode", "()I");
|
||||
|
||||
const jclass motion_event_class = env->FindClass("android/view/MotionEvent");
|
||||
s_motion_event_class = reinterpret_cast<jclass>(env->NewGlobalRef(motion_event_class));
|
||||
s_motion_event_get_axis_value = env->GetMethodID(s_motion_event_class, "getAxisValue", "(I)F");
|
||||
s_motion_event_get_source = env->GetMethodID(s_motion_event_class, "getSource", "()I");
|
||||
|
||||
jintArray keycodes_array = CreateKeyCodesArray(env);
|
||||
s_keycodes_array = reinterpret_cast<jintArray>(env->NewGlobalRef(keycodes_array));
|
||||
env->DeleteLocalRef(keycodes_array);
|
||||
|
@ -579,6 +634,9 @@ void Shutdown()
|
|||
|
||||
env->DeleteGlobalRef(s_input_device_class);
|
||||
env->DeleteGlobalRef(s_motion_range_class);
|
||||
env->DeleteGlobalRef(s_input_event_class);
|
||||
env->DeleteGlobalRef(s_key_event_class);
|
||||
env->DeleteGlobalRef(s_motion_event_class);
|
||||
env->DeleteGlobalRef(s_keycodes_array);
|
||||
}
|
||||
|
||||
|
@ -591,8 +649,17 @@ static void AddDevice(JNIEnv* env, int device_id)
|
|||
|
||||
env->DeleteLocalRef(input_device);
|
||||
|
||||
if (!device->Inputs().empty() || !device->Outputs().empty())
|
||||
g_controller_interface.AddDevice(std::move(device));
|
||||
if (device->Inputs().empty() && device->Outputs().empty())
|
||||
return;
|
||||
|
||||
g_controller_interface.AddDevice(device);
|
||||
|
||||
Core::DeviceQualifier qualifier;
|
||||
qualifier.FromDevice(device.get());
|
||||
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "Added device ID {} as {}", device_id,
|
||||
device->GetQualifiedName());
|
||||
s_device_id_to_device_qualifier.emplace(device_id, qualifier);
|
||||
}
|
||||
|
||||
void PopulateDevices()
|
||||
|
@ -612,3 +679,93 @@ void PopulateDevices()
|
|||
}
|
||||
|
||||
} // namespace ciface::Android
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchKeyEvent(
|
||||
JNIEnv* env, jclass, jobject key_event)
|
||||
{
|
||||
const jint action = env->CallIntMethod(key_event, s_key_event_get_action);
|
||||
ControlState state;
|
||||
switch (action)
|
||||
{
|
||||
case AKEY_EVENT_ACTION_DOWN:
|
||||
state = 1;
|
||||
break;
|
||||
case AKEY_EVENT_ACTION_UP:
|
||||
state = 0;
|
||||
break;
|
||||
default:
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
const jobject input_device = env->CallObjectMethod(key_event, s_input_event_get_device);
|
||||
const jint device_id = env->CallIntMethod(input_device, s_input_device_get_id);
|
||||
env->DeleteLocalRef(input_device);
|
||||
const std::shared_ptr<ciface::Core::Device> device = FindDevice(device_id);
|
||||
if (!device)
|
||||
return JNI_FALSE;
|
||||
|
||||
const jint keycode = env->CallIntMethod(key_event, s_key_event_get_keycode);
|
||||
const std::string input_name = ConstructKeyName(keycode);
|
||||
|
||||
ciface::Core::Device::Input* input = device->FindInput(input_name);
|
||||
if (!input)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Could not find input {} in device {}", input_name,
|
||||
device->GetQualifiedName());
|
||||
return false;
|
||||
}
|
||||
|
||||
static_cast<ciface::Android::AndroidInput*>(input)->SetState(state);
|
||||
|
||||
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "Set {} of {} to {}", input_name, device->GetQualifiedName(),
|
||||
state);
|
||||
|
||||
// TODO: Return true when appropriate
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchGenericMotionEvent(
|
||||
JNIEnv* env, jclass, jobject motion_event)
|
||||
{
|
||||
const jobject input_device = env->CallObjectMethod(motion_event, s_input_event_get_device);
|
||||
const jint device_id = env->CallIntMethod(input_device, s_input_device_get_id);
|
||||
env->DeleteLocalRef(input_device);
|
||||
const std::shared_ptr<ciface::Core::Device> device = FindDevice(device_id);
|
||||
if (!device)
|
||||
return JNI_FALSE;
|
||||
|
||||
const jint source = env->CallIntMethod(motion_event, s_motion_event_get_source);
|
||||
const std::string axis_name_prefix = ConstructAxisNamePrefix(source);
|
||||
|
||||
for (ciface::Core::Device::Input* input : device->Inputs())
|
||||
{
|
||||
const std::string input_name = input->GetName();
|
||||
if (input_name.starts_with(axis_name_prefix))
|
||||
{
|
||||
const std::string axis_id_str = input_name.substr(
|
||||
axis_name_prefix.size(), input_name.size() - axis_name_prefix.size() - sizeof('+'));
|
||||
|
||||
int axis_id;
|
||||
if (!TryParse(axis_id_str, &axis_id))
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Failed to parse \"{}\" from \"{}\" as axis ID",
|
||||
axis_id_str, input_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
float value = env->CallFloatMethod(motion_event, s_motion_event_get_axis_value, axis_id);
|
||||
static_cast<ciface::Android::AndroidInput*>(input)->SetState(value);
|
||||
|
||||
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "Set {} of {} to {}", input_name,
|
||||
device->GetQualifiedName(), value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Return true when appropriate
|
||||
return JNI_FALSE;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue