diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt new file mode 100644 index 0000000000..e180814a58 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.model + +import androidx.annotation.Keep + +/** + * Waits for the user to press inputs, and reports which inputs were pressed. + * + * The caller is responsible for forwarding input events from Android to ControllerInterface + * and then calling [update]. + */ +class InputDetector { + @Keep + private val pointer: Long + + constructor() { + pointer = createNew() + } + + @Keep + private constructor(pointer: Long) { + this.pointer = pointer + } + + external fun finalize() + + private external fun createNew(): Long + + /** + * Starts a detection session. + * + * @param defaultDevice The device to detect inputs from. + * @param allDevices Whether to also detect inputs from devices other than the specified one. + */ + external fun start(defaultDevice: String, allDevices: Boolean) + + /** + * Checks what inputs are currently pressed and updates internal state. + * + * During a detection session, this should be called after each call to + * [ControllerInterface.dispatchKeyEvent] and [ControllerInterface#dispatchGenericMotionEvent]. + */ + external fun update() + + /** + * Returns whether a detection session has finished. + * + * A detection session can end once the user has pressed and released an input or once a timeout + * has been reached. + */ + external fun isComplete(): Boolean + + /** + * Returns the result of a detection session. + * + * The result of each detection session is only returned once. If this method is called more + * than once without starting a new detection session, the second call onwards will return an + * empty string. + * + * @param defaultDevice The device to detect inputs from. Should normally be the same as the one + * passed to [start]. + * + * @return The input(s) pressed by the user in the form of an InputCommon expression, + * or an empty string if there were no inputs. + */ + external fun takeResults(defaultDevice: String): String +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt index 32775a7ebc..622f4f5515 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt @@ -2,23 +2,7 @@ package org.dolphinemu.dolphinemu.features.input.model -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController - object MappingCommon { - /** - * Waits until the user presses one or more inputs or until a timeout, - * then returns the pressed inputs. - * - * When this is being called, a separate thread must be calling ControllerInterface's - * dispatchKeyEvent and dispatchGenericMotionEvent, otherwise no inputs will be registered. - * - * @param controller The device to detect inputs from. - * @param allDevices Whether to also detect inputs from devices other than the specified one. - * @return The input(s) pressed by the user in the form of an InputCommon expression, - * or an empty string if there were no inputs. - */ - external fun detectInput(controller: EmulatedController, allDevices: Boolean): String - external fun getExpressionForControl( control: String, device: String, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt index ff4a278f4c..c3b471899e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt @@ -3,12 +3,14 @@ package org.dolphinemu.dolphinemu.features.input.ui import android.app.Activity +import android.os.Looper +import android.os.Handler import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import androidx.appcompat.app.AlertDialog import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface -import org.dolphinemu.dolphinemu.features.input.model.MappingCommon +import org.dolphinemu.dolphinemu.features.input.model.InputDetector import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting /** @@ -24,21 +26,15 @@ class MotionAlertDialog( private val setting: InputMappingControlSetting, private val allDevices: Boolean ) : AlertDialog(activity) { + private val handler = Handler(Looper.getMainLooper()) + private val inputDetector: InputDetector = InputDetector() private var running = false override fun onStart() { super.onStart() - running = true - Thread { - val result = MappingCommon.detectInput(setting.controller, allDevices) - activity.runOnUiThread { - if (running) { - setting.value = result - dismiss() - } - } - }.start() + inputDetector.start(setting.controller.getDefaultDevice(), allDevices) + periodicUpdate() } override fun onStop() { @@ -48,9 +44,11 @@ class MotionAlertDialog( override fun dispatchKeyEvent(event: KeyEvent): Boolean { ControllerInterface.dispatchKeyEvent(event) + updateInputDetector() if (event.keyCode == KeyEvent.KEYCODE_BACK && event.isLongPress) { // Special case: Let the user cancel by long-pressing Back (intended for non-touch devices) setting.clearValue() + running = false dismiss() } return true @@ -63,6 +61,29 @@ class MotionAlertDialog( } ControllerInterface.dispatchGenericMotionEvent(event) + updateInputDetector() return true } + + private fun updateInputDetector() { + if (running) { + if (inputDetector.isComplete()) { + setting.value = inputDetector.takeResults(setting.controller.getDefaultDevice()) + running = false + + // Quirk: If this method has been called from onStart, calling dismiss directly + // doesn't seem to do anything. As a workaround, post a call to dismiss instead. + handler.post(this::dismiss) + } else { + inputDetector.update() + } + } + } + + private fun periodicUpdate() { + updateInputDetector() + if (running) { + handler.postDelayed(this::periodicUpdate, 10) + } + } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 33a679bf55..ed382745c0 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -113,6 +113,9 @@ static jclass s_core_device_control_class; static jfieldID s_core_device_control_pointer; static jmethodID s_core_device_control_constructor; +static jclass s_input_detector_class; +static jfieldID s_input_detector_pointer; + static jmethodID s_runnable_run; namespace IDCache @@ -525,6 +528,16 @@ jmethodID GetCoreDeviceControlConstructor() return s_core_device_control_constructor; } +jclass GetInputDetectorClass() +{ + return s_input_detector_class; +} + +jfieldID GetInputDetectorPointer() +{ + return s_input_detector_pointer; +} + jmethodID GetRunnableRun() { return s_runnable_run; @@ -746,6 +759,12 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) "(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V"); env->DeleteLocalRef(core_device_control_class); + const jclass input_detector_class = + env->FindClass("org/dolphinemu/dolphinemu/features/input/model/InputDetector"); + s_input_detector_class = reinterpret_cast(env->NewGlobalRef(input_detector_class)); + s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J"); + env->DeleteLocalRef(input_detector_class); + const jclass runnable_class = env->FindClass("java/lang/Runnable"); s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); env->DeleteLocalRef(runnable_class); @@ -779,10 +798,11 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_control_class); env->DeleteGlobalRef(s_control_group_class); env->DeleteGlobalRef(s_control_reference_class); + env->DeleteGlobalRef(s_control_group_container_class); env->DeleteGlobalRef(s_emulated_controller_class); env->DeleteGlobalRef(s_numeric_setting_class); env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); - env->DeleteGlobalRef(s_control_group_container_class); + env->DeleteGlobalRef(s_input_detector_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index d28b493a5b..0b01d14b42 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -112,6 +112,9 @@ jclass GetCoreDeviceControlClass(); jfieldID GetCoreDeviceControlPointer(); jmethodID GetCoreDeviceControlConstructor(); +jclass GetInputDetectorClass(); +jfieldID GetInputDetectorPointer(); + jmethodID GetRunnableRun(); } // namespace IDCache diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index 7ddae94c07..be200affa9 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(main SHARED Input/CoreDevice.h Input/EmulatedController.cpp Input/EmulatedController.h + Input/InputDetector.cpp Input/InputOverrider.cpp Input/MappingCommon.cpp Input/NumericSetting.cpp diff --git a/Source/Android/jni/Input/InputDetector.cpp b/Source/Android/jni/Input/InputDetector.cpp new file mode 100644 index 0000000000..03ad75ba1e --- /dev/null +++ b/Source/Android/jni/Input/InputDetector.cpp @@ -0,0 +1,79 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "InputCommon/ControllerInterface/ControllerInterface.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" + +namespace +{ +constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3); +constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(0); +constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5); +} // namespace + +static ciface::Core::InputDetector* GetPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetInputDetectorPointer())); +} + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_finalize(JNIEnv* env, jobject obj) +{ + delete GetPointer(env, obj); +} + +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_createNew(JNIEnv*, jobject) +{ + return reinterpret_cast(new ciface::Core::InputDetector); +} + +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_start( + JNIEnv* env, jobject obj, jstring j_default_device, jboolean all_devices) +{ + std::vector device_strings; + if (all_devices) + device_strings = g_controller_interface.GetAllDeviceStrings(); + else + device_strings = {GetJString(env, j_default_device)}; + + GetPointer(env, obj)->Start(g_controller_interface, device_strings); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_update(JNIEnv* env, jobject obj) +{ + GetPointer(env, obj)->Update(INPUT_DETECT_INITIAL_TIME, INPUT_DETECT_CONFIRMATION_TIME, + INPUT_DETECT_MAXIMUM_TIME); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_isComplete(JNIEnv* env, + jobject obj) +{ + return GetPointer(env, obj)->IsComplete(); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_takeResults( + JNIEnv* env, jobject obj, jstring j_default_device) +{ + ciface::Core::DeviceQualifier default_device; + default_device.FromString(GetJString(env, j_default_device)); + + auto detections = GetPointer(env, obj)->TakeResults(); + + ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections); + + return ToJString(env, ciface::MappingCommon::BuildExpression(detections, default_device, + ciface::MappingCommon::Quote::On)); +} +} diff --git a/Source/Android/jni/Input/MappingCommon.cpp b/Source/Android/jni/Input/MappingCommon.cpp index 1cc55df2ae..2017c892db 100644 --- a/Source/Android/jni/Input/MappingCommon.cpp +++ b/Source/Android/jni/Input/MappingCommon.cpp @@ -17,42 +17,9 @@ #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/MappingCommon.h" #include "jni/AndroidCommon/AndroidCommon.h" -#include "jni/Input/EmulatedController.h" - -namespace -{ -constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3); -constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(0); -constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5); -} // namespace extern "C" { -JNIEXPORT jstring JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_detectInput( - JNIEnv* env, jclass, jobject j_emulated_controller, jboolean all_devices) -{ - ControllerEmu::EmulatedController* emulated_controller = - EmulatedControllerFromJava(env, j_emulated_controller); - - const ciface::Core::DeviceQualifier default_device = emulated_controller->GetDefaultDevice(); - - std::vector device_strings; - if (all_devices) - device_strings = g_controller_interface.GetAllDeviceStrings(); - else - device_strings = {default_device.ToString()}; - - auto detections = - g_controller_interface.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME, - INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME); - - ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections); - - return ToJString(env, ciface::MappingCommon::BuildExpression(detections, default_device, - ciface::MappingCommon::Quote::On)); -} - JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_getExpressionForControl( JNIEnv* env, jclass, jstring j_control, jstring j_device, jstring j_default_device) diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index 3a29d4c98f..5a370cadc8 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -335,29 +335,6 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const return device != nullptr && device->IsValid(); } -// Wait for inputs on supplied devices. -// Inputs are only considered if they are first seen in a neutral state. -// This is useful for crazy flightsticks that have certain buttons that are always held down -// and also properly handles detection when using "FullAnalogSurface" inputs. -// Multiple detections are returned until the various timeouts have been reached. -auto DeviceContainer::DetectInput(const std::vector& device_strings, - std::chrono::milliseconds initial_wait, - std::chrono::milliseconds confirmation_wait, - std::chrono::milliseconds maximum_wait) const - -> std::vector -{ - InputDetector input_detector; - input_detector.Start(*this, device_strings); - - while (!input_detector.IsComplete()) - { - Common::SleepCurrentThread(10); - input_detector.Update(initial_wait, confirmation_wait, maximum_wait); - } - - return input_detector.TakeResults(); -} - struct InputDetector::Impl { struct InputState diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index 54d7b46110..f30dd4f405 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -230,20 +230,20 @@ public: bool HasConnectedDevice(const DeviceQualifier& qualifier) const; - std::vector DetectInput(const std::vector& device_strings, - std::chrono::milliseconds initial_wait, - std::chrono::milliseconds confirmation_wait, - std::chrono::milliseconds maximum_wait) const; - std::recursive_mutex& GetDevicesMutex() const { return m_devices_mutex; } protected: // Exclusively needed when reading/writing the "m_devices" array. - // Not needed when individually readring/writing a single device ptr. + // Not needed when individually reading/writing a single device ptr. mutable std::recursive_mutex m_devices_mutex; std::vector> m_devices; }; +// Wait for inputs on supplied devices. +// Inputs are only considered if they are first seen in a neutral state. +// This is useful for wacky flight sticks that have certain buttons that are always held down +// and also properly handles detection when using "FullAnalogSurface" inputs. +// Multiple detections are returned until the various timeouts have been reached. class InputDetector { public: