fix: Android, try sync clipboard on connecting (#10218)

* fix: Android, try sync clipboard on connecting

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

* Android, clipboard, more clear skip check

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

* comments

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

* comment todo: Android clipboard listener, callback twice

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

* Android, clipboard, remove listner

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-12-07 15:12:15 +08:00 committed by GitHub
parent 8f44787ba3
commit 3c838e7a92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 41 additions and 118 deletions

View File

@ -306,7 +306,7 @@ class FloatingWindowService : Service(), View.OnTouchListener {
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
// For host side, clipboard sync // For host side, clipboard sync
val idSyncClipboard = 1 val idSyncClipboard = 1
val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isCaptureStarted ?: false
if (isClipboardListenerEnabled) { if (isClipboardListenerEnabled) {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
} }

View File

@ -103,7 +103,6 @@ class MainActivity : FlutterActivity() {
mainService?.let { mainService?.let {
unbindService(serviceConnection) unbindService(serviceConnection)
} }
rdClipboardManager?.rustEnableServiceClipboard(false)
super.onDestroy() super.onDestroy()
} }
@ -221,6 +220,10 @@ class MainActivity : FlutterActivity() {
result.success(true) result.success(true)
} }
"try_sync_clipboard" -> {
rdClipboardManager?.syncClipboard(true)
result.success(true)
}
GET_START_ON_BOOT_OPT -> { GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
@ -402,13 +405,4 @@ class MainActivity : FlutterActivity() {
super.onStart() super.onStart()
stopService(Intent(this, FloatingWindowService::class.java)) 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)
}
}
} }

View File

@ -433,6 +433,7 @@ class MainService : Service() {
checkMediaPermission() checkMediaPermission()
_isStart = true _isStart = true
FFI.setFrameRawEnable("video",true) FFI.setFrameRawEnable("video",true)
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
return true return true
} }
@ -441,6 +442,7 @@ class MainService : Service() {
Log.d(logTag, "Stop Capture") Log.d(logTag, "Stop Capture")
FFI.setFrameRawEnable("video",false) FFI.setFrameRawEnable("video",false)
_isStart = false _isStart = false
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
// release video // release video
if (reuseVirtualDisplay) { if (reuseVirtualDisplay) {
// The virtual display video projection can be paused by calling `setSurface(null)`. // The virtual display video projection can be paused by calling `setSurface(null)`.

View File

@ -36,19 +36,19 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
// though the `lastUpdatedClipData` will be set to null once. // though the `lastUpdatedClipData` will be set to null once.
private var lastUpdatedClipData: ClipData? = null private var lastUpdatedClipData: ClipData? = null
private var isClientEnabled = true; private var isClientEnabled = true;
private var _isListening = false; private var _isCaptureStarted = false;
val isListening: Boolean
get() = _isListening
fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) { val isCaptureStarted: Boolean
get() = _isCaptureStarted
fun checkPrimaryClip(isClient: Boolean) {
val clipData = clipboardManager.primaryClip val clipData = clipboardManager.primaryClip
if (clipData != null && clipData.itemCount > 0) { if (clipData != null && clipData.itemCount > 0) {
// Only handle the first item in the clipboard for now. // Only handle the first item in the clipboard for now.
val clip = clipData.getItemAt(0) val clip = clipData.getItemAt(0)
val isHostSync = !isClient && isSync // Ignore the `isClipboardDataEqual()` check if it's a host operation.
// Ignore the `isClipboardDataEqual()` check if it's a host sync operation. // Because it's an action manually triggered by the user.
// Because it's a action manually triggered by the user. if (isClient) {
if (!isHostSync) {
if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
Log.d(logTag, "Clipboard data is the same as last update, ignore") Log.d(logTag, "Clipboard data is the same as last update, ignore")
return return
@ -95,13 +95,6 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
} }
} }
private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener {
override fun onPrimaryClipChanged() {
Log.d(logTag, "onPrimaryClipChanged")
checkPrimaryClip(true, false)
}
}
private fun isSupportedMimeType(mimeType: String): Boolean { private fun isSupportedMimeType(mimeType: String): Boolean {
return supportedMimeTypes.contains(mimeType) return supportedMimeTypes.contains(mimeType)
} }
@ -136,43 +129,23 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) {
return true return true
} }
@Keep fun setCaptureStarted(started: Boolean) {
fun rustEnableServiceClipboard(enable: Boolean) { _isCaptureStarted = started
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 @Keep
fun rustEnableClientClipboard(enable: Boolean) { fun rustEnableClientClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableClientClipboard: enable: $enable") Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
isClientEnabled = enable isClientEnabled = enable
if (enable) { lastUpdatedClipData = null
lastUpdatedClipData = clipboardManager.primaryClip
} else {
lastUpdatedClipData = null
}
} }
fun syncClipboard(isClient: Boolean) { fun syncClipboard(isClient: Boolean) {
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening") Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled")
if (isClient && !isClientEnabled) { if (isClient && !isClientEnabled) {
return return
} }
if (!isClient && !_isListening) { checkPrimaryClip(isClient)
return
}
checkPrimaryClip(isClient, true)
} }
@Keep @Keep

View File

@ -147,6 +147,19 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.chatModel.onVoiceCallClosed("End connetion"); gFFI.chatModel.onVoiceCallClosed("End connetion");
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
trySyncClipboard();
}
}
// For client side
// When swithing from other app to this app, try to sync clipboard.
void trySyncClipboard() {
gFFI.invokeMethod("try_sync_clipboard");
}
@override @override
void didChangeMetrics() { void didChangeMetrics() {
// If the soft keyboard is visible and the canvas has been changed(panned or scaled) // If the soft keyboard is visible and the canvas has been changed(panned or scaled)

View File

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

View File

@ -30,7 +30,6 @@ class ServerModel with ChangeNotifier {
bool _inputOk = false; bool _inputOk = false;
bool _audioOk = false; bool _audioOk = false;
bool _fileOk = false; bool _fileOk = false;
bool _clipboardOk = false;
bool _showElevation = false; bool _showElevation = false;
bool hideCm = false; bool hideCm = false;
int _connectStatus = 0; // Rendezvous Server status int _connectStatus = 0; // Rendezvous Server status
@ -60,8 +59,6 @@ class ServerModel with ChangeNotifier {
bool get fileOk => _fileOk; bool get fileOk => _fileOk;
bool get clipboardOk => _clipboardOk;
bool get showElevation => _showElevation; bool get showElevation => _showElevation;
int get connectStatus => _connectStatus; int get connectStatus => _connectStatus;
@ -212,10 +209,6 @@ class ServerModel with ChangeNotifier {
_fileOk = fileOption != 'N'; _fileOk = fileOption != 'N';
} }
// clipboard
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
_clipboardOk = clipOption != 'N';
notifyListeners(); notifyListeners();
} }
@ -322,14 +315,6 @@ class ServerModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
toggleClipboard() async {
_clipboardOk = !_clipboardOk;
bind.mainSetOption(
key: kOptionEnableClipboard,
value: _clipboardOk ? defaultOptionYes : 'N');
notifyListeners();
}
toggleInput() async { toggleInput() async {
if (clients.isNotEmpty) { if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target); await showClientsMayNotBeChangedAlert(parent.target);

View File

@ -371,14 +371,6 @@ pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> {
} }
} }
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<()> { pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> {
_call_clipboard_manager( _call_clipboard_manager(
"rustEnableClientClipboard", "rustEnableClientClipboard",

View File

@ -831,17 +831,6 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
SyncReturn(false) 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) { pub fn main_set_option(key: String, value: String) {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
@ -849,11 +838,6 @@ pub fn main_set_option(key: String, value: String) {
config::keys::OPTION_ENABLE_KEYBOARD, config::keys::OPTION_ENABLE_KEYBOARD,
&value, &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") { if key.eq("custom-rendezvous-server") {
set_option(key, value.clone()); set_option(key, value.clone());

View File

@ -8,8 +8,6 @@ use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data};
use clipboard_master::{CallbackResult, ClipboardHandler}; use clipboard_master::{CallbackResult, ClipboardHandler};
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use hbb_common::config::{keys, option2bool}; use hbb_common::config::{keys, option2bool};
#[cfg(target_os = "android")]
use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard;
use std::{ use std::{
io, io,
sync::mpsc::{channel, RecvTimeoutError, Sender}, sync::mpsc::{channel, RecvTimeoutError, Sender},
@ -224,24 +222,13 @@ impl Handler {
} }
} }
#[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")] #[cfg(target_os = "android")]
fn run(sp: EmptyExtraFieldService) -> ResultType<()> { fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
let _res = call_clipboard_manager_enable_service_clipboard(is_clipboard_enabled());
while sp.ok() { while sp.ok() {
if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { if let Some(msg) = crate::clipboard::get_clipboards_msg(false) {
sp.send(msg); sp.send(msg);
} }
std::thread::sleep(Duration::from_millis(INTERVAL)); std::thread::sleep(Duration::from_millis(INTERVAL));
} }
let _res = call_clipboard_manager_enable_service_clipboard(false);
Ok(()) Ok(())
} }

View File

@ -1372,8 +1372,14 @@ impl Connection {
if !self.follow_remote_window { if !self.follow_remote_window {
noperms.push(NAME_WINDOW_FOCUS); noperms.push(NAME_WINDOW_FOCUS);
} }
if !self.clipboard_enabled() // Do not consider the clipboard and keyboard permissions on Android.
|| !self.peer_keyboard_enabled() // Because syncing the clipboard on Android is manually triggered by the user in the floating ball.
#[cfg(target_os = "android")]
let keyboard_clip_noperm = self.disable_keyboard || self.disable_clipboard;
#[cfg(not(target_os = "android"))]
let keyboard_clip_noperm =
!self.clipboard_enabled() || !self.peer_keyboard_enabled();
if keyboard_clip_noperm
|| crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y"
{ {
noperms.push(super::clipboard_service::NAME); noperms.push(super::clipboard_service::NAME);

View File

@ -312,17 +312,6 @@ 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"))] #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline] #[inline]
pub fn get_clients_state() -> String { pub fn get_clients_state() -> String {