diff --git a/Android/library/src/main/cpp/CMakeLists.txt b/Android/library/src/main/cpp/CMakeLists.txt index 2547205..0bbf993 100644 --- a/Android/library/src/main/cpp/CMakeLists.txt +++ b/Android/library/src/main/cpp/CMakeLists.txt @@ -23,8 +23,9 @@ add_compile_options(-Wno-deprecated-declarations) # ---- Source files ---- set(MODMANAGER_SOURCES core/dobby_hook.cpp - core/jni_helper.c - core/misc.c + core/java_input_bridge.cpp + core/jni_helper.c + core/misc.c ) set(MONODROID_SOURCES diff --git a/Android/library/src/main/cpp/core/dobby_hook.cpp b/Android/library/src/main/cpp/core/dobby_hook.cpp index a128bd3..8c625f2 100644 --- a/Android/library/src/main/cpp/core/dobby_hook.cpp +++ b/Android/library/src/main/cpp/core/dobby_hook.cpp @@ -1,4 +1,3 @@ -#include #include #include diff --git a/Android/library/src/main/cpp/core/java_input_bridge.cpp b/Android/library/src/main/cpp/core/java_input_bridge.cpp new file mode 100644 index 0000000..2df1271 --- /dev/null +++ b/Android/library/src/main/cpp/core/java_input_bridge.cpp @@ -0,0 +1,545 @@ +#include +#include "java_input_bridge.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int32_t kInputTypeMotion = 1; +constexpr int32_t kInputTypeKey = 2; +constexpr int32_t kMaxPointers = MODMANAGER_JAVA_INPUT_MAX_POINTERS; +constexpr int32_t kMaxHistorySamples = MODMANAGER_JAVA_INPUT_MAX_HISTORY_SAMPLES; +constexpr int32_t kAxisCount = MODMANAGER_JAVA_INPUT_AXIS_COUNT; +constexpr int32_t kAxisVScroll = 9; +constexpr int32_t kAxisHScroll = 10; +constexpr int32_t kAxisDistance = 24; +constexpr int32_t kAxisTilt = 25; + +using JavaInputEvent = ModManagerJavaInputEvent; +using JavaInputHistorySample = ModManagerJavaInputHistorySample; +using JavaInputPointer = ModManagerJavaInputPointer; +using java_input_callback = ModManagerJavaInputCallback; + +static_assert(sizeof(JavaInputPointer) == 316); +static_assert(sizeof(JavaInputHistorySample) == 24); +static_assert(offsetof(JavaInputEvent, history_samples) == 120); +static_assert(offsetof(JavaInputEvent, pointers) == 128); +static_assert(sizeof(JavaInputEvent) == 10240); + +std::atomic g_java_input_callback{nullptr}; +std::atomic g_java_activity_bridge_active{false}; +std::atomic g_full_data_enabled{false}; +std::atomic g_overlay_visible{false}; +std::atomic g_capture_mouse{false}; +std::atomic g_capture_keyboard{false}; + +struct MotionMethods { + jclass clazz = nullptr; + jmethodID action = nullptr; + jmethodID action_masked = nullptr; + jmethodID action_index = nullptr; + jmethodID pointer_count = nullptr; + jmethodID pointer_id = nullptr; + jmethodID tool_type = nullptr; + jmethodID x = nullptr; + jmethodID y = nullptr; + jmethodID pressure = nullptr; + jmethodID size = nullptr; + jmethodID touch_major = nullptr; + jmethodID touch_minor = nullptr; + jmethodID tool_major = nullptr; + jmethodID tool_minor = nullptr; + jmethodID orientation = nullptr; + jmethodID axis_value = nullptr; + jmethodID event_time_nanos = nullptr; + jmethodID historical_event_time_nanos = nullptr; + jmethodID raw_x_pointer = nullptr; + jmethodID raw_y_pointer = nullptr; + jmethodID raw_x = nullptr; + jmethodID raw_y = nullptr; + jmethodID historical_event_time = nullptr; + jmethodID historical_x = nullptr; + jmethodID historical_y = nullptr; + jmethodID historical_pressure = nullptr; + jmethodID historical_size = nullptr; + jmethodID historical_touch_major = nullptr; + jmethodID historical_touch_minor = nullptr; + jmethodID historical_tool_major = nullptr; + jmethodID historical_tool_minor = nullptr; + jmethodID historical_orientation = nullptr; + jmethodID historical_axis_value = nullptr; + jmethodID history_size = nullptr; + jmethodID event_time = nullptr; + jmethodID down_time = nullptr; + jmethodID source = nullptr; + jmethodID device_id = nullptr; + jmethodID flags = nullptr; + jmethodID meta_state = nullptr; + jmethodID button_state = nullptr; + jmethodID action_button = nullptr; +}; + +struct KeyMethods { + jclass clazz = nullptr; + jmethodID action = nullptr; + jmethodID key_code = nullptr; + jmethodID scan_code = nullptr; + jmethodID meta_state = nullptr; + jmethodID repeat_count = nullptr; + jmethodID event_time = nullptr; + jmethodID down_time = nullptr; + jmethodID source = nullptr; + jmethodID device_id = nullptr; + jmethodID flags = nullptr; + jmethodID unicode_char = nullptr; +}; + +std::mutex g_method_lock; +MotionMethods g_motion_methods; +KeyMethods g_key_methods; + +template +bool has_exception(JNIEnv *env, T *value) { + if (value != nullptr && !env->ExceptionCheck()) + return false; + if (env->ExceptionCheck()) + env->ExceptionClear(); + return true; +} + +jmethodID method(JNIEnv *env, jclass clazz, const char *name, const char *signature) { + jmethodID id = env->GetMethodID(clazz, name, signature); + return has_exception(env, id) ? nullptr : id; +} + +bool initialize_motion_methods(JNIEnv *env, jobject event) { + if (g_motion_methods.clazz != nullptr) + return true; + + std::lock_guard guard(g_method_lock); + if (g_motion_methods.clazz != nullptr) + return true; + + jclass local = env->GetObjectClass(event); + if (has_exception(env, local)) + return false; + jclass global = static_cast(env->NewGlobalRef(local)); + env->DeleteLocalRef(local); + if (has_exception(env, global)) + return false; + + MotionMethods methods; + methods.clazz = global; + methods.action = method(env, global, "getAction", "()I"); + methods.action_masked = method(env, global, "getActionMasked", "()I"); + methods.action_index = method(env, global, "getActionIndex", "()I"); + methods.pointer_count = method(env, global, "getPointerCount", "()I"); + methods.pointer_id = method(env, global, "getPointerId", "(I)I"); + methods.tool_type = method(env, global, "getToolType", "(I)I"); + methods.x = method(env, global, "getX", "(I)F"); + methods.y = method(env, global, "getY", "(I)F"); + methods.pressure = method(env, global, "getPressure", "(I)F"); + methods.size = method(env, global, "getSize", "(I)F"); + methods.touch_major = method(env, global, "getTouchMajor", "(I)F"); + methods.touch_minor = method(env, global, "getTouchMinor", "(I)F"); + methods.tool_major = method(env, global, "getToolMajor", "(I)F"); + methods.tool_minor = method(env, global, "getToolMinor", "(I)F"); + methods.orientation = method(env, global, "getOrientation", "(I)F"); + methods.axis_value = method(env, global, "getAxisValue", "(II)F"); + methods.event_time_nanos = method(env, global, "getEventTimeNanos", "()J"); + methods.historical_event_time_nanos = method( + env, global, "getHistoricalEventTimeNanos", "(I)J"); + methods.raw_x_pointer = method(env, global, "getRawX", "(I)F"); + methods.raw_y_pointer = method(env, global, "getRawY", "(I)F"); + methods.raw_x = method(env, global, "getRawX", "()F"); + methods.raw_y = method(env, global, "getRawY", "()F"); + methods.historical_event_time = method(env, global, "getHistoricalEventTime", "(I)J"); + methods.historical_x = method(env, global, "getHistoricalX", "(II)F"); + methods.historical_y = method(env, global, "getHistoricalY", "(II)F"); + methods.historical_pressure = method(env, global, "getHistoricalPressure", "(II)F"); + methods.historical_size = method(env, global, "getHistoricalSize", "(II)F"); + methods.historical_touch_major = method(env, global, "getHistoricalTouchMajor", "(II)F"); + methods.historical_touch_minor = method(env, global, "getHistoricalTouchMinor", "(II)F"); + methods.historical_tool_major = method(env, global, "getHistoricalToolMajor", "(II)F"); + methods.historical_tool_minor = method(env, global, "getHistoricalToolMinor", "(II)F"); + methods.historical_orientation = method(env, global, "getHistoricalOrientation", "(II)F"); + methods.historical_axis_value = method(env, global, "getHistoricalAxisValue", "(III)F"); + methods.history_size = method(env, global, "getHistorySize", "()I"); + methods.event_time = method(env, global, "getEventTime", "()J"); + methods.down_time = method(env, global, "getDownTime", "()J"); + methods.source = method(env, global, "getSource", "()I"); + methods.device_id = method(env, global, "getDeviceId", "()I"); + methods.flags = method(env, global, "getFlags", "()I"); + methods.meta_state = method(env, global, "getMetaState", "()I"); + methods.button_state = method(env, global, "getButtonState", "()I"); + methods.action_button = method(env, global, "getActionButton", "()I"); + + const bool valid = methods.action && methods.action_masked && methods.action_index && + methods.pointer_count && methods.pointer_id && methods.tool_type && methods.x && + methods.y && methods.pressure && methods.size && methods.touch_major && + methods.touch_minor && methods.tool_major && methods.tool_minor && + methods.orientation && methods.axis_value && methods.historical_event_time && + methods.historical_x && methods.historical_y && methods.historical_pressure && + methods.historical_size && methods.historical_touch_major && + methods.historical_touch_minor && methods.historical_tool_major && + methods.historical_tool_minor && methods.historical_orientation && + methods.historical_axis_value && methods.history_size && methods.event_time && + methods.down_time && methods.source && methods.device_id && methods.flags && + methods.meta_state && methods.button_state && methods.action_button; + if (!valid || env->ExceptionCheck()) { + if (env->ExceptionCheck()) + env->ExceptionClear(); + env->DeleteGlobalRef(global); + return false; + } + + g_motion_methods = methods; + return true; +} + +bool initialize_key_methods(JNIEnv *env, jobject event) { + if (g_key_methods.clazz != nullptr) + return true; + + std::lock_guard guard(g_method_lock); + if (g_key_methods.clazz != nullptr) + return true; + + jclass local = env->GetObjectClass(event); + if (has_exception(env, local)) + return false; + jclass global = static_cast(env->NewGlobalRef(local)); + env->DeleteLocalRef(local); + if (has_exception(env, global)) + return false; + + KeyMethods methods; + methods.clazz = global; + methods.action = method(env, global, "getAction", "()I"); + methods.key_code = method(env, global, "getKeyCode", "()I"); + methods.scan_code = method(env, global, "getScanCode", "()I"); + methods.meta_state = method(env, global, "getMetaState", "()I"); + methods.repeat_count = method(env, global, "getRepeatCount", "()I"); + methods.event_time = method(env, global, "getEventTime", "()J"); + methods.down_time = method(env, global, "getDownTime", "()J"); + methods.source = method(env, global, "getSource", "()I"); + methods.device_id = method(env, global, "getDeviceId", "()I"); + methods.flags = method(env, global, "getFlags", "()I"); + methods.unicode_char = method(env, global, "getUnicodeChar", "(I)I"); + + const bool valid = methods.action && methods.key_code && methods.scan_code && + methods.meta_state && methods.repeat_count && methods.event_time && + methods.down_time && methods.source && methods.device_id && methods.flags && + methods.unicode_char; + if (!valid || env->ExceptionCheck()) { + if (env->ExceptionCheck()) + env->ExceptionClear(); + env->DeleteGlobalRef(global); + return false; + } + + g_key_methods = methods; + return true; +} + +jfloat motion_float(JNIEnv *env, jobject event, jmethodID method_id, jint pointer, + jint history_index, bool historical) { + if (historical) + return env->CallFloatMethod(event, method_id, pointer, history_index); + return env->CallFloatMethod(event, method_id, pointer); +} + +jfloat motion_axis(JNIEnv *env, jobject event, const MotionMethods &methods, + jint pointer, jint axis, jint history_index, bool historical) { + if (historical) { + return env->CallFloatMethod(event, methods.historical_axis_value, + axis, pointer, history_index); + } + return env->CallFloatMethod(event, methods.axis_value, axis, pointer); +} + +void fill_motion_pointer(JNIEnv *env, jobject event, const MotionMethods &methods, + int32_t index, bool historical, int32_t history_index, + bool include_full_data, float raw_offset_x, + float raw_offset_y, JavaInputPointer &pointer) { + pointer.id = env->CallIntMethod(event, methods.pointer_id, index); + pointer.tool_type = env->CallIntMethod(event, methods.tool_type, index); + pointer.x = motion_float(env, event, + historical ? methods.historical_x : methods.x, index, history_index, historical); + pointer.y = motion_float(env, event, + historical ? methods.historical_y : methods.y, index, history_index, historical); + pointer.raw_x = pointer.x + raw_offset_x; + pointer.raw_y = pointer.y + raw_offset_y; + pointer.pressure = motion_float(env, event, + historical ? methods.historical_pressure : methods.pressure, + index, history_index, historical); + + if (!include_full_data) + return; + + pointer.size = motion_float(env, event, + historical ? methods.historical_size : methods.size, index, history_index, historical); + pointer.touch_major = motion_float(env, event, + historical ? methods.historical_touch_major : methods.touch_major, + index, history_index, historical); + pointer.touch_minor = motion_float(env, event, + historical ? methods.historical_touch_minor : methods.touch_minor, + index, history_index, historical); + pointer.tool_major = motion_float(env, event, + historical ? methods.historical_tool_major : methods.tool_major, + index, history_index, historical); + pointer.tool_minor = motion_float(env, event, + historical ? methods.historical_tool_minor : methods.tool_minor, + index, history_index, historical); + pointer.orientation = motion_float(env, event, + historical ? methods.historical_orientation : methods.orientation, + index, history_index, historical); + for (int32_t axis = 0; axis < kAxisCount; ++axis) { + pointer.axis_values[axis] = motion_axis( + env, event, methods, index, axis, history_index, historical); + } + pointer.tilt = pointer.axis_values[kAxisTilt]; + pointer.distance = pointer.axis_values[kAxisDistance]; +} + +bool build_motion_event(JNIEnv *env, jobject event, jint viewport_width, + jint viewport_height, bool generic, bool include_full_data, + JavaInputEvent &input, + std::vector &history_samples, + std::vector &history_pointers) { + const MotionMethods &m = g_motion_methods; + input = {}; + input.struct_size = sizeof(input); + input.abi_version = MODMANAGER_JAVA_INPUT_ABI_VERSION; + input.type = kInputTypeMotion; + input.is_generic_motion = generic ? 1 : 0; + input.full_data_included = include_full_data ? 1 : 0; + input.raw_action = env->CallIntMethod(event, m.action); + input.action = env->CallIntMethod(event, m.action_masked); + input.action_index = env->CallIntMethod(event, m.action_index); + input.pointer_count = env->CallIntMethod(event, m.pointer_count); + input.stored_pointer_count = std::clamp(input.pointer_count, 0, kMaxPointers); + input.source = env->CallIntMethod(event, m.source); + input.device_id = env->CallIntMethod(event, m.device_id); + input.flags = env->CallIntMethod(event, m.flags); + input.meta_state = env->CallIntMethod(event, m.meta_state); + input.button_state = env->CallIntMethod(event, m.button_state); + input.action_button = env->CallIntMethod(event, m.action_button); + input.event_time_nanos = m.event_time_nanos != nullptr + ? static_cast(env->CallLongMethod(event, m.event_time_nanos)) + : static_cast(env->CallLongMethod(event, m.event_time)) * 1000000LL; + input.down_time_nanos = static_cast(env->CallLongMethod(event, m.down_time)) * 1000000LL; + input.viewport_width = viewport_width; + input.viewport_height = viewport_height; + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + + float raw_offset_x = 0.0f; + float raw_offset_y = 0.0f; + if (input.stored_pointer_count > 0) { + if (m.raw_x_pointer != nullptr && m.raw_y_pointer != nullptr) { + const jfloat raw_x = env->CallFloatMethod(event, m.raw_x_pointer, 0); + const jfloat raw_y = env->CallFloatMethod(event, m.raw_y_pointer, 0); + const jfloat local_x = env->CallFloatMethod(event, m.x, 0); + const jfloat local_y = env->CallFloatMethod(event, m.y, 0); + raw_offset_x = raw_x - local_x; + raw_offset_y = raw_y - local_y; + } else if (m.raw_x != nullptr && m.raw_y != nullptr) { + const jfloat raw_x = env->CallFloatMethod(event, m.raw_x); + const jfloat raw_y = env->CallFloatMethod(event, m.raw_y); + const jfloat local_x = env->CallFloatMethod(event, m.x, 0); + const jfloat local_y = env->CallFloatMethod(event, m.y, 0); + raw_offset_x = raw_x - local_x; + raw_offset_y = raw_y - local_y; + } + } + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + + for (int32_t index = 0; index < input.stored_pointer_count; ++index) { + fill_motion_pointer(env, event, m, index, false, 0, include_full_data, + raw_offset_x, raw_offset_y, input.pointers[index]); + } + + const jint axis_index = std::clamp(input.action_index, 0, + std::max(0, input.stored_pointer_count - 1)); + if (input.stored_pointer_count > 0) { + input.horizontal_scroll = motion_axis(env, event, m, axis_index, kAxisHScroll, + 0, false); + input.vertical_scroll = motion_axis(env, event, m, axis_index, kAxisVScroll, + 0, false); + } + + input.history_sample_count = std::max(0, env->CallIntMethod(event, m.history_size)); + input.stored_history_sample_count = include_full_data + ? std::min(input.history_sample_count, kMaxHistorySamples) + : 0; + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + + if (input.stored_history_sample_count > 0) { + const size_t sample_count = static_cast(input.stored_history_sample_count); + const size_t pointers_per_sample = static_cast(input.stored_pointer_count); + history_samples.resize(sample_count); + history_pointers.resize(sample_count * pointers_per_sample); + + for (int32_t history_index = 0; + history_index < input.stored_history_sample_count; + ++history_index) { + JavaInputHistorySample &sample = history_samples[history_index]; + sample.pointer_count = input.pointer_count; + sample.stored_pointer_count = input.stored_pointer_count; + const jlong sample_time_nanos = m.historical_event_time_nanos != nullptr + ? env->CallLongMethod(event, m.historical_event_time_nanos, history_index) + : static_cast(env->CallLongMethod( + event, m.historical_event_time, history_index)) * 1000000LL; + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + sample.event_time_nanos = static_cast(sample_time_nanos); + JavaInputPointer *sample_pointers = pointers_per_sample > 0 + ? history_pointers.data() + + static_cast(history_index) * pointers_per_sample + : nullptr; + sample.pointers = sample_pointers; + for (int32_t pointer_index = 0; + pointer_index < sample.stored_pointer_count; + ++pointer_index) { + fill_motion_pointer(env, event, m, pointer_index, true, history_index, + true, raw_offset_x, raw_offset_y, sample_pointers[pointer_index]); + } + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + } + + input.history_samples = history_samples.data(); + } + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + return true; +} + +bool should_capture_motion() { + return g_java_activity_bridge_active.load(std::memory_order_acquire) && + (g_overlay_visible.load(std::memory_order_acquire) || + g_capture_mouse.load(std::memory_order_acquire)); +} + +} // namespace + +extern "C" { + +void modmanager_set_java_input_callback(void *callback) { + g_java_input_callback.store(reinterpret_cast(callback), + std::memory_order_release); +} + +void modmanager_set_java_input_capture_state(int overlay_visible, + int capture_mouse, + int capture_keyboard) { + g_overlay_visible.store(overlay_visible != 0, std::memory_order_release); + g_capture_mouse.store(capture_mouse != 0, std::memory_order_release); + g_capture_keyboard.store(capture_keyboard != 0, std::memory_order_release); +} + +void modmanager_set_java_input_full_data_enabled(int enabled) { + g_full_data_enabled.store(enabled != 0, std::memory_order_release); +} + +} // extern "C" + +extern "C" JNIEXPORT void JNICALL +Java_starray_adofai_v3_MainActivity_nativeRegisterInputBridge( + JNIEnv *, jobject) { + g_java_activity_bridge_active.store(true, std::memory_order_release); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_starray_adofai_v3_MainActivity_nativeObserveMotionEvent( + JNIEnv *env, jobject, jobject event, jint viewport_width, + jint viewport_height, jboolean generic_motion) { + if (event == nullptr || !initialize_motion_methods(env, event)) + return JNI_FALSE; + + java_input_callback callback = g_java_input_callback.load(std::memory_order_acquire); + if (callback != nullptr) { + const bool include_full_data = g_full_data_enabled.load(std::memory_order_acquire); + JavaInputEvent input{}; + std::vector history_samples; + std::vector history_pointers; + try { + if (build_motion_event(env, event, viewport_width, viewport_height, + generic_motion == JNI_TRUE, include_full_data, input, + history_samples, history_pointers)) { + input.history_samples = history_samples.empty() + ? nullptr + : history_samples.data(); + callback(&input); + } + } catch (...) { + if (env->ExceptionCheck()) + env->ExceptionClear(); + } + } + + return should_capture_motion() ? JNI_TRUE : JNI_FALSE; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_starray_adofai_v3_MainActivity_nativeObserveKeyEvent( + JNIEnv *env, jobject, jobject event) { + if (event == nullptr || !initialize_key_methods(env, event)) + return JNI_FALSE; + + const KeyMethods &m = g_key_methods; + JavaInputEvent input{}; + input.struct_size = sizeof(input); + input.abi_version = MODMANAGER_JAVA_INPUT_ABI_VERSION; + input.type = kInputTypeKey; + input.action = env->CallIntMethod(event, m.action); + input.key_code = env->CallIntMethod(event, m.key_code); + input.scan_code = env->CallIntMethod(event, m.scan_code); + input.meta_state = env->CallIntMethod(event, m.meta_state); + input.repeat_count = env->CallIntMethod(event, m.repeat_count); + input.event_time_nanos = static_cast(env->CallLongMethod(event, m.event_time)) * 1000000LL; + input.down_time_nanos = static_cast(env->CallLongMethod(event, m.down_time)) * 1000000LL; + input.source = env->CallIntMethod(event, m.source); + input.device_id = env->CallIntMethod(event, m.device_id); + input.flags = env->CallIntMethod(event, m.flags); + input.unicode_code_point = env->CallIntMethod(event, m.unicode_char, input.meta_state); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return JNI_FALSE; + } + + java_input_callback callback = g_java_input_callback.load(std::memory_order_acquire); + if (callback != nullptr) + callback(&input); + + const bool back_key = input.key_code == 4; + const bool capture = g_java_activity_bridge_active.load(std::memory_order_acquire) && + !back_key && (g_overlay_visible.load(std::memory_order_acquire) || + g_capture_keyboard.load(std::memory_order_acquire)); + return capture ? JNI_TRUE : JNI_FALSE; +} diff --git a/Android/library/src/main/cpp/core/java_input_bridge.h b/Android/library/src/main/cpp/core/java_input_bridge.h new file mode 100644 index 0000000..ba8346f --- /dev/null +++ b/Android/library/src/main/cpp/core/java_input_bridge.h @@ -0,0 +1,88 @@ +#ifndef STARRAY_JAVA_INPUT_BRIDGE_H +#define STARRAY_JAVA_INPUT_BRIDGE_H + +#include + +#define MODMANAGER_JAVA_INPUT_ABI_VERSION 2 +#define MODMANAGER_JAVA_INPUT_MAX_POINTERS 32 +#define MODMANAGER_JAVA_INPUT_MAX_HISTORY_SAMPLES 256 +#define MODMANAGER_JAVA_INPUT_AXIS_COUNT 64 + +typedef struct ModManagerJavaInputPointer { + int32_t id; + int32_t tool_type; + float x; + float y; + float raw_x; + float raw_y; + float pressure; + float size; + float touch_major; + float touch_minor; + float tool_major; + float tool_minor; + float orientation; + float tilt; + float distance; + float axis_values[MODMANAGER_JAVA_INPUT_AXIS_COUNT]; +} ModManagerJavaInputPointer; + +#pragma pack(push, 4) +typedef struct ModManagerJavaInputHistorySample { + int32_t pointer_count; + int32_t stored_pointer_count; + int64_t event_time_nanos; + const ModManagerJavaInputPointer *pointers; +} ModManagerJavaInputHistorySample; + +typedef struct ModManagerJavaInputEvent { + int32_t struct_size; + int32_t abi_version; + int32_t type; + int32_t is_generic_motion; + int32_t full_data_included; + int32_t action; + int32_t action_index; + int32_t pointer_count; + int32_t stored_pointer_count; + int32_t source; + int32_t device_id; + int32_t flags; + int32_t meta_state; + int32_t button_state; + int32_t action_button; + int32_t key_code; + int32_t scan_code; + int32_t repeat_count; + int32_t unicode_code_point; + int32_t viewport_width; + int32_t viewport_height; + int32_t raw_action; + int64_t event_time_nanos; + int64_t down_time_nanos; + float horizontal_scroll; + float vertical_scroll; + int32_t history_sample_count; + int32_t stored_history_sample_count; + const ModManagerJavaInputHistorySample *history_samples; + ModManagerJavaInputPointer pointers[MODMANAGER_JAVA_INPUT_MAX_POINTERS]; +} ModManagerJavaInputEvent; +#pragma pack(pop) + +typedef void (*ModManagerJavaInputCallback)(const ModManagerJavaInputEvent *event); + +#ifdef __cplusplus +extern "C" { +#endif + +void modmanager_set_java_input_callback(void *callback); +void modmanager_set_java_input_capture_state(int overlay_visible, + int capture_mouse, + int capture_keyboard); +void modmanager_set_java_input_full_data_enabled(int enabled); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Android/library/src/main/java/net/dot/MonoRunner.java b/Android/library/src/main/java/net/dot/MonoRunner.java index a2e9e70..c471cac 100644 --- a/Android/library/src/main/java/net/dot/MonoRunner.java +++ b/Android/library/src/main/java/net/dot/MonoRunner.java @@ -3,6 +3,7 @@ package net.dot; +import starray.android.modmanager.ModManagerUtils; import android.util.Log; import java.util.ArrayList; @@ -26,6 +27,7 @@ public final class MonoRunner { System.loadLibrary("System.Security.Cryptography.Native.Android"); System.loadLibrary("monodroid"); System.loadLibrary("modmanager"); + ModManagerUtils.registerHostInputBridge(); } // ======================================================================== diff --git a/Android/library/src/main/java/starray/android/modmanager/ModManagerUtils.java b/Android/library/src/main/java/starray/android/modmanager/ModManagerUtils.java index 6d0eae7..f1a60a4 100644 --- a/Android/library/src/main/java/starray/android/modmanager/ModManagerUtils.java +++ b/Android/library/src/main/java/starray/android/modmanager/ModManagerUtils.java @@ -38,6 +38,25 @@ public static Activity getUnityActivity() { return null; } + /** + * Activate the Java input bridge exposed by patched host Activities. + * Reflection avoids a hard class/method dependency; unpatched hosts can load the AAR, + * but they do not provide input events on the bridge-only input path. + */ + public static boolean registerHostInputBridge() { + Activity activity = getUnityActivity(); + if (activity == null) return false; + + try { + java.lang.reflect.Method method = activity.getClass() + .getMethod("registerJavaInputBridge"); + Object result = method.invoke(activity); + return result instanceof Boolean && (Boolean) result; + } catch (Exception | LinkageError ignored) { + return false; + } + } + /** showSoftInput 前调用,把 ImGui 已有文本同步到 EditText */ public static void setInputText(String text) { if (unityActivity == null) return; diff --git a/StArray.ModManager.Android/Native/InputEvents.cs b/StArray.ModManager.Android/Native/InputEvents.cs index 4dca2df..5a9cbbf 100644 --- a/StArray.ModManager.Android/Native/InputEvents.cs +++ b/StArray.ModManager.Android/Native/InputEvents.cs @@ -1,9 +1,8 @@ -using System.Diagnostics; using StArray.ModManager.Manager; namespace StArray.ModManager.Android.Native; -/// 一次完整触摸事件的快照。所有字段在广播时即已从原生事件读出。 +/// 一次完整触摸事件的快照。所有字段均从 Java MotionEvent 复制而来。 /// 主动作,已去除指针索引 /// 指针索引 /// 稳定的指针 ID,比索引更适合跨事件跟踪多指输入 @@ -43,35 +42,31 @@ public readonly record struct TouchTimestampInfo( long EventTimeNanos); /// -/// 输入事件广播点。原生输入 Hook 只负责解析一次事件,订阅方在自己的队列中异步处理。 +/// 输入事件广播点。每个订阅者收到的都是从原始 Android 输入事件复制出的托管快照。 /// /// -/// 回调运行在 Android 输入分发线程,不是 Unity 主线程。订阅方只能做廉价的值类型快照 -/// 和入队操作,不能访问 Unity 对象或执行 IL2CPP 游戏逻辑。 +/// 所有回调都在 Android 输入分发线程同步执行;OnInput 每个 Java KeyEvent/MotionEvent +/// 恰好回调一次,MotionEvent 的当前样本和历史样本都包含在同一份快照中。回调应快速返回, +/// 耗时工作应由订阅者自行转交到后台队列。OnInput、OnTouch 和 OnTouchTimestamp +/// 都由宿主 Activity 的 Java 输入桥接提供,因此宿主 APK 必须包含对应的 Activity 补丁。 /// public static class InputEvents { private const string LogTag = nameof(InputEvents); - private const long DuplicateWindowMilliseconds = 8L; - private static Action? s_onTouch; private static Action? s_onTouchTimestamp; + private static Action? s_onInput; private static int s_touchSubscriberCount; private static int s_touchTimestampSubscriberCount; + private static int s_inputSubscriberCount; private static int s_faultLogged; private static readonly object DedupLock = new(); - private static int s_lastRawAction; - private static int s_lastPointerIndex; - private static int s_lastPointerCount; - private static int s_lastPointerId; - private static long s_lastEventTimeNanos; - private static long s_lastDispatchTicks; - /// 是否已有任一类订阅者。 public static bool HasSubscribers => Volatile.Read(ref s_touchSubscriberCount) > 0 - || Volatile.Read(ref s_touchTimestampSubscriberCount) > 0; + || Volatile.Read(ref s_touchTimestampSubscriberCount) > 0 + || Volatile.Read(ref s_inputSubscriberCount) > 0; /// 完整触摸事件广播,保留坐标和 Move 事件。 public static event Action? OnTouch @@ -122,115 +117,112 @@ public static event Action? OnTouchTimestamp } /// - /// 从原生 AInputEvent 解析并广播。输入事件只在这里读取一次,避免每个 Mod 重复访问 - /// 原生对象;重复的同一事件只在广播层过滤一次。 + /// Full Java KeyEvent/MotionEvent snapshots, including external keyboard metadata, multi-touch + /// pointers, historical samples, screen coordinates, timestamps, and Android axis values. + /// Each callback runs synchronously on the Android input-dispatch thread. This event and the + /// touch-only compatibility events require the patched host Activity bridge. /// - internal static void RaiseFrom(nint inputEvent) + public static event Action? OnInput { - Action? handlers; + add + { + if (value == null) return; + lock (DedupLock) + { + s_onInput += value; + Interlocked.Increment(ref s_inputSubscriberCount); + AndroidJavaInputBridge.SetFullDataEnabled(true); + } + } + remove + { + if (value == null) return; + lock (DedupLock) + { + s_onInput -= value; + int count = Interlocked.Decrement(ref s_inputSubscriberCount); + if (count <= 0) + { + Interlocked.Exchange(ref s_inputSubscriberCount, 0); + AndroidJavaInputBridge.SetFullDataEnabled(false); + } + } + } + } + + /// Synchronously broadcast one copied Java event on its input-dispatch thread. + internal static void RaiseFromJava(AndroidInputEventInfo input) + { + Action? inputHandlers; + Action? touchHandlers; Action? timestampHandlers; lock (DedupLock) { - handlers = s_onTouch; + inputHandlers = s_onInput; + touchHandlers = s_onTouch; timestampHandlers = s_onTouchTimestamp; } - if ((handlers == null && timestampHandlers == null) || inputEvent == 0) - return; + if (inputHandlers != null) + DispatchInputHandlers(inputHandlers, input); - try + if (input.Kind != AndroidInputEventKind.Motion + || input.IsGenericMotion + || (touchHandlers == null && timestampHandlers == null)) { - if (AndroidInput.AInputEvent_getType(inputEvent) != AndroidInput.EventType.Motion) - return; + return; + } - int rawAction = AndroidInput.AMotionEvent_getAction(inputEvent); - AndroidInput.MotionAction action = AndroidInput.GetMainAction(rawAction); - int pointerIndex = AndroidInput.GetPointerIndex(rawAction); - bool timestampAction = action is AndroidInput.MotionAction.Down - or AndroidInput.MotionAction.PointerDown - or AndroidInput.MotionAction.Up - or AndroidInput.MotionAction.PointerUp - or AndroidInput.MotionAction.Cancel; + int pointerCount = input.StoredPointerCount; + if (pointerCount == 0) + return; - // AsyncInput subscribes only to the timestamp channel. Do not - // inspect Move coordinates, pointer IDs, or pointer counts on the - // hot path when no full-gesture subscriber exists. - if (!timestampAction && handlers == null) - return; + AndroidInput.MotionAction action = (AndroidInput.MotionAction)input.Action; + int pointerIndex = Math.Clamp(input.ActionIndex, 0, pointerCount - 1); + AndroidInputPointerInfo pointer = input.Pointers.Span[pointerIndex]; + long eventTimeNanos = input.EventTimeNanos; + int pointerId = action == AndroidInput.MotionAction.Cancel ? -1 : pointer.Id; + bool timestampAction = action is AndroidInput.MotionAction.Down + or AndroidInput.MotionAction.PointerDown + or AndroidInput.MotionAction.Up + or AndroidInput.MotionAction.PointerUp + or AndroidInput.MotionAction.Cancel; - int pointerCount = AndroidInput.AMotionEvent_getPointerCount(inputEvent); - long eventTimeNanos = AndroidInput.AMotionEvent_getEventTime(inputEvent); - int pointerId = action == AndroidInput.MotionAction.Cancel - ? -1 - : AndroidInput.AMotionEvent_getPointerId(inputEvent, pointerIndex); + if (timestampHandlers != null && timestampAction) + { + DispatchTimestampHandlers( + timestampHandlers, + new TouchTimestampInfo(action, pointerId, eventTimeNanos)); + } - if (IsDuplicate( - rawAction, + if (touchHandlers != null) + { + DispatchTouchHandlers( + touchHandlers, + new TouchEventInfo( + action, pointerIndex, - pointerCount, pointerId, - eventTimeNanos)) - return; - - if (timestampHandlers != null && timestampAction) - { - DispatchTimestampHandlers( - timestampHandlers, - new TouchTimestampInfo(action, pointerId, eventTimeNanos)); - } - - if (handlers == null) - return; - - TouchEventInfo info = new( - action, - pointerIndex, - pointerId, - eventTimeNanos, - AndroidInput.AMotionEvent_getX(inputEvent, pointerIndex), - AndroidInput.AMotionEvent_getY(inputEvent, pointerIndex)); - DispatchTouchHandlers(handlers, info); - } - catch (Exception exception) - { - LogOnce($"Failed to read native input event: {exception}"); + eventTimeNanos, + pointer.X, + pointer.Y)); } } - private static bool IsDuplicate( - int rawAction, - int pointerIndex, - int pointerCount, - int pointerId, - long eventTimeNanos) + private static void DispatchInputHandlers( + Action handlers, + AndroidInputEventInfo input) { - long now = Stopwatch.GetTimestamp(); - long windowTicks = Math.Max( - 1L, - Stopwatch.Frequency * DuplicateWindowMilliseconds / 1000L); - - lock (DedupLock) + foreach (Delegate handler in handlers.GetInvocationList()) { - long elapsed = now - s_lastDispatchTicks; - bool sameEventPayload = s_lastRawAction == rawAction - && s_lastPointerIndex == pointerIndex - && s_lastPointerCount == pointerCount - && s_lastPointerId == pointerId - && s_lastEventTimeNanos == eventTimeNanos; - bool duplicate = sameEventPayload - && elapsed >= 0L - && elapsed <= windowTicks; - - if (duplicate) - return true; - - s_lastRawAction = rawAction; - s_lastPointerIndex = pointerIndex; - s_lastPointerCount = pointerCount; - s_lastPointerId = pointerId; - s_lastEventTimeNanos = eventTimeNanos; - s_lastDispatchTicks = now; - return false; + try + { + ((Action)handler)(input); + } + catch (Exception exception) + { + LogOnce($"Input subscriber threw: {exception}"); + } } } diff --git a/StArray.ModManager.Android/Native/JavaInputBridge.cs b/StArray.ModManager.Android/Native/JavaInputBridge.cs new file mode 100644 index 0000000..e62c205 --- /dev/null +++ b/StArray.ModManager.Android/Native/JavaInputBridge.cs @@ -0,0 +1,455 @@ +using System.Runtime.InteropServices; +using System.Collections.Concurrent; +using ImGuiNET; +using StArray.ModManager.Android.UI; +using StArray.ModManager.Manager; + +namespace StArray.ModManager.Android.Native; + +public enum AndroidInputEventKind +{ + Motion = 1, + Key = 2, +} + +/// A pointer sample included in a forwarded Android MotionEvent. +public readonly record struct AndroidInputPointerInfo( + int Id, + int ToolType, + float X, + float Y, + float RawX, + float RawY, + float Pressure, + float Size, + float TouchMajor, + float TouchMinor, + float ToolMajor, + float ToolMinor, + float Orientation, + float Tilt, + float Distance, + ReadOnlyMemory AxisValues) +{ + /// Returns an Android MotionEvent axis value, or zero outside [0, 63]. + public float GetAxisValue(int axis) + => (uint)axis < (uint)AxisValues.Length ? AxisValues.Span[axis] : 0.0f; +} + +/// +/// One historical sample carried by a Java MotionEvent. Its timestamp uses the platform's +/// nanosecond API on Android 14 (API 34) and later, with a millisecond-resolution fallback on +/// earlier Android versions. +/// +public readonly record struct AndroidInputHistorySampleInfo( + long EventTimeNanos, + int PointerCount, + ReadOnlyMemory Pointers) +{ + public int StoredPointerCount => Pointers.Length; + public bool HasTruncatedPointers => StoredPointerCount < PointerCount; +} + +/// +/// Immutable snapshot of one Java KeyEvent or MotionEvent. EventTimeNanos and historical sample +/// times use the MotionEvent nanosecond APIs on API 34+; older Android versions and KeyEvent use +/// millisecond values promoted to nanoseconds. DownTimeNanos is currently sourced from the public +/// millisecond API. All timestamps use Android's uptime clock. A MotionEvent's current sample and +/// history are kept together so each Activity input dispatch produces exactly one OnInput callback. +/// Whether expanded MotionEvent axes/history sampling was enabled. +/// +public readonly record struct AndroidInputEventInfo( + AndroidInputEventKind Kind, + bool IsGenericMotion, + int Action, + int RawAction, + int ActionIndex, + int PointerCount, + ReadOnlyMemory Pointers, + int Source, + int DeviceId, + int Flags, + int MetaState, + int ButtonState, + int ActionButton, + int KeyCode, + int ScanCode, + int RepeatCount, + int UnicodeCodePoint, + int ViewportWidth, + int ViewportHeight, + long EventTimeNanos, + long DownTimeNanos, + float HorizontalScroll, + float VerticalScroll, + int HistorySampleCount, + ReadOnlyMemory HistoricalSamples, + bool FullDataIncluded) +{ + /// Number of pointers copied, limited to the bridge's 32-pointer bound. + public int StoredPointerCount => Pointers.Length; + + /// True when the source event had more pointers than the bridge can store. + public bool HasTruncatedPointers => StoredPointerCount < PointerCount; + + /// Number of historical samples copied, limited to the bridge's 256-sample bound. + public int StoredHistorySampleCount => HistoricalSamples.Length; + + /// True when one or more source history samples were not included. + public bool HasTruncatedHistory => StoredHistorySampleCount < HistorySampleCount; +} + +/// JNI interop for input events forwarded by the host Activity. +internal static unsafe class AndroidJavaInputBridge +{ + private const int AbiVersion = 2; + private const int MaxPointers = 32; + private const int MaxHistorySamples = 256; + private const int AxisCount = 64; + private const int MotionEventType = 1; + private const int KeyEventType = 2; + private const int MaxPendingImGuiEvents = 512; + + private static readonly ConcurrentQueue ImGuiInputQueue = new(); + private static int s_queuedImGuiEventCount; + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct NativeInputEventHeader + { + public int StructSize; + public int AbiVersion; + public int Type; + public int IsGenericMotion; + public int FullDataIncluded; + public int Action; + public int ActionIndex; + public int PointerCount; + public int StoredPointerCount; + public int Source; + public int DeviceId; + public int Flags; + public int MetaState; + public int ButtonState; + public int ActionButton; + public int KeyCode; + public int ScanCode; + public int RepeatCount; + public int UnicodeCodePoint; + public int ViewportWidth; + public int ViewportHeight; + public int RawAction; + public long EventTimeNanos; + public long DownTimeNanos; + public float HorizontalScroll; + public float VerticalScroll; + public int HistorySampleCount; + public int StoredHistorySampleCount; + public nint HistorySamples; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct NativeInputHistorySample + { + public int PointerCount; + public int StoredPointerCount; + public long EventTimeNanos; + public nint Pointers; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct NativeInputPointer + { + public int Id; + public int ToolType; + public float X; + public float Y; + public float RawX; + public float RawY; + public float Pressure; + public float Size; + public float TouchMajor; + public float TouchMinor; + public float ToolMajor; + public float ToolMinor; + public float Orientation; + public float Tilt; + public float Distance; + public fixed float AxisValues[AxisCount]; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeInputCallback(nint inputEvent); + + [DllImport("modmanager", EntryPoint = "modmanager_set_java_input_callback", + CallingConvention = CallingConvention.Cdecl)] + private static extern void SetInputCallback(nint callback); + + [DllImport("modmanager", EntryPoint = "modmanager_set_java_input_capture_state", + CallingConvention = CallingConvention.Cdecl)] + private static extern void SetCaptureStateNative(int overlayVisible, int captureMouse, + int captureKeyboard); + + [DllImport("modmanager", EntryPoint = "modmanager_set_java_input_full_data_enabled", + CallingConvention = CallingConvention.Cdecl)] + private static extern void SetFullDataEnabledNative(int enabled); + + private static readonly NativeInputCallback s_callback = OnNativeInput; + + internal static bool RegisterCallback() + { + try + { + SetInputCallback(Marshal.GetFunctionPointerForDelegate(s_callback)); + return true; + } + catch (Exception exception) when (exception is DllNotFoundException + or EntryPointNotFoundException or BadImageFormatException) + { + return false; + } + } + + internal static void SetCaptureState(bool captureMouse, bool captureKeyboard) + { + try + { + SetCaptureStateNative(0, captureMouse ? 1 : 0, captureKeyboard ? 1 : 0); + } + catch (Exception exception) when (exception is DllNotFoundException + or EntryPointNotFoundException or BadImageFormatException) + { + // Input capture is available only through the host Activity bridge. + } + } + + /// Called on the render thread after the current ImGui frame is built. + internal static void UpdateCaptureState() + { + if (!ImGuiInputHandler.IsInitialized) + return; + + try + { + var io = ImGui.GetIO(); + SetCaptureState(io.WantCaptureMouse, io.WantCaptureKeyboard); + } + catch (Exception exception) + { + Logger.Warn(nameof(AndroidJavaInputBridge), + $"Could not update Java input capture state: {exception.Message}"); + } + } + + internal static void SetFullDataEnabled(bool enabled) + { + try + { + SetFullDataEnabledNative(enabled ? 1 : 0); + } + catch (Exception exception) when (exception is DllNotFoundException + or EntryPointNotFoundException or BadImageFormatException) + { + // This option only controls optional MotionEvent axis sampling. + } + } + + private static void OnNativeInput(nint eventPointer) + { + if (eventPointer == nint.Zero) + return; + + try + { + NativeInputEventHeader* input = (NativeInputEventHeader*)eventPointer; + int requiredSize = sizeof(NativeInputEventHeader) + + MaxPointers * sizeof(NativeInputPointer); + if (input->AbiVersion != AbiVersion + || input->StructSize < requiredSize + || input->Type is not (MotionEventType or KeyEventType) + || input->PointerCount < 0 + || input->StoredPointerCount < 0 + || input->StoredPointerCount > MaxPointers + || input->StoredPointerCount > input->PointerCount + || input->StoredPointerCount != Math.Min(input->PointerCount, MaxPointers) + || input->FullDataIncluded is not (0 or 1) + || input->HistorySampleCount < 0 + || input->StoredHistorySampleCount < 0 + || input->StoredHistorySampleCount > MaxHistorySamples + || input->StoredHistorySampleCount > input->HistorySampleCount + || (input->StoredHistorySampleCount > 0 && input->FullDataIncluded == 0) + || (input->Type == KeyEventType + && (input->PointerCount != 0 + || input->StoredPointerCount != 0 + || input->HistorySampleCount != 0 + || input->StoredHistorySampleCount != 0)) + || (input->StoredHistorySampleCount > 0 && input->HistorySamples == nint.Zero)) + return; + + AndroidInputEventInfo snapshot = CopyEvent(input); + if (ImGuiInputHandler.IsInitialized) + EnqueueForImGui(CreateImGuiSnapshot(snapshot)); + + // Mod subscribers run synchronously on the Activity input-dispatch thread. + InputEvents.RaiseFromJava(snapshot); + } + catch (Exception exception) + { + // Never unwind a managed exception through Activity's Java input dispatch. + Logger.Error(nameof(AndroidJavaInputBridge), + $"Java input event dispatch failed: {exception}"); + } + } + + internal static AndroidInputEventInfo CopyEvent(NativeInputEventHeader* input) + { + int storedCount = input->StoredPointerCount; + var nativePointers = (NativeInputPointer*)((byte*)input + sizeof(NativeInputEventHeader)); + AndroidInputPointerInfo[] pointers = CopyPointers( + nativePointers, storedCount, input->FullDataIncluded != 0); + + int storedHistoryCount = input->StoredHistorySampleCount; + var history = new AndroidInputHistorySampleInfo[storedHistoryCount]; + NativeInputHistorySample* nativeHistory = (NativeInputHistorySample*)input->HistorySamples; + for (int index = 0; index < storedHistoryCount; index++) + { + NativeInputHistorySample* sample = &nativeHistory[index]; + if (sample->PointerCount < 0 + || sample->StoredPointerCount < 0 + || sample->StoredPointerCount > MaxPointers + || sample->StoredPointerCount > sample->PointerCount + || sample->StoredPointerCount != Math.Min(sample->PointerCount, MaxPointers) + || sample->PointerCount != input->PointerCount + || (sample->StoredPointerCount > 0 && sample->Pointers == nint.Zero)) + { + throw new InvalidOperationException("Invalid native MotionEvent history sample."); + } + + history[index] = new AndroidInputHistorySampleInfo( + sample->EventTimeNanos, + sample->PointerCount, + CopyPointers( + (NativeInputPointer*)sample->Pointers, + sample->StoredPointerCount, + input->FullDataIncluded != 0)); + } + + return new AndroidInputEventInfo( + input->Type == MotionEventType + ? AndroidInputEventKind.Motion + : AndroidInputEventKind.Key, + input->IsGenericMotion != 0, + input->Action, + input->RawAction, + input->ActionIndex, + input->PointerCount, + pointers, + input->Source, + input->DeviceId, + input->Flags, + input->MetaState, + input->ButtonState, + input->ActionButton, + input->KeyCode, + input->ScanCode, + input->RepeatCount, + input->UnicodeCodePoint, + input->ViewportWidth, + input->ViewportHeight, + input->EventTimeNanos, + input->DownTimeNanos, + input->HorizontalScroll, + input->VerticalScroll, + input->HistorySampleCount, + history, + input->FullDataIncluded != 0); + } + + private static AndroidInputPointerInfo[] CopyPointers( + NativeInputPointer* nativePointers, + int storedCount, + bool includeAxes) + { + if (storedCount == 0) + return Array.Empty(); + + var pointers = new AndroidInputPointerInfo[storedCount]; + float[]? axisStorage = includeAxes ? new float[storedCount * AxisCount] : null; + for (int index = 0; index < storedCount; index++) + { + NativeInputPointer* pointer = &nativePointers[index]; + ReadOnlyMemory axes = ReadOnlyMemory.Empty; + if (axisStorage != null) + { + for (int axis = 0; axis < AxisCount; axis++) + axisStorage[index * AxisCount + axis] = pointer->AxisValues[axis]; + axes = new ReadOnlyMemory(axisStorage, index * AxisCount, AxisCount); + } + + pointers[index] = new AndroidInputPointerInfo( + pointer->Id, + pointer->ToolType, + pointer->X, + pointer->Y, + pointer->RawX, + pointer->RawY, + pointer->Pressure, + pointer->Size, + pointer->TouchMajor, + pointer->TouchMinor, + pointer->ToolMajor, + pointer->ToolMinor, + pointer->Orientation, + pointer->Tilt, + pointer->Distance, + axes); + } + + return pointers; + } + + private static void EnqueueForImGui(AndroidInputEventInfo input) + { + ImGuiInputQueue.Enqueue(input); + int queued = Interlocked.Increment(ref s_queuedImGuiEventCount); + if (queued > MaxPendingImGuiEvents && ImGuiInputQueue.TryDequeue(out _)) + { + Interlocked.Decrement(ref s_queuedImGuiEventCount); + } + } + + private static AndroidInputEventInfo CreateImGuiSnapshot(AndroidInputEventInfo input) + { + ReadOnlySpan source = input.Pointers.Span; + var pointers = new AndroidInputPointerInfo[source.Length]; + for (int index = 0; index < source.Length; index++) + pointers[index] = source[index] with { AxisValues = ReadOnlyMemory.Empty }; + + return input with + { + Pointers = pointers, + HistorySampleCount = 0, + HistoricalSamples = ReadOnlyMemory.Empty, + FullDataIncluded = false, + }; + } + + /// Drain Java input on the ImGui render thread, never on Activity dispatch. + internal static void DispatchPendingImGuiEvents() + { + int pendingAtFrameStart = Math.Max(0, Volatile.Read(ref s_queuedImGuiEventCount)); + while (pendingAtFrameStart-- > 0 + && ImGuiInputQueue.TryDequeue(out AndroidInputEventInfo input)) + { + Interlocked.Decrement(ref s_queuedImGuiEventCount); + try + { + ImGuiInputHandler.DispatchJavaInput(input); + } + catch (Exception exception) + { + Logger.Error(nameof(AndroidJavaInputBridge), + $"Failed to apply queued ImGui input: {exception}"); + } + } + } +} diff --git a/StArray.ModManager.Android/UI/ImGuiEGLRender.cs b/StArray.ModManager.Android/UI/ImGuiEGLRender.cs index da2ddf0..87ddd24 100644 --- a/StArray.ModManager.Android/UI/ImGuiEGLRender.cs +++ b/StArray.ModManager.Android/UI/ImGuiEGLRender.cs @@ -121,11 +121,13 @@ public static int OnSwapBuffers(IntPtr display, IntPtr surface) var io = ImGui.GetIO(); io.DisplaySize = new Vector2(width, height); + StArray.ModManager.Android.Native.AndroidJavaInputBridge.DispatchPendingImGuiEvents(); ImGui.NewFrame(); // 构建 UI self.BuildUI(); ImGuiInputHandler.UpdateIme(); + StArray.ModManager.Android.Native.AndroidJavaInputBridge.UpdateCaptureState(); // 渲染 ImGui.Render(); ImGuiImplOpenGL3.RenderDrawData((IntPtr)ImGui.GetDrawData().NativePtr); diff --git a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs index 2eb0cd4..48dd4a1 100644 --- a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs +++ b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs @@ -1,14 +1,11 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using ImGuiNET; using StArray.ModManager.Android.Native; -using StArray.ModManager.Hooks; using StArray.ModManager.Manager; -using StArray.ModManager.Runtime; +using StArray.ModManager.Native; namespace StArray.ModManager.Android.UI; -/// ImGui input handler / 输入处理器 — touch/key hooks + IME control +/// ImGui input handler / 输入处理器 — Java bridge input + IME control public static partial class ImGuiInputHandler { /// ImGui 上下文就绪后由渲染器设置 @@ -16,16 +13,20 @@ public static partial class ImGuiInputHandler private static bool s_wantTextInputLast; + private static int s_javaPrimaryPointerId = -1; /// - /// 安装触摸事件和按键事件 Hook + /// 注册 Java Activity 输入桥接回调。宿主 APK 必须包含 Activity 转发补丁。 /// public static void InstallInputHooks() { if (!IsInitialized) return; try { - InstallHooks(); + if (!AndroidJavaInputBridge.RegisterCallback()) + Logger.Error(nameof(ImGuiInputHandler), + "Could not register the Java Activity input bridge callback."); + // IME 字符回调:Java nativeSendChar → C → 此回调 → ImGui NativeFunctions.SetOnAcceptCharCallback(codepoint => { @@ -68,88 +69,218 @@ public static void InstallInputHooks() IsInitialized = true; } - /* - /// 触摸事件 Hook 回调 - [NativeHook("libinput.so","_ZN7android13InputConsumer14consumeSamplesEPNS_26InputEventFactoryInterfaceERNS0_5BatchEmPjPPNS_10InputEventE")] - public unsafe static long OnConsumeSamples(void* thiz,void* factory, IntPtr batch, - ulong count, uint* outSeq, void** outEvent) - { - var result = OnConsumeSamplesOriginal(thiz,factory, batch, count, outSeq, outEvent); - if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); - return result; - } - - [NativeHook("libinput.so","_ZN7android13InputConsumer7consumeEPNS_26InputEventFactoryInterfaceEblPjPPNS_10InputEventE")] - public unsafe static long OnConsume(void* thiz, void* factory, bool consumeBatches, ulong frameTime, uint* outSeq, void** outEvent) + internal static void DispatchJavaInput(AndroidInputEventInfo input) { - var result = OnConsumeOriginal(thiz, factory, consumeBatches, frameTime, outSeq, outEvent); - if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); - return result; - }*/ - - [NativeHook("GetInitializeMotionEventAddress")] - public unsafe static bool OnInitializeMotionEvent(void* @event, void* message) - { - var result = OnInitializeMotionEventOriginal(@event, message); - var x = AndroidInput.AMotionEvent_getX(new(@event), 0); - var y = AndroidInput.AMotionEvent_getY(new(@event), 0); - if (InputEvents.HasSubscribers) - InputEvents.RaiseFrom(new IntPtr(@event)); - ImGuiImplAndroid.HandleInputEvent(new IntPtr(@event)); - return result; + if (!IsInitialized) + return; + + var io = ImGui.GetIO(); + if (input.Kind == AndroidInputEventKind.Key) + { + DispatchJavaKeyEvent(io, input); + return; + } + + ReadOnlySpan pointers = input.Pointers.Span; + if (input.Kind != AndroidInputEventKind.Motion || pointers.Length == 0) + return; + + int pointerCount = pointers.Length; + int actionIndex = Math.Clamp(input.ActionIndex, 0, pointerCount - 1); + int selectedIndex = actionIndex; + if (s_javaPrimaryPointerId >= 0) + { + for (int index = 0; index < pointerCount; index++) + { + if (pointers[index].Id == s_javaPrimaryPointerId) + { + selectedIndex = index; + break; + } + } + } + + AndroidInputPointerInfo pointer = pointers[selectedIndex]; + float scaleX = input.ViewportWidth > 0 + ? io.DisplaySize.X / input.ViewportWidth + : 1.0f; + float scaleY = input.ViewportHeight > 0 + ? io.DisplaySize.Y / input.ViewportHeight + : 1.0f; + io.AddMousePosEvent(pointer.X * scaleX, pointer.Y * scaleY); + + if (!input.IsGenericMotion) + { + DispatchJavaTouchAction(io, input, pointers, pointerCount, actionIndex); + return; + } + + UpdateJavaMouseButtons(io, input.ButtonState); + if (input.HorizontalScroll != 0.0f || input.VerticalScroll != 0.0f) + io.AddMouseWheelEvent(input.HorizontalScroll, input.VerticalScroll); } - private static nint GetInitializeMotionEventAddress() + private static void DispatchJavaTouchAction( + ImGuiIOPtr io, + AndroidInputEventInfo input, + ReadOnlySpan pointers, + int pointerCount, + int actionIndex) { - byte?[] sig = NativeFuncResolver.ParseHexPattern( - "e8 0f 19 fc fd 7b 01 a9 fc 6f 02 a9 fa 67 03 a9 " + - "f8 5f 04 a9 f6 57 05 a9 f4 4f 06 a9 fd 43 00 91"); - var r = new NativeFuncResolver("/system/lib64/libinput.so"); - long rva = r.FindSymbolRva("_ZN7android13InputConsumer21initializeMotionEventEPNS_11MotionEventEPKNS_12InputMessageE"); - try + AndroidInput.MotionAction action = (AndroidInput.MotionAction)input.Action; + int actionPointerId = pointers[actionIndex].Id; + switch (action) { - if (rva < 0) - { - var text = r.TextBytes; - long textAddr = r.TextBaseAddress; - int pos = 0; - while ((pos = NativeFuncResolver.Search(text, sig, pos)) >= 0) + case AndroidInput.MotionAction.Down: + case AndroidInput.MotionAction.PointerDown: + if (s_javaPrimaryPointerId < 0 || action == AndroidInput.MotionAction.Down) + s_javaPrimaryPointerId = actionPointerId; + if (actionPointerId == s_javaPrimaryPointerId) + io.AddMouseButtonEvent(0, true); + break; + + case AndroidInput.MotionAction.PointerUp: + if (actionPointerId == s_javaPrimaryPointerId) { - int ctxEnd = Math.Min(pos + 60, text.Length); - bool hasMrs = false, hasLdr = false; - for (int j = pos + 32; j <= ctxEnd - 4; j += 4) + int replacementIndex = -1; + for (int index = 0; index < pointerCount; index++) { - int inst = BitConverter.ToInt32(text.AsSpan(j)); - if (!hasMrs && (inst & 0xffffffe0) == 0xd53bd040) - hasMrs = true; - if (!hasLdr && (inst & 0xffe003e0) == 0xb9400020) + if (index != actionIndex) { - int imm12 = (inst >> 10) & 0xfff; - if (imm12 * 4 == 0xc) hasLdr = true; + replacementIndex = index; + break; } - if (hasMrs && hasLdr) break; } - if (hasMrs && hasLdr) + + if (replacementIndex >= 0) { - rva = textAddr + pos; - if (pos >= 4) - { - int prev = BitConverter.ToInt32(text.AsSpan(pos - 4)); - if (prev == unchecked((int)0xd503233f)) rva -= 4; - } - break; + s_javaPrimaryPointerId = pointers[replacementIndex].Id; + float scaleX = input.ViewportWidth > 0 + ? io.DisplaySize.X / input.ViewportWidth + : 1.0f; + float scaleY = input.ViewportHeight > 0 + ? io.DisplaySize.Y / input.ViewportHeight + : 1.0f; + io.AddMousePosEvent( + pointers[replacementIndex].X * scaleX, + pointers[replacementIndex].Y * scaleY); + } + else + { + s_javaPrimaryPointerId = -1; + io.AddMouseButtonEvent(0, false); } - pos++; } - if (rva < 0) throw new KeyNotFoundException("initializeMotionEvent not found by signature."); - } + break; + + case AndroidInput.MotionAction.Up: + case AndroidInput.MotionAction.Cancel: + s_javaPrimaryPointerId = -1; + io.AddMouseButtonEvent(0, false); + break; } - catch (Exception ex) + } + + private static void DispatchJavaKeyEvent( + ImGuiIOPtr io, + AndroidInputEventInfo input) + { + const int KeyActionDown = 0; + const int KeyActionUp = 1; + bool isDown = input.Action == KeyActionDown; + if (input.Action is KeyActionDown or KeyActionUp) { - Logger.Error(nameof(ImGuiInputHandler), ex.ToString()); + ImGuiKey key = MapAndroidKeyCode(input.KeyCode); + if (key != ImGuiKey.None) + io.AddKeyEvent(key, isDown); } - r.Load(); - return r.GetFuncPtr(rva); + + int meta = input.MetaState; + io.AddKeyEvent((ImGuiKey)4096, (meta & 0x1000) != 0); // Ctrl + io.AddKeyEvent((ImGuiKey)8192, (meta & 0x0001) != 0); // Shift + io.AddKeyEvent((ImGuiKey)16384, (meta & 0x0002) != 0); // Alt + io.AddKeyEvent((ImGuiKey)32768, (meta & 0x10000) != 0); // Meta/Super + + if (isDown && !s_wantTextInputLast && input.UnicodeCodePoint > 0 + && !char.IsControl((char)input.UnicodeCodePoint)) + { + io.AddInputCharacter((uint)input.UnicodeCodePoint); + } + } + + private static ImGuiKey MapAndroidKeyCode(int keyCode) + { + if (keyCode is >= 29 and <= 54) + return (ImGuiKey)(546 + keyCode - 29); // A-Z + if (keyCode is >= 7 and <= 16) + return (ImGuiKey)(536 + keyCode - 7); // 0-9 + if (keyCode is >= 131 and <= 142) + return (ImGuiKey)(572 + keyCode - 131); // F1-F12 + if (keyCode is >= 144 and <= 153) + return (ImGuiKey)(612 + keyCode - 144); // Keypad 0-9 + + return keyCode switch + { + 61 => (ImGuiKey)512, // Tab + 21 => (ImGuiKey)513, // Left + 22 => (ImGuiKey)514, // Right + 19 => (ImGuiKey)515, // Up + 20 => (ImGuiKey)516, // Down + 92 => (ImGuiKey)517, // PageUp + 93 => (ImGuiKey)518, // PageDown + 122 => (ImGuiKey)519, // Home + 123 => (ImGuiKey)520, // End + 124 => (ImGuiKey)521, // Insert + 112 => ImGuiKey.Delete, + 67 => ImGuiKey.Backspace, + 62 => (ImGuiKey)524, // Space + 66 => ImGuiKey.Enter, + 111 => (ImGuiKey)526, // Escape + 113 => (ImGuiKey)527, // Left Ctrl + 59 => (ImGuiKey)528, // Left Shift + 57 => (ImGuiKey)529, // Left Alt + 117 => (ImGuiKey)530, // Left Meta + 114 => (ImGuiKey)531, // Right Ctrl + 60 => (ImGuiKey)532, // Right Shift + 58 => (ImGuiKey)533, // Right Alt + 118 => (ImGuiKey)534, // Right Meta + 82 => (ImGuiKey)535, // Menu + 55 => (ImGuiKey)597, // Comma + 69 => (ImGuiKey)598, // Minus + 56 => (ImGuiKey)599, // Period + 76 => (ImGuiKey)600, // Slash + 74 => (ImGuiKey)601, // Semicolon + 70 => (ImGuiKey)602, // Equal + 71 => (ImGuiKey)603, // Left bracket + 73 => (ImGuiKey)604, // Backslash + 72 => (ImGuiKey)605, // Right bracket + 68 => (ImGuiKey)606, // Grave + 115 => (ImGuiKey)607, // Caps lock + 116 => (ImGuiKey)608, // Scroll lock + 143 => (ImGuiKey)609, // Num lock + 120 => (ImGuiKey)610, // Print screen + 121 => (ImGuiKey)611, // Pause + 154 => (ImGuiKey)623, // Keypad divide + 155 => (ImGuiKey)624, // Keypad multiply + 156 => (ImGuiKey)625, // Keypad subtract + 157 => (ImGuiKey)626, // Keypad add + 160 => (ImGuiKey)627, // Keypad enter + 161 => (ImGuiKey)628, // Keypad equal + 4 => (ImGuiKey)629, // App back + 125 => (ImGuiKey)630, // App forward + 75 => (ImGuiKey)596, // Apostrophe + 158 => (ImGuiKey)622, // Keypad decimal + _ => ImGuiKey.None, + }; + } + + private static void UpdateJavaMouseButtons(ImGuiIOPtr io, int buttonState) + { + io.AddMouseButtonEvent(0, (buttonState & 0x01) != 0); // primary + io.AddMouseButtonEvent(1, (buttonState & 0x02) != 0); // secondary + io.AddMouseButtonEvent(2, (buttonState & 0x04) != 0); // tertiary + io.AddMouseButtonEvent(3, (buttonState & 0x08) != 0); // back + io.AddMouseButtonEvent(4, (buttonState & 0x10) != 0); // forward } private static JavaClass? s_utilsClass; diff --git a/StArray.ModManager.Android/UI/ImGuiVulkanRenderer.cs b/StArray.ModManager.Android/UI/ImGuiVulkanRenderer.cs index 6690014..0f808fe 100644 --- a/StArray.ModManager.Android/UI/ImGuiVulkanRenderer.cs +++ b/StArray.ModManager.Android/UI/ImGuiVulkanRenderer.cs @@ -350,9 +350,11 @@ public static int OnQueuePresentKHR(IntPtr queue, IntPtr pPresentInfo) var io = ImGui.GetIO(); io.DisplaySize = new Vector2(self._fbWidth, self._fbHeight); + StArray.ModManager.Android.Native.AndroidJavaInputBridge.DispatchPendingImGuiEvents(); ImGui.NewFrame(); self.BuildUI(); ImGuiInputHandler.UpdateIme(); + StArray.ModManager.Android.Native.AndroidJavaInputBridge.UpdateCaptureState(); ImGui.Render(); // 录制 Vulkan 命令