From 516c1314d2e08a81aa707566265597d82591e22c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 16 Mar 2025 11:05:06 +0100 Subject: [PATCH 1/2] Android: Don't use separate thread for MotionAlertDialog This is an Android continuation of bc95c00. We now call InputDetector::Update immediately after receiving an input event from Android instead of periodically calling it in a sleep loop. This improves detection of very short inputs, which are especially likely to occur for volume buttons on phones (or at least on my phone) if you don't intentionally keep them held down. --- .../features/input/model/InputDetector.kt | 68 ++++++++++++++++ .../features/input/model/MappingCommon.kt | 16 ---- .../features/input/ui/MotionAlertDialog.kt | 43 +++++++--- Source/Android/jni/AndroidCommon/IDCache.cpp | 22 +++++- Source/Android/jni/AndroidCommon/IDCache.h | 3 + Source/Android/jni/CMakeLists.txt | 1 + Source/Android/jni/Input/InputDetector.cpp | 79 +++++++++++++++++++ Source/Android/jni/Input/MappingCommon.cpp | 33 -------- .../ControllerInterface/CoreDevice.cpp | 23 ------ .../ControllerInterface/CoreDevice.h | 12 +-- 10 files changed, 210 insertions(+), 90 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt create mode 100644 Source/Android/jni/Input/InputDetector.cpp 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: From 9e9faf3be1582ac5cdcaa46dc60287b10c921aa2 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 16 Mar 2025 11:24:10 +0100 Subject: [PATCH 2/2] Android: Show message when trying to map disconnected device Having the MotionAlertDialog immediately close is confusing for users. Let's show a message to tell them what went wrong. --- .../dolphinemu/features/input/ui/MotionAlertDialog.kt | 8 ++++++++ Source/Android/app/src/main/res/values/strings.xml | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) 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 c3b471899e..12f3ffb138 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 @@ -9,6 +9,8 @@ import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.InputDetector import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting @@ -35,6 +37,12 @@ class MotionAlertDialog( running = true inputDetector.start(setting.controller.getDefaultDevice(), allDevices) periodicUpdate() + if (running == false) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.input_binding_disconnected_device) + .setPositiveButton(R.string.ok, null) + .show() + } } override fun onStop() { diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index e10a781114..c0b33237af 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -54,7 +54,8 @@ Input Binding Press or move an input to bind it to %1$s. - You need to select a device first! + You need to select a device first. + The selected device is disconnected.\n\nPlease reconnect the device or select a different device. Configure Input Configure Output Expression