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 e117f5b9f..c5da81c7c 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 @@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener { val popupMenu = PopupMenu(this, floatingView) val idShowRustDesk = 0 popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) - val idStopService = 1 + // For host side, clipboard sync + val idSyncClipboard = 1 + val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false + if (isClipboardListenerEnabled) { + popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) + } + val idStopService = 2 popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { @@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { openMainActivity() true } + idSyncClipboard -> { + syncClipboard() + true + } idStopService -> { stopMainService() true @@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { } } + private fun syncClipboard() { + MainActivity.rdClipboardManager?.syncClipboard(false) + } + private fun stopMainService() { MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 6b197a39f..15dee6002 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -13,6 +13,8 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.ClipboardManager +import android.os.Bundle import android.os.Build import android.os.IBinder import android.util.Log @@ -40,6 +42,9 @@ import kotlin.concurrent.thread class MainActivity : FlutterActivity() { companion object { var flutterMethodChannel: MethodChannel? = null + private var _rdClipboardManager: RdClipboardManager? = null + val rdClipboardManager: RdClipboardManager? + get() = _rdClipboardManager; } private val channelTag = "mChannel" @@ -90,11 +95,20 @@ class MainActivity : FlutterActivity() { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (_rdClipboardManager == null) { + _rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) + FFI.setClipboardManager(_rdClipboardManager!!) + } + } + override fun onDestroy() { Log.e(logTag, "onDestroy") mainService?.let { unbindService(serviceConnection) } + rdClipboardManager?.rustEnableServiceClipboard(false) super.onDestroy() } @@ -393,6 +407,15 @@ class MainActivity : FlutterActivity() { super.onStart() stopService(Intent(this, FloatingWindowService::class.java)) } + + // For client side + // When swithing from other app to this app, try to sync clipboard. + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + rdClipboardManager?.syncClipboard(true) + } + } } // https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt new file mode 100644 index 000000000..0e098cb08 --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt @@ -0,0 +1,224 @@ +package com.carriez.flutter_hbb + +import java.nio.ByteBuffer +import java.util.Timer +import java.util.TimerTask + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.util.Log +import androidx.annotation.Keep + +import hbb.MessageOuterClass.ClipboardFormat +import hbb.MessageOuterClass.Clipboard +import hbb.MessageOuterClass.MultiClipboards + +import ffi.FFI + +class RdClipboardManager(private val clipboardManager: ClipboardManager) { + private val logTag = "RdClipboardManager" + private val supportedMimeTypes = arrayOf( + ClipDescription.MIMETYPE_TEXT_PLAIN, + ClipDescription.MIMETYPE_TEXT_HTML + ) + + // 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`. + // 2. Avoid sending the clipboard data before enabling client clipboard. + // 1) Disable clipboard + // 2) Copy text "a" + // 3) Enable clipboard + // 4) Switch to another app + // 5) Switch back to the app + // 6) "a" should not be sent to the client, because it's copied before enabling clipboard + // + // It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`, + // though the `lastUpdatedClipData` will be set to null once. + private var lastUpdatedClipData: ClipData? = null + private var isClientEnabled = true; + private var _isListening = false; + val isListening: Boolean + get() = _isListening + + fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + // Only handle the first item in the clipboard for now. + val clip = clipData.getItemAt(0) + val isHostSync = !isClient && isSync + // Ignore the `isClipboardDataEqual()` check if it's a host sync operation. + // Because it's a action manually triggered by the user. + if (!isHostSync) { + if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { + Log.d(logTag, "Clipboard data is the same as last update, ignore") + return + } + } + val mimeTypeCount = clipData.description.getMimeTypeCount() + val mimeTypes = mutableListOf() + for (i in 0 until mimeTypeCount) { + mimeTypes.add(clipData.description.getMimeType(i)) + } + var text: CharSequence? = null; + var html: String? = null; + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + text = clip?.text + } + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { + text = clip?.text + html = clip?.htmlText + } + var count = 0 + val clips = MultiClipboards.newBuilder() + if (text != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString()) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build()) + count++ + } + if (html != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(html) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build()) + count++ + } + if (count > 0) { + val clipsBytes = clips.build().toByteArray() + val isClientFlag = if (isClient) 1 else 0 + val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply { + put(isClientFlag.toByte()) + put(clipsBytes) + } + clipsBuf.flip() + lastUpdatedClipData = clipData + Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote") + FFI.onClipboardUpdate(clipsBuf) + } + } + } + + private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener { + override fun onPrimaryClipChanged() { + Log.d(logTag, "onPrimaryClipChanged") + checkPrimaryClip(true, false) + } + } + + private fun isSupportedMimeType(mimeType: String): Boolean { + return supportedMimeTypes.contains(mimeType) + } + + private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean { + if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) { + return false + } + val mimeTypeCount = left.description.getMimeTypeCount() + for (i in 0 until mimeTypeCount) { + if (left.description.getMimeType(i) != right.description.getMimeType(i)) { + return false + } + } + + if (left.itemCount != right.itemCount) { + return false + } + for (i in 0 until left.itemCount) { + val mimeType = left.description.getMimeType(i) + if (!isSupportedMimeType(mimeType)) { + continue + } + val leftItem = left.getItemAt(i) + val rightItem = right.getItemAt(i) + if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) { + if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) { + return false + } + } + } + return true + } + + @Keep + fun rustEnableServiceClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening") + if (enable) { + if (!_isListening) { + clipboardManager.addPrimaryClipChangedListener(clipboardListener) + _isListening = true + } + } else { + if (_isListening) { + clipboardManager.removePrimaryClipChangedListener(clipboardListener) + _isListening = false + lastUpdatedClipData = null + } + } + } + + @Keep + fun rustEnableClientClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableClientClipboard: enable: $enable") + isClientEnabled = enable + if (enable) { + lastUpdatedClipData = clipboardManager.primaryClip + } else { + lastUpdatedClipData = null + } + } + + fun syncClipboard(isClient: Boolean) { + Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening") + if (isClient && !isClientEnabled) { + return + } + if (!isClient && !_isListening) { + return + } + checkPrimaryClip(isClient, true) + } + + @Keep + fun rustUpdateClipboard(clips: ByteArray) { + val clips = MultiClipboards.parseFrom(clips) + var mimeTypes = mutableListOf() + var text: String? = null + var html: String? = null + for (clip in clips.getClipboardsList()) { + when (clip.format) { + ClipboardFormat.Text -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN) + text = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.Html -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML) + html = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.ImageRgba -> { + } + ClipboardFormat.ImagePng -> { + } + else -> { + Log.e(logTag, "Unsupported clipboard format: ${clip.format}") + } + } + } + + val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray()) + var item: ClipData.Item? = null + if (text == null) { + Log.e(logTag, "No text content in clipboard") + return + } else { + if (html == null) { + item = ClipData.Item(text) + } else { + item = ClipData.Item(text, html) + } + } + if (item == null) { + Log.e(logTag, "No item in clipboard") + return + } + val clipData = ClipData(clipDescription, item) + lastUpdatedClipData = clipData + clipboardManager.setPrimaryClip(clipData) + } +} diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 43368c1e6..653465782 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -5,6 +5,8 @@ package ffi import android.content.Context import java.nio.ByteBuffer +import com.carriez.flutter_hbb.RdClipboardManager + object FFI { init { System.loadLibrary("rustdesk") @@ -12,6 +14,7 @@ object FFI { external fun init(ctx: Context) external fun initContext(ctx: Context) + external fun setClipboardManager(clipboardManager: RdClipboardManager) external fun startServer(app_dir: String, custom_client_config: String) external fun startService() external fun onVideoFrameUpdate(buf: ByteBuffer) @@ -21,4 +24,5 @@ object FFI { external fun setFrameRawEnable(name: String, value: Boolean) external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String -} \ No newline at end of file + external fun onClipboardUpdate(clips: ByteBuffer) +} diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index d6710b43d..db91e998b 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -596,7 +596,9 @@ class _PermissionCheckerState extends State { translate("android_version_audio_tip"), 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 1d800ef69..7754672c0 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/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index 4608597ce..0e48f60e6 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -5,9 +5,11 @@ use jni::sys::jboolean; use jni::JNIEnv; use jni::{ objects::{GlobalRef, JClass, JObject}, + strings::JNIString, JavaVM, }; +use hbb_common::{message_proto::MultiClipboards, protobuf::Message}; use jni::errors::{Error as JniError, Result as JniResult}; use lazy_static::lazy_static; use serde::Deserialize; @@ -16,6 +18,7 @@ use std::os::raw::c_void; use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; use std::sync::{Mutex, RwLock}; use std::time::{Duration, Instant}; + lazy_static! { static ref JVM: RwLock> = RwLock::new(None); static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info @@ -23,6 +26,9 @@ lazy_static! { static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); static ref NDK_CONTEXT_INITED: Mutex = Default::default(); static ref MEDIA_CODEC_INFOS: RwLock> = RwLock::new(None); + static ref CLIPBOARD_MANAGER: RwLock> = RwLock::new(None); + static ref CLIPBOARDS_HOST: Mutex> = Mutex::new(None); + static ref CLIPBOARDS_CLIENT: Mutex> = Mutex::new(None); } const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100); @@ -105,6 +111,14 @@ pub fn get_audio_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { AUDIO_RAW.lock().ok()?.take(dst, last) } +pub fn get_clipboards(client: bool) -> Option { + if client { + CLIPBOARDS_CLIENT.lock().ok()?.take() + } else { + CLIPBOARDS_HOST.lock().ok()?.take() + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate( env: JNIEnv, @@ -133,6 +147,27 @@ pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate( } } +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onClipboardUpdate( + env: JNIEnv, + _class: JClass, + buffer: JByteBuffer, +) { + if let Ok(data) = env.get_direct_buffer_address(&buffer) { + if let Ok(len) = env.get_direct_buffer_capacity(&buffer) { + let data = unsafe { std::slice::from_raw_parts(data, len) }; + if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) { + let is_client = data[0] == 1; + if is_client { + *CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips); + } else { + *CLIPBOARDS_HOST.lock().unwrap() = Some(clips); + } + } + } + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( env: JNIEnv, @@ -157,7 +192,11 @@ pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObje log::debug!("MainService init from java"); if let Ok(jvm) = env.get_java_vm() { let java_vm = jvm.get_java_vm_pointer() as *mut c_void; - *JVM.write().unwrap() = Some(jvm); + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); if let Ok(context) = env.new_global_ref(ctx) { let context_jobject = context.as_obj().as_raw() as *mut c_void; *MAIN_SERVICE_CTX.write().unwrap() = Some(context); @@ -178,6 +217,26 @@ pub extern "system" fn Java_ffi_FFI_initContext(env: JNIEnv, _class: JClass, ctx } } +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setClipboardManager( + env: JNIEnv, + _class: JClass, + clipboard_manager: JObject, +) { + log::debug!("ClipboardManager init from java"); + if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); + if let Ok(manager) = env.new_global_ref(clipboard_manager) { + *CLIPBOARD_MANAGER.write().unwrap() = Some(manager); + } + } +} + #[derive(Debug, Deserialize, Clone)] pub struct MediaCodecInfo { pub name: String, @@ -287,6 +346,59 @@ pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> { } } +fn _call_clipboard_manager(name: S, sig: T, args: &[JValue]) -> JniResult<()> +where + S: Into, + T: Into + AsRef, +{ + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + env.call_method(cm, name, sig, args)?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> { + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + let data = env.byte_array_from_slice(data)?; + + env.call_method( + cm, + "rustUpdateClipboard", + "([B)V", + &[JValue::Object(&JObject::from(data))], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_enable_service_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableServiceClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + +pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableClientClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + pub fn call_main_service_get_by_name(name: &str) -> JniResult { if let (Some(jvm), Some(ctx)) = ( JVM.read().unwrap().as_ref(), diff --git a/src/client.rs b/src/client.rs index 2abc4d0ff..c36063530 100644 --- a/src/client.rs +++ b/src/client.rs @@ -71,8 +71,10 @@ use crate::{ ui_session_interface::{InvokeUiSession, Session}, }; +#[cfg(not(target_os = "ios"))] +use crate::clipboard::CLIPBOARD_INTERVAL; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -131,7 +133,7 @@ pub(crate) struct ClientClipboardContext { /// Client of the remote desktop. pub struct Client; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] struct TextClipboardState { is_required: bool, running: bool, @@ -144,6 +146,10 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); +} + +#[cfg(not(target_os = "ios"))] +lazy_static::lazy_static! { static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } @@ -648,12 +654,12 @@ impl Client { #[inline] #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] fn try_stop_clipboard() { // There's a bug here. // If session is closed by the peer, `has_sessions_running()` will always return true. @@ -748,9 +754,41 @@ impl Client { Some(rx_started) } + + #[cfg(target_os = "android")] + fn try_start_clipboard(_p: Option<()>) -> Option> { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return None; + } + clipboard_lock.running = true; + + log::info!("Start text clipboard loop"); + std::thread::spawn(move || { + loop { + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + continue; + } + + if let Some(msg) = crate::clipboard::get_clipboards_msg(true) { + crate::flutter::send_text_clipboard_msg(msg); + } + + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + } + log::info!("Stop text clipboard loop"); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + }); + + None + } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] impl TextClipboardState { fn new() -> Self { Self { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index cc74c96ed..9aec89dd3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -8,9 +8,9 @@ use std::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] -use crate::{audio_service, ConnInner, CLIENT_SERVER}; +use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER}; use crate::{ client::{ self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, @@ -302,7 +302,7 @@ impl Remote { .unwrap() .set_disconnected(round); - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if _set_disconnected_ok { Client::try_stop_clipboard(); } @@ -1177,7 +1177,7 @@ impl Remote { self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1188,7 +1188,7 @@ impl Remote { }, )); // To make sure current text clipboard data is updated. - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Some(mut rx) = rx { timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); } @@ -1209,6 +1209,11 @@ impl Remote { }); } } + // to-do: Android, is `sync_init_clipboard` really needed? + // https://github.com/rustdesk/rustdesk/discussions/9010 + + #[cfg(target_os = "android")] + crate::flutter::update_text_clipboard_required(); // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1240,7 +1245,7 @@ impl Remote { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(vec![cb], ClipboardSide::Client); - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -1251,12 +1256,16 @@ impl Remote { self.handler.clipboard(content); } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); } } Some(message::Union::MultiClipboards(_mcb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(_mcb.clipboards, ClipboardSide::Client); + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -1421,14 +1430,14 @@ impl Remote { Ok(Permission::Keyboard) => { *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); self.handler.set_permission("clipboard", p.enabled); } diff --git a/src/clipboard.rs b/src/clipboard.rs index 329b392bb..ac3a83f00 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,4 +1,6 @@ +#[cfg(not(target_os = "android"))] use arboard::{ClipboardData, ClipboardFormat}; +#[cfg(not(target_os = "android"))] use clipboard_master::{ClipboardHandler, Master, Shutdown}; use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ @@ -16,6 +18,7 @@ const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; // Add special format for Excel XML Spreadsheet const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; +#[cfg(not(target_os = "android"))] lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg @@ -27,9 +30,12 @@ lazy_static::lazy_static! { static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } +#[cfg(not(target_os = "android"))] const CLIPBOARD_GET_MAX_RETRY: usize = 3; +#[cfg(not(target_os = "android"))] const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); +#[cfg(not(target_os = "android"))] const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::Text, ClipboardFormat::Html, @@ -146,6 +152,7 @@ impl ClipboardContext { } } +#[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, side: ClipboardSide, @@ -194,6 +201,7 @@ pub fn check_clipboard_cm() -> ResultType { } } +#[cfg(not(target_os = "android"))] fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { @@ -224,17 +232,20 @@ fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { } } +#[cfg(not(target_os = "android"))] pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { std::thread::spawn(move || { update_clipboard_(multi_clipboards, side); }); } +#[cfg(not(target_os = "android"))] #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, } +#[cfg(not(target_os = "android"))] #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { @@ -337,10 +348,20 @@ impl ClipboardContext { pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { use hbb_common::get_version_number; - get_version_number(peer_version) >= get_version_number("1.3.0") - && !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform) + if get_version_number(peer_version) < get_version_number("1.3.0") { + return false; + } + if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) { + return false; + } + if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3") + { + return false; + } + true } +#[cfg(not(target_os = "android"))] pub fn get_current_clipboard_msg( peer_version: &str, peer_platform: &str, @@ -406,6 +427,7 @@ impl std::fmt::Display for ClipboardSide { } } +#[cfg(not(target_os = "android"))] pub fn start_clipbard_master_thread( handler: impl ClipboardHandler + Send + 'static, tx_start_res: Sender<(Option, String)>, @@ -437,6 +459,7 @@ pub fn start_clipbard_master_thread( pub use proto::get_msg_if_not_support_multi_clip; mod proto { + #[cfg(not(target_os = "android"))] use arboard::ClipboardData; use hbb_common::{ compress::{compress as compress_func, decompress}, @@ -459,6 +482,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn image_to_proto(a: arboard::ImageData) -> Clipboard { match &a { arboard::ImageData::Rgba(rgba) => { @@ -519,6 +543,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn clipboard_data_to_proto(data: ClipboardData) -> Option { let d = match data { ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), @@ -531,6 +556,7 @@ mod proto { Some(d) } + #[cfg(not(target_os = "android"))] pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { MultiClipboards { clipboards: vec_data @@ -541,6 +567,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn from_clipboard(clipboard: Clipboard) -> Option { let data = if clipboard.compress { decompress(&clipboard.content) @@ -569,6 +596,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { multi_clipboards .into_iter() @@ -597,3 +625,49 @@ mod proto { }) } } + +#[cfg(target_os = "android")] +pub fn handle_msg_clipboard(mut cb: Clipboard) { + use hbb_common::protobuf::Message; + + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + let multi_clips = MultiClipboards { + clipboards: vec![cb], + ..Default::default() + }; + if let Ok(bytes) = multi_clips.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) { + use hbb_common::protobuf::Message; + + for cb in mcb.clipboards.iter_mut() { + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + } + if let Ok(bytes) = mcb.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn get_clipboards_msg(client: bool) -> Option { + let mut clipboards = scrap::android::ffi::get_clipboards(client)?; + let mut msg = Message::new(); + for c in &mut clipboards.clipboards { + let compressed = hbb_common::compress::compress(&c.content); + let compress = compressed.len() < c.content.len(); + if compress { + c.content = compressed.into(); + } + c.compress = compress; + } + msg.set_multi_clipboards(clipboards); + Some(msg) +} diff --git a/src/flutter.rs b/src/flutter.rs index a1c9c7e34..f6fff4234 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1250,15 +1250,17 @@ fn try_send_close_event(event_stream: &Option>) { } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn update_text_clipboard_required() { let is_required = sessions::get_sessions() .iter() .any(|s| s.is_text_clipboard_required()); + #[cfg(target_os = "android")] + let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); Client::set_is_text_clipboard_required(is_required); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { @@ -2051,7 +2053,7 @@ pub mod sessions { } #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn has_sessions_running(conn_type: ConnType) -> bool { SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| { *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7a0c5e874..ba03bc761 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -274,7 +274,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { session.toggle_option(value.clone()); try_sync_peer_option(&session, &session_id, &value, None); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } @@ -817,6 +817,17 @@ pub fn main_show_option(_key: String) -> SyncReturn { SyncReturn(false) } +#[inline] +#[cfg(target_os = "android")] +fn enable_server_clipboard(keyboard_enabled: &str, clip_enabled: &str) { + use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; + let keyboard_enabled = + config::option2bool(config::keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); + let clip_enabled = config::option2bool(config::keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); + crate::ui_cm_interface::switch_permission_all("clipboard".to_owned(), clip_enabled); + let _ = call_clipboard_manager_enable_service_clipboard(keyboard_enabled && clip_enabled); +} + pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { @@ -824,6 +835,11 @@ pub fn main_set_option(key: String, value: String) { config::keys::OPTION_ENABLE_KEYBOARD, &value, )); + enable_server_clipboard(&value, &get_option(config::keys::OPTION_ENABLE_CLIPBOARD)); + } + #[cfg(target_os = "android")] + if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + enable_server_clipboard(&get_option(config::keys::OPTION_ENABLE_KEYBOARD), &value); } if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 31fd680fd..039ad4b11 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index fbe161535..26281c26b 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 4f0131cc8..46126056c 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8e0ff1479..d680b66a5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 12b3a4257..d15c1b6ba 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), + ("Update client clipboard", "更新客户端的粘贴板"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a9fb5b233..c4ff80c7e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 34e5433f5..905f31739 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a73221371..b9e1e6cf9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e6df3bc3d..73a306c7c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 876c901a4..83747f03c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ce77f620c..7f3bf0f70 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Subir carpeta"), ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 21de56c9e..9f67c1226 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index ac958d79c..93f5a60b4 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ff4381573..33c6b7427 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 85b1354c3..8c8332ad1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index b63d42122..400b5156b 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 4a3136c83..d6389480a 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c80bb654d..fc58fe5a6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 066f2980c..b488f5740 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e0dd83db6..bee9d322b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5edd50572..1d0f3b7ea 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 4960e752a..f266f2536 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "폴더 업로드"), ("Upload files", "파일 업로드"), ("Clipboard is synchronized", "클립보드가 동기화됨"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 07ca645f2..46733ce71 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 9a2069163..723b46a30 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 4c78dfbd9..0439a45e6 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index a91e31e45..c9f3ce243 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index ca46a3285..a48683914 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fd5641ac1..fb77c9abb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Wyślij folder"), ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c0564e0f4..3fe795187 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 14254388c..f382b7aba 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index cbce2f2a9..7aaef0e01 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6d173f109..0344f7270 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Загрузить папку"), ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b3c8fddf9..50ba1aeb0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 20fd24c9c..4e52bbe40 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7c63c8ea5..abab6acd8 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e80bb6181..96bf3e1e0 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index dae48e7a3..69806fa7f 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 60b281851..1b0cf69e4 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 71af446c1..a657201e9 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ce11544b5..1b7b783d3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 86aad4fee..fb9259ef9 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", "剪貼簿已同步"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index ff5c8b64a..3ef8f4c6f 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5a2c47bef..0d4751cd4 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 7f9ca4e9a..693f36dbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ mod custom_server; mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] mod clipboard; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/server.rs b/src/server.rs index 46c30b8bc..ed2c9f2fd 100644 --- a/src/server.rs +++ b/src/server.rs @@ -32,7 +32,7 @@ use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { -if #[cfg(not(any(target_os = "android", target_os = "ios")))] { +if #[cfg(not(target_os = "ios"))] { mod clipboard_service; #[cfg(target_os = "linux")] pub(crate) mod wayland; @@ -42,17 +42,20 @@ pub mod uinput; pub mod rdp_input; #[cfg(target_os = "linux")] pub mod dbus; +#[cfg(not(target_os = "android"))] pub mod input_service; } else { mod clipboard_service { pub const NAME: &'static str = ""; } +} +} + +#[cfg(any(target_os = "android", target_os = "ios"))] pub mod input_service { -pub const NAME_CURSOR: &'static str = ""; -pub const NAME_POS: &'static str = ""; -pub const NAME_WINDOW_FOCUS: &'static str = ""; -} -} + pub const NAME_CURSOR: &'static str = ""; + pub const NAME_POS: &'static str = ""; + pub const NAME_WINDOW_FOCUS: &'static str = ""; } mod connection; @@ -99,10 +102,12 @@ pub fn new() -> ServerPtr { }; server.add_service(Box::new(audio_service::new())); #[cfg(not(target_os = "ios"))] - server.add_service(Box::new(display_service::new())); + { + server.add_service(Box::new(display_service::new())); + server.add_service(Box::new(clipboard_service::new())); + } #[cfg(not(any(target_os = "android", target_os = "ios")))] { - server.add_service(Box::new(clipboard_service::new())); if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 3aadb3ad5..401bb4933 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,11 +1,15 @@ use super::*; -pub use crate::clipboard::{ - check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, - CLIPBOARD_NAME as NAME, -}; +#[cfg(not(target_os = "android"))] +pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; +pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; #[cfg(windows)] use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; +#[cfg(not(target_os = "android"))] use clipboard_master::{CallbackResult, ClipboardHandler}; +#[cfg(target_os = "android")] +use hbb_common::config::{keys, option2bool}; +#[cfg(target_os = "android")] +use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; use std::{ io, sync::mpsc::{channel, RecvTimeoutError, Sender}, @@ -14,6 +18,7 @@ use std::{ #[cfg(windows)] use tokio::runtime::Runtime; +#[cfg(not(target_os = "android"))] struct Handler { sp: EmptyExtraFieldService, ctx: Option, @@ -25,11 +30,12 @@ struct Handler { } pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); GenericService::run(&svc.clone(), run); svc.sp } +#[cfg(not(target_os = "android"))] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { let (tx_cb_result, rx_cb_result) = channel(); let handler = Handler { @@ -73,9 +79,9 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { Ok(()) } +#[cfg(not(target_os = "android"))] impl ClipboardHandler for Handler { fn on_clipboard_change(&mut self) -> CallbackResult { - self.sp.snapshot(|_sps| Ok(())).ok(); if self.sp.ok() { if let Some(msg) = self.get_clipboard_msg() { self.sp.send(msg); @@ -92,6 +98,7 @@ impl ClipboardHandler for Handler { } } +#[cfg(not(target_os = "android"))] impl Handler { fn get_clipboard_msg(&mut self) -> Option { #[cfg(target_os = "windows")] @@ -216,3 +223,25 @@ impl Handler { bail!("failed to get clipboard data from cm"); } } + +#[cfg(target_os = "android")] +fn is_clipboard_enabled() -> bool { + let keyboard_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_KEYBOARD); + let keyboard_enabled = option2bool(keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); + let clip_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_CLIPBOARD); + let clip_enabled = option2bool(keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); + keyboard_enabled && clip_enabled +} + +#[cfg(target_os = "android")] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + let _res = call_clipboard_manager_enable_service_clipboard(is_clipboard_enabled()); + while sp.ok() { + if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { + sp.send(msg); + } + std::thread::sleep(Duration::from_millis(INTERVAL)); + } + let _res = call_clipboard_manager_enable_service_clipboard(false); + Ok(()) +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 3315cac69..4bdda795f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -690,7 +690,7 @@ impl Connection { } } Some(message::Union::MultiClipboards(_multi_clipboards)) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { if let Err(err) = conn.stream.send(&msg_out).await { conn.on_close(&err.to_string(), false).await; @@ -2074,7 +2074,9 @@ impl Connection { if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(vec![cb], ClipboardSide::Host); - #[cfg(all(feature = "flutter", target_os = "android"))] + // ios as the controlled side is actually not supported for now. + // The following code is only used to preserve the logic of handling text clipboard on mobile. + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -2092,14 +2094,17 @@ impl Connection { } } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); } } - Some(message::Union::MultiClipboards(_mcb)) => - { + Some(message::Union::MultiClipboards(_mcb)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { update_clipboard(_mcb.clipboards, ClipboardSide::Host); } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } Some(message::Union::Cliprdr(_clip)) => { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c34e15e26..c34671d57 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -312,6 +312,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 { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 321707d3f..4bd4b6e2d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -354,7 +354,7 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn is_text_clipboard_required(&self) -> bool { *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() @@ -526,10 +526,7 @@ impl Session { #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_xfce(&self) -> bool { - #[cfg(not(any(target_os = "ios")))] - return crate::platform::is_xfce(); - #[cfg(any(target_os = "ios"))] - false + crate::platform::is_xfce() } pub fn remove_port_forward(&self, port: i32) {