Android: Rework input device hotplug

Previously, when an input device was connected or disconnected, we would
recreate all devices. This commit makes it so we only touch the relevant
device instead. This matters because recreating a device causes us to
drop all held buttons for that device. Due to Android only delivering
inputs as events, we're unable to poll for currently held buttons when
recreating a device.

This recently became a problem for users of Ayn devices due to a
firmware update. Every now and then, something about the display
viewports changes, triggering an update to an input device that I assume
is a touch input device. This input device isn't something users
normally map in Dolphin's controller settings, but it changing was
causing Dolphin to drop all held buttons for the device's built-in
gamepad as well as any other connected gamepads.
This commit is contained in:
JosJuice 2026-02-07 16:36:37 +01:00
parent a8fbe8f28f
commit bd92388d2f
2 changed files with 53 additions and 26 deletions

View File

@ -95,11 +95,6 @@ object ControllerInterface {
deviceQualifier: String, axisNames: Array<String>, suspended: Boolean
)
/**
* Rescans for input devices.
*/
external fun refreshDevices()
external fun getAllDeviceStrings(): Array<String>
external fun getDevice(deviceString: String): CoreDevice?
@ -193,13 +188,10 @@ object ControllerInterface {
}
private class InputDeviceListener : InputManager.InputDeviceListener {
// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceAdded(deviceId: Int) = refreshDevices()
override external fun onInputDeviceAdded(deviceId: Int)
// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceRemoved(deviceId: Int) = refreshDevices()
override external fun onInputDeviceRemoved(deviceId: Int)
// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceChanged(deviceId: Int) = refreshDevices()
override external fun onInputDeviceChanged(deviceId: Int)
}
}

View File

@ -31,6 +31,8 @@
namespace
{
std::string SOURCE = "Android";
jclass s_list_class;
jmethodID s_list_get;
jmethodID s_list_size;
@ -468,13 +470,13 @@ namespace ciface::Android
class InputBackend final : public ciface::InputBackend
{
public:
InputBackend(ControllerInterface* controller_interface);
explicit InputBackend(ControllerInterface* controller_interface);
~InputBackend();
void PopulateDevices() override;
private:
void AddDevice(JNIEnv* env, int device_id);
void AddSensorDevice(JNIEnv* env);
static void AddDevice(JNIEnv* env, int device_id);
static void AddSensorDevice(JNIEnv* env);
static void RemoveDevice(int device_id);
};
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
@ -611,10 +613,11 @@ private:
class AndroidDevice final : public Core::Device
{
public:
AndroidDevice(JNIEnv* env, jobject input_device)
AndroidDevice(JNIEnv* env, jint device_id, jobject input_device)
: m_sensor_event_listener(AddSensors(env, input_device)),
m_source(env->CallIntMethod(input_device, s_input_device_get_sources)),
m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number))
m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number)),
m_device_id(device_id)
{
jstring j_name =
reinterpret_cast<jstring>(env->CallObjectMethod(input_device, s_input_device_get_name));
@ -631,7 +634,7 @@ public:
// Constructor for the device added by Dolphin to contain sensor inputs
AndroidDevice(JNIEnv* env, std::string name)
: m_sensor_event_listener(AddSensors(env, nullptr)), m_source(AINPUT_SOURCE_SENSOR),
m_controller_number(0), m_name(std::move(name))
m_controller_number(0), m_device_id(std::nullopt), m_name(std::move(name))
{
AddSystemMotors(env);
}
@ -644,7 +647,7 @@ public:
std::string GetName() const override { return m_name; }
std::string GetSource() const override { return "Android"; }
std::string GetSource() const override { return SOURCE; }
std::optional<int> GetPreferredId() const override
{
@ -666,6 +669,8 @@ public:
return -3;
}
std::optional<jint> GetDeviceID() const { return m_device_id; }
jobject GetSensorEventListener() { return m_sensor_event_listener; }
private:
@ -801,6 +806,7 @@ private:
const jobject m_sensor_event_listener;
const int m_source;
const int m_controller_number;
const std::optional<jint> m_device_id;
std::string m_name;
};
@ -944,8 +950,12 @@ InputBackend::~InputBackend()
env->DeleteGlobalRef(s_keycodes_array);
}
void InputBackend::AddDevice(JNIEnv* env, int device_id)
void InputBackend::AddDevice(JNIEnv* env, jint device_id)
{
// Remove the device in case it already exists (maybe it's possible for a device to connect,
// be processed by PopulateDevices, and then be processed by onInputDeviceAdded)
RemoveDevice(device_id);
jobject input_device =
env->CallStaticObjectMethod(s_input_device_class, s_input_device_get_device, device_id);
@ -955,14 +965,14 @@ void InputBackend::AddDevice(JNIEnv* env, int device_id)
return;
}
auto device = std::make_shared<AndroidDevice>(env, input_device);
auto device = std::make_shared<AndroidDevice>(env, device_id, input_device);
env->DeleteLocalRef(input_device);
if (device->Inputs().empty() && device->Outputs().empty())
return;
GetControllerInterface().AddDevice(device);
g_controller_interface.AddDevice(device);
Core::DeviceQualifier qualifier;
qualifier.FromDevice(device.get());
@ -987,7 +997,7 @@ void InputBackend::AddSensorDevice(JNIEnv* env)
if (device->Inputs().empty() && device->Outputs().empty())
return;
GetControllerInterface().AddDevice(device);
g_controller_interface.AddDevice(device);
Core::DeviceQualifier qualifier;
qualifier.FromDevice(device.get());
@ -1000,6 +1010,16 @@ void InputBackend::AddSensorDevice(JNIEnv* env)
env->DeleteLocalRef(j_qualifier);
}
void InputBackend::RemoveDevice(jint device_id)
{
g_controller_interface.RemoveDevice([device_id](const ciface::Core::Device* device) {
return device->GetSource() == SOURCE &&
static_cast<const AndroidDevice*>(device)->GetDeviceID() == device_id;
});
s_device_id_to_device_qualifier.erase(device_id);
}
void InputBackend::PopulateDevices()
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "Android populating devices");
@ -1163,10 +1183,25 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_notifySe
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_refreshDevices(JNIEnv* env,
jclass)
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceAdded(
JNIEnv* env, jobject, jint device_id)
{
g_controller_interface.RefreshDevices();
ciface::Android::InputBackend::AddDevice(env, device_id);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceRemoved(
JNIEnv*, jobject, jint device_id)
{
ciface::Android::InputBackend::RemoveDevice(device_id);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceChanged(
JNIEnv* env, jobject, jint device_id)
{
// AddDevice will automatically remove the existing device
ciface::Android::InputBackend::AddDevice(env, device_id);
}
JNIEXPORT jobjectArray JNICALL