From 174a3f8ac0eea7a670955999515586749e469ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Fri, 30 Oct 2026 11:29:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(viewer-core,android-ffi):=20=E4=B8=BB?= =?UTF-8?q?=E6=8E=A7=E7=AB=AF=E6=96=87=E6=9C=AC=E4=B8=8A=E8=A1=8C=E4=B8=8E?= =?UTF-8?q?=20JNI=20=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session::send_text 收 &[u16] 而不是 &str:整条链(Java 字符串 → JNI → 线格式 → Windows 注入)的原生表示都是 UTF-16,任何一次 UTF-8 往返都有损,故中间层不做转换。 空输入与超限在本层拦掉,超限走 clamp_text_units 并打日志。 nativeSendText 的参数用 char[](JObject 收再转 JCharArray),经 GetCharArrayRegion 按 u16 直接拷贝。不用 JString + get_string:jni 0.21 那条路走 Modified-UTF-8, IME 提交边界的落单代理项会在往返里丢。JNI 取数失败必须 eprintln,不能静默—— 用户对着远端打字最坏的情况是「看起来发送了」。 --- android-ffi/src/lib.rs | 41 ++++++++++++++++++++++++++++++++++++++++- viewer-core/src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/android-ffi/src/lib.rs b/android-ffi/src/lib.rs index 0a3e9b1..d283387 100644 --- a/android-ffi/src/lib.rs +++ b/android-ffi/src/lib.rs @@ -20,7 +20,7 @@ //! 会话结束则整个返回 null)。 use common::InputEvent; -use jni::objects::{JClass, JString}; +use jni::objects::{JCharArray, JClass, JObject, JString}; use jni::sys::{jbyteArray, jint, jlong, jstring}; use jni::JNIEnv; use std::sync::{Arc, Mutex}; @@ -262,6 +262,45 @@ pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendKey( }); } +/// 协议 v3:文本上行。`units` 是 Kotlin 侧的 `CharArray`(UTF-16 code unit)。 +/// +/// 为什么不用 `JString`:jni 0.21 的 `get_string` 走 Modified-UTF-8 → Rust `String`, +/// 落单代理项(IME 提交边界常见)会在这次往返里丢失。`char[]` 经 +/// `GetCharArrayRegion` 按 u16 直接拷贝,零转换(`jchar = u16`,见 jni-sys-0.3.1/lib.rs:15)。 +/// +/// 截断与空输入的判断在 `viewer_core::Session::send_text` 里做,与被控端同一口径。 +/// 失败必须有可见后果:JNI 取数失败走 eprintln(Android 上落 stderr→logcat), +/// 不静默返回成功——用户对着远端打字却没字,最坏的就是「看起来发送了」。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendText<'local>( + // 不需要 `mut`:jni 0.21.1 的 `get_array_length` / `get_char_array_region` + // 都是 `&self`(jnienv.rs:1652 / :1887),与 `new_string` 之类的 `&mut self` 不同。 + env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, + units: JObject<'local>, +) { + if units.as_raw().is_null() { + // Kotlin 侧不会传 null,但 JNI 入口的对端数据一律不信任(全局安全规则) + return; + } + let arr = JCharArray::from(units); + let len = match env.get_array_length(&arr) { + Ok(n) if n > 0 => n as usize, + Ok(_) => return, + Err(e) => { + eprintln!("[android-ffi] get_array_length failed: {e}"); + return; + } + }; + let mut buf = vec![0u16; len]; + if let Err(e) = env.get_char_array_region(&arr, 0, &mut buf) { + eprintln!("[android-ffi] get_char_array_region failed: {e}"); + return; + } + with_session(handle, |s| s.send_text(&buf)); +} + #[no_mangle] pub extern "system" fn Java_com_verify_remote_RustBridge_nativeRequestKeyframe( _env: JNIEnv, diff --git a/viewer-core/src/lib.rs b/viewer-core/src/lib.rs index dd371fa..9760f24 100644 --- a/viewer-core/src/lib.rs +++ b/viewer-core/src/lib.rs @@ -344,6 +344,29 @@ impl Session { let _ = self.tx.send(Command::Input(ev)); } + /// 协议 v3:发送一段文本(UTF-16 code unit)到被控端注入。 + /// + /// 为什么参数是 `&[u16]` 而不是 `&str`:被控端的注入通道(Windows + /// `KEYEVENTF_UNICODE`)与主控端来源(Java 字符串)都是 UTF-16 code unit, + /// 中间任何一次 UTF-8 往返都会把 IME 提交边界的落单代理项变成替换字符。 + /// 见 `common::InputEvent::Text` 的取舍说明。 + /// + /// 超限处理:截断到 [`common::MAX_TEXT_UNITS`] 并打日志——静默丢弃会让用户 + /// 以为长文本已送达。日志只报长度,不报内容。 + pub fn send_text(&self, units: &[u16]) { + if units.is_empty() { + return; // 空提交没有注入语义,也不该占用一条消息 + } + let (units, truncated) = common::clamp_text_units(units.to_vec()); + if truncated { + log(&format!( + "文本超过上限,已截断到 {} 个 code unit", + common::MAX_TEXT_UNITS + )); + } + self.send_input(InputEvent::Text { units }); + } + /// 请求被控端强制关键帧(解码器错误/跳帧后的恢复路径)。 pub fn request_keyframe(&self) { let _ = self.tx.send(Command::KeyFrameReq);