From 96cb8c3d9c2d315a22fbfaac724f6002f85f6803 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 09:41:24 +0800 Subject: [PATCH 1/3] flutter_desktop: fix image scale quanlity Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2ad9dd53b..da5ad1455 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -277,8 +277,7 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); - Provider.of(context, listen: false).tabBarHeight = - super.widget.tabBarHeight; + _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; return WillPopScope( onWillPop: () async { clientClose(); @@ -882,11 +881,11 @@ class ImagePainter extends CustomPainter { if (image == null) return; canvas.scale(scale, scale); // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html var paint = new Paint(); - if (scale > 1.00001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; - } else if (scale < 0.99999) { - paint.filterQuality = FilterQuality.medium; } canvas.drawImage(image!, new Offset(x, y), paint); } From e553756ad824140f770eb3ca8c441d486ae7c5bc Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 17:03:28 +0800 Subject: [PATCH 2/3] flutter_desktop: fix clipboard Signed-off-by: fufesou --- src/flutter.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 6c2e66656..d83e37b4e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, VecDeque}, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex, RwLock, }, }; @@ -31,7 +31,10 @@ use hbb_common::{ Stream, }; -use crate::common::make_fd_to_json; +use crate::common::{ + self, check_clipboard, make_fd_to_json, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; + use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; pub(super) const APP_TYPE_MAIN: &str = "main"; @@ -44,6 +47,9 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } +static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); +static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); + // pub fn get_session<'a>(id: &str) -> Option<&'a Session> { // SESSIONS.read().unwrap().get(id) // } @@ -657,6 +663,43 @@ struct Connection { } impl Connection { + fn start_clipboard( + tx_protobuf: mpsc::UnboundedSender, + lc: Arc>, + ) -> Option> { + let (tx, rx) = std::sync::mpsc::channel(); + match ClipboardContext::new() { + Ok(mut ctx) => { + let old_clipboard: Arc> = Default::default(); + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&old_clipboard)); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit clipboard service of client"); + break; + } + _ => {} + } + if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + || lc.read().unwrap().disable_clipboard + { + continue; + } + if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { + tx_protobuf.send(Data::Message(msg)).ok(); + } + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + Some(tx) + } + /// Create a new connection. /// /// # Arguments @@ -667,6 +710,10 @@ impl Connection { async fn start(session: Session, is_file_transfer: bool) { let mut last_recv_time = Instant::now(); let (sender, mut receiver) = mpsc::unbounded_channel::(); + let mut stop_clipboard = None; + if !is_file_transfer { + stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); + } *session.sender.write().unwrap() = Some(sender); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; @@ -695,6 +742,9 @@ impl Connection { match Client::start(&session.id, &key, &token, conn_type).await { Ok((mut peer, direct)) => { + SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); + session.push_event( "connection_ready", vec![ @@ -774,6 +824,12 @@ impl Connection { session.msgbox("error", "Connection Error", &err.to_string()); } } + + if let Some(stop) = stop_clipboard { + stop.send(()).ok(); + } + SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); } /// Handle message from peer. From b2ffe9dee4a7e8385efe93fc87532f04492e5165 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 22:00:01 +0800 Subject: [PATCH 3/3] flutter_desktop: handle privacy mode back notifications Signed-off-by: fufesou --- flutter/lib/common.dart | 28 +++- flutter/lib/desktop/pages/remote_page.dart | 14 +- flutter/lib/models/model.dart | 17 +++ flutter/lib/utils/multi_window_manager.dart | 1 + src/client.rs | 7 + src/common.rs | 13 ++ src/flutter.rs | 134 +++++++++++++++++++- src/ui/remote.rs | 34 +---- 8 files changed, 210 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ef53b2c41..1a0d59e16 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -194,14 +194,20 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { style: TextStyle(color: MyTheme.accent)))); SmartDialog.dismiss(); - final buttons = [ - wrap(Translator.call('OK'), () { - SmartDialog.dismiss(); - backToHome(); - }) - ]; + List buttons = []; + if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { + buttons.insert( + 0, + wrap(Translator.call('OK'), () { + SmartDialog.dismiss(); + backToHome(); + })); + } if (hasCancel == null) { - hasCancel = type != 'error'; + // hasCancel = type != 'error'; + hasCancel = type.indexOf("error") < 0 && + type.indexOf("nocancel") < 0 && + type != "restarting"; } if (hasCancel) { buttons.insert( @@ -210,6 +216,14 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { SmartDialog.dismiss(); })); } + // TODO: test this button + if (type.indexOf("hasclose") >= 0) { + buttons.insert( + 0, + wrap(Translator.call('Close'), () { + SmartDialog.dismiss(); + })); + } DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index da5ad1455..da7a317a8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -604,8 +604,12 @@ class _RemotePageState extends State await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( - child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') + - 'lock user input')), + child: Consumer( + builder: (_context, ffiModel, _child) => () { + return Text(translate( + (ffiModel.inputBlocked ? 'Unb' : 'B') + + 'lock user input')); + }()), value: 'block-input')); } } @@ -951,7 +955,11 @@ void showOptions(String id) async { more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { - more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); + more.add(Consumer( + builder: (_context, _ffiModel, _child) => () { + return getToggle( + id, setState, 'privacy-mode', 'Privacy mode'); + }())); } } var setQuality = (String? value) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c7295f57e..4f295e377 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -173,6 +173,10 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); } }; } @@ -228,6 +232,10 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); } }; platformFFI.setEventCallback(cb); @@ -331,6 +339,15 @@ class FfiModel with ChangeNotifier { } notifyListeners(); } + + updateBlockInputState(Map evt) { + _inputBlocked = evt['input_state'] == 'on'; + notifyListeners(); + } + + updatePrivacyMode(Map evt) { + notifyListeners(); + } } class ImageModel with ChangeNotifier { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index bea110dab..4da0dca7f 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/services.dart'; diff --git a/src/client.rs b/src/client.rs index 89d66c6ca..3c1e5c3c3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1004,6 +1004,13 @@ impl LoginConfigHandler { Some(msg_out) } + /// Get [`PeerConfig`] of the current [`LoginConfigHandler`]. + /// + /// # Arguments + pub fn get_config(&mut self) -> &mut PeerConfig { + &mut self.config + } + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. /// Return `None` if there's no option, for example, when the session is only for file transfer. /// diff --git a/src/common.rs b/src/common.rs index 605435956..5c387c07e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -104,6 +104,19 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) } } +pub async fn send_opts_after_login( + config: &crate::client::LoginConfigHandler, + peer: &mut hbb_common::tcp::FramedStream, +) { + if let Some(opts) = config.get_option_message_after_login() { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } +} + #[cfg(feature = "use_rubato")] pub fn resample_channels( data: &[f32], diff --git a/src/flutter.rs b/src/flutter.rs index d83e37b4e..bb8881c58 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -76,7 +76,7 @@ impl Session { // TODO close // Self::close(); let events2ui = Arc::new(RwLock::new(events2ui)); - let mut session = Session { + let session = Session { id: session_id.clone(), sender: Default::default(), lc: Default::default(), @@ -663,6 +663,8 @@ struct Connection { } impl Connection { + // TODO: Similar to remote::start_clipboard + // merge the code fn start_clipboard( tx_protobuf: mpsc::UnboundedSender, lc: Arc>, @@ -842,6 +844,7 @@ impl Connection { Some(message::Union::VideoFrame(vf)) => { if !self.first_frame { self.first_frame = true; + common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format { @@ -1083,6 +1086,11 @@ impl Connection { self.session.msgbox("error", "Connection Error", &c); return false; } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } _ => {} }, Some(message::Union::TestDelay(t)) => { @@ -1107,6 +1115,130 @@ impl Connection { true } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.session.push_event( + "update_block_input_state", + [("input_state", if on { "on" } else { "off" })].into(), + ); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.session + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.session + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.session.load_config(); + config.privacy_mode = on; + self.session.save_config(&config); + self.session.lc.write().unwrap().get_config().privacy_mode = on; + self.session.push_event("update_privacy_mode", [].into()); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.session.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.session + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.session + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.session + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5d036dee2..060aa59db 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -25,8 +25,12 @@ use clipboard::{ use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, - fs, log, + config::{Config, LocalConfig, PeerConfig, TransferSerde}, + fs::{ + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + DigestCheckResult, RemoveJobMeta, TransferJobMeta, + }, + get_version_number, log, message_proto::{permission_info::Permission, *}, protobuf::Message as _, rendezvous_proto::ConnType, @@ -38,14 +42,6 @@ use hbb_common::{ }, Stream, }; -use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; -use hbb_common::{ - fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, - }, - get_version_number, -}; #[cfg(windows)] use crate::clipboard_file::*; @@ -2071,22 +2067,6 @@ impl Remote { true } - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -2095,7 +2075,7 @@ impl Remote { self.first_frame = true; self.handler.call2("closeSuccess", &make_args!()); self.handler.call("adaptSize", &make_args!()); - self.send_opts_after_login(peer).await; + common::send_opts_after_login(&self.handler.lc.read().unwrap(), peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format {