diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index 42a1add7bebf..696d536c62c3 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -306,8 +306,8 @@ class FloatingWindowService : Service(), View.OnTouchListener { popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) // For host side, clipboard sync val idSyncClipboard = 1 - val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isCaptureStarted ?: false - if (isClipboardListenerEnabled) { + val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled() + if (isServiceSyncEnabled) { popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) } val idStopService = 2 diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 69b395ac2e9d..9f0f0216b727 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -24,4 +24,5 @@ object FFI { external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String external fun onClipboardUpdate(clips: ByteBuffer) + external fun isServiceClipboardEnabled(): Boolean } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e9382be9f9f4..db91e998b6ea 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -597,6 +597,8 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), + PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, + serverModel.toggleClipboard), ])); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 1d800ef69678..8775764619ee 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; + bool _clipboardOk = false; bool _showElevation = false; bool hideCm = false; int _connectStatus = 0; // Rendezvous Server status @@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier { bool get fileOk => _fileOk; + bool get clipboardOk => _clipboardOk; + bool get showElevation => _showElevation; int get connectStatus => _connectStatus; @@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption != 'N'; } + // clipboard + final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard); + _clipboardOk = clipOption != 'N'; + notifyListeners(); } @@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + toggleClipboard() async { + _clipboardOk = !clipboardOk; + bind.mainSetOption( + key: kOptionEnableClipboard, + value: clipboardOk ? defaultOptionYes : 'N'); + notifyListeners(); + } + toggleInput() async { if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7e3f39c83224..0bb17c9036dc 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -834,11 +834,19 @@ pub fn main_show_option(_key: String) -> SyncReturn { pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { - crate::ui_cm_interface::notify_input_control(config::option2bool( - config::keys::OPTION_ENABLE_KEYBOARD, - &value, - )); + crate::ui_cm_interface::switch_permission_all( + "keyboard".to_owned(), + config::option2bool(&key, &value), + ); + } + #[cfg(target_os = "android")] + if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + crate::ui_cm_interface::switch_permission_all( + "clipboard".to_owned(), + config::option2bool(&key, &value), + ); } + if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); #[cfg(target_os = "android")] @@ -2332,7 +2340,7 @@ pub mod server_side { use jni::{ errors::{Error as JniError, Result as JniResult}, objects::{JClass, JObject, JString}, - sys::jstring, + sys::{jboolean, jstring}, JNIEnv, }; @@ -2405,4 +2413,12 @@ pub mod server_side { }; return env.new_string(res).unwrap_or_default().into_raw(); } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( + env: JNIEnv, + _class: JClass, + ) -> jboolean { + jboolean::from(crate::server::is_clipboard_service_ok()) + } } diff --git a/src/ipc.rs b/src/ipc.rs index e3bcfac9a49f..5126aaf4e408 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -217,8 +217,6 @@ pub enum Data { MouseMoveTime(i64), Authorize, Close, - #[cfg(target_os = "android")] - InputControl(bool), #[cfg(windows)] SAS, UserSid(Option), diff --git a/src/server.rs b/src/server.rs index ed2c9f2fd541..ba1682f3d0f5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,6 +34,8 @@ pub mod audio_service; cfg_if::cfg_if! { if #[cfg(not(target_os = "ios"))] { mod clipboard_service; +#[cfg(target_os = "android")] +pub use clipboard_service::is_clipboard_service_ok; #[cfg(target_os = "linux")] pub(crate) mod wayland; #[cfg(target_os = "linux")] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index bfba41c92a03..8ae482500550 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -8,6 +8,8 @@ use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use clipboard_master::{CallbackResult, ClipboardHandler}; #[cfg(target_os = "android")] use hbb_common::config::{keys, option2bool}; +#[cfg(target_os = "android")] +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ io, sync::mpsc::{channel, RecvTimeoutError, Sender}, @@ -16,6 +18,9 @@ use std::{ #[cfg(windows)] use tokio::runtime::Runtime; +#[cfg(target_os = "android")] +static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); + #[cfg(not(target_os = "android"))] struct Handler { sp: EmptyExtraFieldService, @@ -27,6 +32,11 @@ struct Handler { rt: Option, } +#[cfg(target_os = "android")] +pub fn is_clipboard_service_ok() -> bool { + CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) +} + pub fn new() -> GenericService { let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); GenericService::run(&svc.clone(), run); @@ -224,11 +234,13 @@ impl Handler { #[cfg(target_os = "android")] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + CLIPBOARD_SERVICE_OK.store(sp.ok(), Ordering::SeqCst); while sp.ok() { if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { sp.send(msg); } std::thread::sleep(Duration::from_millis(INTERVAL)); } + CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 28f653fdec31..153740c28a3d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -463,11 +463,6 @@ impl Connection { conn.on_close("connection manager", true).await; break; } - #[cfg(target_os = "android")] - ipc::Data::InputControl(v) => { - conn.keyboard = v; - conn.send_permission(Permission::Keyboard, v).await; - } ipc::Data::CmErr(e) => { if e != "expected" { // cm closed before connection @@ -492,6 +487,9 @@ impl Connection { conn.keyboard = enabled; conn.send_permission(Permission::Keyboard, enabled).await; if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.can_sub_clipboard_service()); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -502,7 +500,7 @@ impl Connection { if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, - conn.inner.clone(), conn.clipboard_enabled() && conn.peer_keyboard_enabled()); + conn.inner.clone(), conn.can_sub_clipboard_service()); } } else if &name == "audio" { conn.audio = enabled; @@ -1372,16 +1370,7 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - // Do not consider the clipboard and keyboard permissions on Android. - // Because syncing the clipboard on Android is manually triggered by the user in the floating ball. - #[cfg(target_os = "android")] - let keyboard_clip_noperm = self.disable_keyboard || self.disable_clipboard; - #[cfg(not(target_os = "android"))] - let keyboard_clip_noperm = - !self.clipboard_enabled() || !self.peer_keyboard_enabled(); - if keyboard_clip_noperm - || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" - { + if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { @@ -1453,6 +1442,13 @@ impl Connection { self.clipboard && !self.disable_clipboard } + #[inline] + fn can_sub_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.peer_keyboard_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) != "Y" + } + fn audio_enabled(&self) -> bool { self.audio && !self.disable_audio } @@ -2930,7 +2926,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); } } @@ -2942,7 +2938,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); s.write().unwrap().subscribe( NAME_CURSOR, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d2d5b2c83354..a3373f8ccd72 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -280,15 +280,6 @@ pub fn close(id: i32) { }; } -#[inline] -#[cfg(target_os = "android")] -pub fn notify_input_control(v: bool) { - for (_, mut client) in CLIENTS.write().unwrap().iter_mut() { - client.keyboard = v; - allow_err!(client.tx.send(Data::InputControl(v))); - } -} - #[inline] pub fn remove(id: i32) { CLIENTS.write().unwrap().remove(&id); @@ -312,6 +303,17 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { }; } +#[inline] +#[cfg(target_os = "android")] +pub fn switch_permission_all(name: String, enabled: bool) { + for (_, client) in CLIENTS.read().unwrap().iter() { + allow_err!(client.tx.send(Data::SwitchPermission { + name: name.clone(), + enabled + })); + } +} + #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_clients_state() -> String {