feat: android clipboard, multi-formats (#9950)
Some checks are pending
CI / ${{ matrix.job.target }} (${{ matrix.job.os }}) (map[os:ubuntu-20.04 target:x86_64-unknown-linux-gnu]) (push) Waiting to run
Full Flutter CI / run-ci (push) Waiting to run

* feat: android clipboard, multi-formats

Signed-off-by: fufesou <linlong1266@gmail.com>

* Chore

Signed-off-by: fufesou <linlong1266@gmail.com>

* Remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-11-18 15:43:41 +08:00 committed by GitHub
parent 0707e791e8
commit 8b710f62c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 670 additions and 47 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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<String>()
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<String>()
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)
}
}

View File

@ -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
}
external fun onClipboardUpdate(clips: ByteBuffer)
}

View File

@ -596,7 +596,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
translate("android_version_audio_tip"),
style: const TextStyle(color: MyTheme.darkGray),
))
])
]),
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
]));
}
}

View File

@ -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);

View File

@ -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<Option<JavaVM>> = RwLock::new(None);
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
@ -23,6 +26,9 @@ lazy_static! {
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
static ref CLIPBOARD_MANAGER: RwLock<Option<GlobalRef>> = RwLock::new(None);
static ref CLIPBOARDS_HOST: Mutex<Option<MultiClipboards>> = Mutex::new(None);
static ref CLIPBOARDS_CLIENT: Mutex<Option<MultiClipboards>> = 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<u8>, last: &mut Vec<u8>) -> Option<()> {
AUDIO_RAW.lock().ok()?.take(dst, last)
}
pub fn get_clipboards(client: bool) -> Option<MultiClipboards> {
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<S, T>(name: S, sig: T, args: &[JValue]) -> JniResult<()>
where
S: Into<JNIString>,
T: Into<JNIString> + AsRef<str>,
{
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<String> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),

View File

@ -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<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
}
#[cfg(not(target_os = "ios"))]
lazy_static::lazy_static! {
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = 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<UnboundedReceiver<()>> {
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 {

View File

@ -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<T: InvokeUiSession> Remote<T> {
.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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
},
));
// 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<T: InvokeUiSession> Remote<T> {
});
}
}
// 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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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);
}

View File

@ -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<Mutex<()>> = Arc::new(Mutex::new(()));
// cache the clipboard msg
@ -27,9 +30,12 @@ lazy_static::lazy_static! {
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = 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<ClipboardContext>,
side: ClipboardSide,
@ -194,6 +201,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
}
}
#[cfg(not(target_os = "android"))]
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, 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<Clipboard>, side: ClipboardSide) {
}
}
#[cfg(not(target_os = "android"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, 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<Shutdown>, 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<Clipboard> {
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<ClipboardData>) -> MultiClipboards {
MultiClipboards {
clipboards: vec_data
@ -541,6 +567,7 @@ mod proto {
}
}
#[cfg(not(target_os = "android"))]
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
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<Clipboard>) -> Vec<ClipboardData> {
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<Message> {
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)
}

View File

@ -1250,15 +1250,17 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
}
}
#[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

View File

@ -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<bool> {
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());

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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"))]

View File

@ -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()));

View File

@ -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<ClipboardContext>,
@ -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<Message> {
#[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(())
}

View File

@ -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)) =>
{

View File

@ -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 {

View File

@ -354,7 +354,7 @@ impl<T: InvokeUiSession> Session<T> {
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<T: InvokeUiSession> Session<T> {
#[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) {