From 302499d1e01babe5d7eb147b07407e1bbfd3d4d5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:32:17 +0800 Subject: [PATCH] fix sync displays info && select monitor menu Signed-off-by: fufesou --- .../widgets/material_mod_popup_menu.dart | 2 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 86 ++++++++++--------- flutter/lib/models/model.dart | 24 ++++++ flutter/lib/models/state_model.dart | 1 + libs/hbb_common/protos/message.proto | 1 + src/client/io_loop.rs | 8 ++ src/common.rs | 2 + src/flutter.rs | 33 ++++--- src/server/connection.rs | 84 ++++++++++-------- src/server/video_service.rs | 52 +++++++++-- src/ui/header.tis | 8 ++ src/ui/remote.rs | 34 +++++--- src/ui_session_interface.rs | 1 + 14 files changed, 234 insertions(+), 108 deletions(-) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 47de1be20..3e85cb296 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1400,7 +1400,7 @@ class PopupMenuButtonState extends State> { } return MenuButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), + child: widget.icon ?? Icon(Icons.adaptive.more), tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 7c9fe67eb..96cc9fa9b 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -5,7 +5,7 @@ class MenuButton extends StatefulWidget { final Color color; final Color hoverColor; final Color? splashColor; - final Widget icon; + final Widget child; final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; @@ -14,7 +14,7 @@ class MenuButton extends StatefulWidget { required this.onPressed, required this.color, required this.hoverColor, - required this.icon, + required this.child, this.splashColor, this.tooltip = "", this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), @@ -51,7 +51,7 @@ class _MenuButtonState extends State { splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, - child: widget.icon, + child: widget.child, ), ), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..c97ef9d32 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,7 +472,7 @@ class _RemoteMenubarState extends State { onPressed: () { widget.state.switchPin(); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", color: Colors.white, ), @@ -488,7 +488,7 @@ class _RemoteMenubarState extends State { onPressed: () { _setFullscreen(!isFullscreen); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", color: Colors.white, ), @@ -499,7 +499,7 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return mod_menu.PopupMenuButton( + final monitor = mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -524,43 +524,44 @@ class _RemoteMenubarState extends State { itemBuilder: (BuildContext context) { final List rowChildren = []; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - TextButton( - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - ), + rowChildren.add(MenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + child: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, ), ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - ) - ], + ) + ], + ), ), - ); + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + _menuDismissCallback(); + } + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + } + }, + )); } return >[ mod_menu.PopupMenuItem( @@ -576,6 +577,11 @@ class _RemoteMenubarState extends State { ]; }, ); + + return Obx(() => Offstage( + offstage: stateGlobal.displaysCount.value < 2, + child: monitor, + )); } Widget _buildControl(BuildContext context) { @@ -674,7 +680,7 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/rec.svg", color: Colors.white, ), @@ -697,7 +703,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/close.svg", color: Colors.white, ), @@ -767,7 +773,7 @@ class _RemoteMenubarState extends State { return tooltipText == null ? const Offstage() : MenuButton( - icon: _getVoiceCallIcon(), + child: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 458ca29f4..1afb5b147 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -140,6 +140,8 @@ class FfiModel with ChangeNotifier { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); + } else if (name == 'sync_peer_info') { + handleSyncPeerInfo(evt, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); @@ -415,6 +417,7 @@ class FfiModel with ChangeNotifier { d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } + stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; } @@ -431,6 +434,27 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the peer info synchronization event based on [evt]. + handleSyncPeerInfo(Map evt, String peerId) async { + if (evt['displays'] != null) { + List displays = json.decode(evt['displays']); + List newDisplays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + d.cursorEmbedded = d0['cursor_embedded'] == 1; + newDisplays.add(d); + } + _pi.displays = newDisplays; + stateGlobal.displaysCount.value = _pi.displays.length; + } + notifyListeners(); + } + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e4c9fa03f..761c95ded 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,6 +14,7 @@ class StateGlobal { final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; + final RxInt displaysCount = 0.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 7e3d0b0a4..2a3fd05b4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -636,5 +636,6 @@ message Message { SwitchSidesResponse switch_sides_response = 22; VoiceCallRequest voice_call_request = 23; VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c673531ec..b51c481a5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1253,6 +1253,14 @@ impl Remote { } } } + Some(message::Union::PeerInfo(pi)) => { + match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); + } + _ => {} + } + } _ => {} } } diff --git a/src/common.rs b/src/common.rs index ee44cf4f2..02d367b5e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -37,6 +37,8 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } } impl InvokeUiSession for FlutterHandler { @@ -316,17 +330,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_peer_info(&self, pi: &PeerInfo) { - let mut displays = Vec::new(); - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); - displays.push(h); - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + let displays = Self::make_displays_msg(&pi.displays); let mut features: HashMap<&str, i32> = Default::default(); for ref f in pi.features.iter() { features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); @@ -351,6 +355,13 @@ impl InvokeUiSession for FlutterHandler { ); } + fn set_displays(&self, displays: &Vec) { + self.push_event( + "sync_peer_info", + vec![("displays", &Self::make_displays_msg(displays))], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 53ccd7008..1a974c51d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -6,7 +6,10 @@ use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ - client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, LatencyController, + MediaData, MediaSender, + }, common::{get_default_sound_input, set_sound_input}, video_service, }; @@ -672,15 +675,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -806,7 +809,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -833,7 +836,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -877,7 +880,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -893,10 +896,11 @@ impl Connection { res.set_error(format!("{}", err)); } Ok((current, displays)) => { - pi.displays = displays.into(); + pi.displays = displays.clone(); pi.current_display = current as _; res.set_peer_info(pi); sub_service = true; + *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } } @@ -1160,7 +1164,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1324,12 +1328,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); } + } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1512,15 +1516,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), } } + } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1530,8 +1534,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1549,8 +1553,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1571,7 +1575,11 @@ impl Connection { // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self + .audio_sender + .as_ref() + .unwrap() + .send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1583,7 +1591,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1596,7 +1604,9 @@ impl Connection { if let Some(sender) = &self.audio_sender { allow_err!(sender.send(MediaData::AudioFrame(frame))); } else { - log::warn!("Processing audio frame without the voice call audio sender."); + log::warn!( + "Processing audio frame without the voice call audio sender." + ); } } } @@ -1646,7 +1656,9 @@ impl Connection { pub async fn close_voice_call(&mut self) { // Restore to the prior audio device. - if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + if let Some(sound_input) = + std::mem::replace(&mut self.audio_input_device_before_voice_call, None) + { set_sound_input(sound_input); } // Notify the connection manager that the voice call has been closed. @@ -1821,13 +1833,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index bc9c5ff6f..52b1717c4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -65,6 +65,7 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); } fn is_capturer_mag_supported() -> bool { @@ -407,6 +408,43 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType Option> { + let displays = try_get_displays().ok()?; + let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); + + if displays.len() != last_sync_displays.len() { + Some(displays) + } else { + for i in 0..displays.len() { + if displays[i].height() != (last_sync_displays[i].height as usize) { + return Some(displays); + } + if displays[i].width() != (last_sync_displays[i].width as usize) { + return Some(displays); + } + if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) { + return Some(displays); + } + } + None + } +} + +fn check_displays_changed() -> Option { + let displays = check_displays_new()?; + let (current, displays) = get_displays_2(&displays); + let mut pi = PeerInfo { + conn_id: crate::SYNC_PEER_INFO_DISPLAYS, + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = current as _; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *LAST_SYNC_DISPLAYS.write().unwrap() = displays; + Some(msg_out) +} + fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; @@ -529,6 +567,11 @@ fn run(sp: GenericService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + + if let Some(msg_out) = check_displays_changed() { + sp.send(msg_out); + } + if c.ndisplay != get_display_num() { log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; @@ -798,11 +841,7 @@ fn get_display_num() -> usize { } } - if let Ok(d) = try_get_displays() { - d.len() - } else { - 0 - } + LAST_SYNC_DISPLAYS.read().unwrap().len() } pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { @@ -861,6 +900,7 @@ pub async fn switch_display(i: i32) { } } +#[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); @@ -888,10 +928,12 @@ fn get_primary() -> usize { 0 } +#[inline] pub async fn switch_to_primary() { switch_display(get_primary() as _).await; } +#[inline] #[cfg(not(windows))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) diff --git a/src/ui/header.tis b/src/ui/header.tis index 1fb694397..e25c0d544 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -480,6 +480,14 @@ handler.updatePi = function(v) { } } +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a86f07d0f..4794efb65 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -53,6 +53,20 @@ impl SciterHandler { allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); } } + + fn make_displays_array(displays: &Vec) -> Value { + let mut displays_value = Value::array(0); + for d in displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + display.set_item("cursor_embedded", d.cursor_embedded); + displays_value.push(display); + } + displays_value + } } impl InvokeUiSession for SciterHandler { @@ -215,22 +229,18 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("hostname", pi.hostname.clone()); pi_sciter.set_item("platform", pi.platform.clone()); pi_sciter.set_item("sas_enabled", pi.sas_enabled); - - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - display.set_item("cursor_embedded", d.cursor_embedded); - displays.push(display); - } - pi_sciter.set_item("displays", displays); + pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); self.call("updatePi", &make_args!(pi_sciter)); } + fn set_displays(&self, displays: &Vec) { + self.call( + "updateDisplays", + &make_args!(Self::make_displays_array(displays)), + ); + } + fn on_connected(&self, conn_type: ConnType) { match conn_type { ConnType::RDP => {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b225151ff..5a83ee572 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -761,6 +761,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn set_displays(&self, displays: &Vec); fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool);