Ctrl K
ComunidadGuías…

src/flutter_ffi.rs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::{
    keyboard::input_source::{change_input_source, get_cur_session_input_source},
    trusted_devices::authority,
};
#[cfg(target_os = "linux")]
use crate::platform::linux::is_x11;
use crate::{
    client::file_trait::FileManager,
    common::{make_fd_to_json, make_vec_fd_to_json},
    flutter::{
        self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option,
    },
    input::*,
    ui_interface::{self, *},
};
use flutter_rust_bridge::{StreamSink, SyncReturn};
use hbb_common::{
    config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
    lazy_static, log,
    rendezvous_proto::ConnType,
    ResultType,
};
use base::{
    config::keys,
    fs,
};
use std::{
    collections::HashMap,
    sync::{
        atomic::{AtomicI32, Ordering},
        Arc,
    },
    time::SystemTime,
};

pub type SessionID = uuid::Uuid;

lazy_static::lazy_static! {
    static ref TEXTURE_RENDER_KEY: Arc<AtomicI32> = Arc::new(AtomicI32::new(0));
}

fn initialize(app_dir: &str, custom_client_config: &str) {
    flutter::async_tasks::start_flutter_async_runner();
    // `APP_DIR` is set in `main_get_data_dir_ios()` on iOS.
    #[cfg(not(target_os = "ios"))]
    {
        *config::APP_DIR.write().unwrap() = app_dir.to_owned();
    }
    // core_main's load_custom_client does not work for flutter since it is only applied to its load_library in main.c
    if custom_client_config.is_empty() {
        crate::load_custom_client();
    } else {
        crate::read_custom_client(custom_client_config);
    }
    #[cfg(target_os = "android")]
    {
        // flexi_logger can't work when android_logger initialized.
        #[cfg(debug_assertions)]
        android_logger::init_once(
            android_logger::Config::default()
                .with_max_level(log::LevelFilter::Debug) // limit log level
                .with_tag("ffi"), // logs will show under mytag tag
        );
        #[cfg(not(debug_assertions))]
        crate::platform::android_log::initialize();
        #[cfg(feature = "mediacodec")]
        scrap::mediacodec::check_mediacodec();
        crate::common::test_rendezvous_server();
        crate::common::test_nat_type();
    }
    #[cfg(target_os = "ios")]
    {
        use hbb_common::env_logger::*;
        init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug"));
        crate::common::test_nat_type();
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        let _ = crate::common::global_init();
    }
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        // core_main's init_log does not work for flutter since it is only applied to its load_library in main.c
        hbb_common::init_log(false, "flutter_ffi");
    }
}

#[inline]
pub fn start_global_event_stream(s: StreamSink<String>, app_type: String) -> ResultType<()> {
    super::flutter::start_global_event_stream(s, app_type)
}

#[inline]
pub fn stop_global_event_stream(app_type: String) {
    super::flutter::stop_global_event_stream(app_type)
}
pub enum EventToUI {
    Event(String),
    Rgba(usize),
    Texture(usize, bool), // (display, gpu_texture)
    // A shape's RGBA as bytes: as text it was four times the size and parsed on the UI thread.
    // The id stays text, since the peer's ids can exceed Dart's int.
    Cursor {
        id: String,
        hotx: i32,
        hoty: i32,
        width: i32,
        height: i32,
        colors: Vec<u8>,
    },
}

pub fn host_stop_system_key_propagate(_stopped: bool) {
    #[cfg(windows)]
    crate::platform::windows::stop_system_key_propagate(_stopped);
}

// This function is only used to count the number of control sessions.
pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn<usize> {
    let conn_type = if conn_type == ConnType::VIEW_CAMERA as i32 {
        ConnType::VIEW_CAMERA
    } else if conn_type == ConnType::FILE_TRANSFER as i32 {
        ConnType::FILE_TRANSFER
    } else if conn_type == ConnType::PORT_FORWARD as i32 {
        ConnType::PORT_FORWARD
    } else if conn_type == ConnType::RDP as i32 {
        ConnType::RDP
    } else if conn_type == ConnType::TERMINAL as i32 {
        ConnType::TERMINAL
    } else {
        ConnType::DEFAULT_CONN
    };
    SyncReturn(sessions::get_session_count(id, conn_type))
}

pub fn session_add_existed_sync(
    id: String,
    session_id: SessionID,
    displays: Vec<i32>,
    is_view_camera: bool,
) -> SyncReturn<String> {
    if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) {
        SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
    } else {
        SyncReturn("".to_owned())
    }
}

pub fn session_add_sync(
    session_id: SessionID,
    id: String,
    is_file_transfer: bool,
    is_view_camera: bool,
    is_port_forward: bool,
    is_rdp: bool,
    is_terminal: bool,
    switch_uuid: String,
    force_relay: bool,
    password: String,
    is_shared_password: bool,
    conn_token: Option<String>,
    login_method: String,
) -> SyncReturn<String> {
    let add_res = session_add(
        &session_id,
        &id,
        is_file_transfer,
        is_view_camera,
        is_port_forward,
        is_rdp,
        is_terminal,
        &switch_uuid,
        force_relay,
        password,
        is_shared_password,
        conn_token,
        &login_method,
    );
    // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`.
    // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last.
    if is_terminal {
        std::env::remove_var("IS_TERMINAL_ADMIN");
    }

    if let Err(e) = add_res {
        SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
    } else {
        SyncReturn("".to_owned())
    }
}

pub fn session_start(
    events2ui: StreamSink<EventToUI>,
    session_id: SessionID,
    id: String,
) -> ResultType<()> {
    session_start_(&session_id, &id, events2ui)
}

pub fn session_start_with_displays(
    events2ui: StreamSink<EventToUI>,
    session_id: SessionID,
    id: String,
    displays: Vec<i32>,
) -> ResultType<()> {
    session_start_(&session_id, &id, events2ui)?;

    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.capture_displays(displays.clone(), vec![], vec![]);
        for display in displays {
            session.refresh_video(display as _);
        }
    }
    Ok(())
}

// A cursor shape the core keeps, as RGBA, for a window that has to draw it again.
pub struct CursorShape {
    pub hotx: i32,
    pub hoty: i32,
    pub width: i32,
    pub height: i32,
    pub colors: Vec<u8>,
}

pub fn session_get_cursor_shape(session_id: SessionID, id: String) -> Option<CursorShape> {
    let id = id.parse::<u64>().ok()?;
    let session = sessions::get_session_by_session_id(&session_id)?;
    let shape = session.cursor_shapes.read().unwrap().get(&id).cloned()?;
    match crate::client::io_loop::kept_cursor_rgba(shape) {
        Ok(cd) => Some(CursorShape {
            hotx: cd.hotx,
            hoty: cd.hoty,
            width: cd.width,
            height: cd.height,
            colors: cd.colors.to_vec(),
        }),
        Err(err) => {
            log::warn!("Kept cursor {id} failed to decode: {err}");
            None
        }
    }
}

pub fn session_get_remember(session_id: SessionID) -> Option<bool> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_remember())
    } else {
        None
    }
}

pub fn session_get_toggle_option(session_id: SessionID, arg: String) -> Option<bool> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_toggle_option(arg))
    } else {
        None
    }
}

pub fn session_get_toggle_option_sync(session_id: SessionID, arg: String) -> SyncReturn<bool> {
    let res = session_get_toggle_option(session_id, arg) == Some(true);
    SyncReturn(res)
}

pub fn session_get_option(session_id: SessionID, arg: String) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_option(arg))
    } else {
        None
    }
}

pub fn session_login(
    session_id: SessionID,
    os_username: String,
    os_password: String,
    password: String,
    remember: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.login(os_username, os_password, password, remember);
    }
}

pub fn session_send2fa(session_id: SessionID, code: String, trust_this_device: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send2fa(code, trust_this_device);
    }
}

pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn<bool> {
    let v = if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.get_enable_trusted_devices()
    } else {
        false
    };
    SyncReturn(v)
}

pub fn will_session_close_close_session(session_id: SessionID) -> SyncReturn<bool> {
    SyncReturn(sessions::would_remove_peer_by_session_id(&session_id))
}

pub fn session_close(session_id: SessionID) {
    if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
        crate::keyboard::shortcuts::clear_session_state(&session_id);
        // `release_remote_keys` is not required for mobile platforms in common cases.
        // But we still call it to make the code more stable.
        #[cfg(any(target_os = "android", target_os = "ios"))]
        crate::keyboard::release_remote_keys("map");
        session.close_event_stream(session_id);
        session.close();
    }
}

pub fn session_refresh(session_id: SessionID, display: usize) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.refresh_video(display as _);
    }
}

pub fn session_take_screenshot(session_id: SessionID, display: usize) {
    if let Some(s) = sessions::get_session_by_session_id(&session_id) {
        s.take_screenshot(display as _, session_id.to_string());
    }
}

pub fn session_handle_screenshot(
    #[allow(unused_variables)] session_id: SessionID,
    action: String,
) -> String {
    crate::client::screenshot::handle_screenshot(action)
}

pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn<bool> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.is_multi_ui_session())
    } else {
        SyncReturn(false)
    }
}

pub fn session_record_screen(session_id: SessionID, start: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.record_screen(start);
    }
}

pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn<bool> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.is_recording())
    } else {
        SyncReturn(false)
    }
}

pub fn session_reconnect(session_id: SessionID, force_relay: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.reconnect(force_relay);
    }
    session_on_waiting_for_image_dialog_show(session_id);
}

pub fn session_toggle_option(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        log::warn!("toggle option {}", &value);
        session.toggle_option(value.clone());
        if value == "view-only" && session.get_toggle_option(value.clone()) {
            crate::keyboard::shortcuts::enter_view_only(&session_id);
        }
        try_sync_peer_option(&session, &session_id, &value, None);
    }
    #[cfg(not(target_os = "ios"))]
    if sessions::get_session_by_session_id(&session_id).is_some()
        && (value == "disable-clipboard" || value == "view-only")
    {
        crate::flutter::update_text_clipboard_required();
    }
    #[cfg(feature = "unix-file-copy-paste")]
    if sessions::get_session_by_session_id(&session_id).is_some()
        && (value == keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only")
    {
        crate::flutter::update_file_clipboard_required();
    }
}

pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.toggle_privacy_mode(impl_key, on);
    }
}

pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_flutter_option(k))
    } else {
        None
    }
}

pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_flutter_option(k, v);
    }
}

pub fn get_next_texture_key() -> SyncReturn<i32> {
    let k = TEXTURE_RENDER_KEY.fetch_add(1, Ordering::SeqCst) + 1;
    SyncReturn(k)
}

pub fn get_local_flutter_option(k: String) -> SyncReturn<String> {
    SyncReturn(ui_interface::get_local_flutter_option(k))
}

pub fn set_local_flutter_option(k: String, v: String) {
    ui_interface::set_local_flutter_option(k, v);
}

pub fn get_local_kb_layout_type() -> SyncReturn<String> {
    SyncReturn(ui_interface::get_kb_layout_type())
}

pub fn set_local_kb_layout_type(kb_layout_type: String) {
    ui_interface::set_kb_layout_type(kb_layout_type)
}

pub fn session_get_view_style(session_id: SessionID) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_view_style())
    } else {
        None
    }
}

pub fn session_set_view_style(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_view_style(value);
    }
}

pub fn session_get_scroll_style(session_id: SessionID) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_scroll_style())
    } else {
        None
    }
}

pub fn session_set_scroll_style(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_scroll_style(value);
    }
}

pub fn session_get_edge_scroll_edge_thickness(session_id: SessionID) -> Option<i32> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_edge_scroll_edge_thickness())
    } else {
        None
    }
}

pub fn session_set_edge_scroll_edge_thickness(session_id: SessionID, value: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_edge_scroll_edge_thickness(value);
    }
}

pub fn session_get_image_quality(session_id: SessionID) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_image_quality())
    } else {
        None
    }
}

pub fn session_set_image_quality(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_image_quality(value);
    }
}

pub fn session_get_keyboard_mode(session_id: SessionID) -> Option<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_keyboard_mode())
    } else {
        None
    }
}

pub fn session_set_keyboard_mode(session_id: SessionID, value: String) {
    let mut _mode_updated = false;
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_keyboard_mode(value.clone());
        _mode_updated = true;
        try_sync_peer_option(&session, &session_id, "keyboard_mode", None);
    }
    #[cfg(windows)]
    if _mode_updated {
        crate::keyboard::update_grab_get_key_name(&value);
    }
}

pub fn session_get_reverse_mouse_wheel_sync(session_id: SessionID) -> SyncReturn<Option<String>> {
    let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_reverse_mouse_wheel())
    } else {
        None
    };
    SyncReturn(res)
}

pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_reverse_mouse_wheel(value);
    }
}

pub fn session_get_displays_as_individual_windows(
    session_id: SessionID,
) -> SyncReturn<Option<String>> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(Some(session.get_displays_as_individual_windows()))
    } else {
        SyncReturn(None)
    }
}

pub fn session_set_displays_as_individual_windows(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_displays_as_individual_windows(value);
    }
}

pub fn session_get_use_all_my_displays_for_the_remote_session(
    session_id: SessionID,
) -> SyncReturn<Option<String>> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(Some(
            session.get_use_all_my_displays_for_the_remote_session(),
        ))
    } else {
        SyncReturn(None)
    }
}

pub fn session_set_use_all_my_displays_for_the_remote_session(
    session_id: SessionID,
    value: String,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_use_all_my_displays_for_the_remote_session(value);
    }
}

pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32>> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_custom_image_quality())
    } else {
        None
    }
}

pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.is_keyboard_mode_supported(mode))
    } else {
        SyncReturn(false)
    }
}

pub fn session_set_custom_image_quality(session_id: SessionID, value: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_custom_image_quality(value);
    }
}

pub fn session_set_custom_fps(session_id: SessionID, fps: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.set_custom_fps(fps);
    }
}

pub fn session_get_trackpad_speed(session_id: SessionID) -> Option<i32> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        Some(session.get_trackpad_speed())
    } else {
        None
    }
}

pub fn session_set_trackpad_speed(session_id: SessionID, value: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.save_trackpad_speed(value);
    }
}

pub fn session_lock_screen(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.lock_screen();
    }
}

pub fn session_ctrl_alt_del(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.ctrl_alt_del();
    }
}

pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Vec<i32>) {
    sessions::session_switch_display(is_desktop, session_id, value);
}

pub fn session_handle_flutter_key_event(
    session_id: SessionID,
    character: String,
    usb_hid: i32,
    lock_modes: i32,
    down_or_up: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        let keyboard_mode = session.get_keyboard_mode();
        session.handle_flutter_key_event(
            session_id,
            &keyboard_mode,
            &character,
            usb_hid,
            lock_modes,
            down_or_up,
        );
    }
}

pub fn session_handle_flutter_raw_key_event(
    session_id: SessionID,
    name: String,
    platform_code: i32,
    position_code: i32,
    lock_modes: i32,
    down_or_up: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        let keyboard_mode = session.get_keyboard_mode();
        session.handle_flutter_raw_key_event(
            session_id,
            &keyboard_mode,
            &name,
            platform_code,
            position_code,
            lock_modes,
            down_or_up,
        );
    }
}

// If the cursor jumps between remote page of two connections, leave view and enter view will be called.
// session_enter_or_leave() will be called then.
// As Rust is multi-threaded, enter() can be called before leave().
// The Rust-side grab ownership state filters stale transitions.
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
        let keyboard_mode = session.get_keyboard_mode();
        // Use the full per-window UUID (not lc.session_id which is per-connection)
        // so that two windows viewing the same peer get distinct grab owners.
        let window_id = _session_id.as_u128();
        if _enter {
            set_cur_session_id_(_session_id, &keyboard_mode);
            crate::keyboard::client::change_grab_status(
                crate::common::GrabState::Run,
                &keyboard_mode,
                window_id,
            );
        } else {
            crate::keyboard::client::change_grab_status(
                crate::common::GrabState::Wait,
                &keyboard_mode,
                window_id,
            );
        }
    }
    SyncReturn(())
}

pub fn session_input_key(
    session_id: SessionID,
    name: String,
    down: bool,
    press: bool,
    alt: bool,
    ctrl: bool,
    shift: bool,
    command: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        // #[cfg(any(target_os = "android", target_os = "ios"))]
        session.input_key(&name, down, press, alt, ctrl, shift, command);
    }
}

pub fn session_input_string(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        // #[cfg(any(target_os = "android", target_os = "ios"))]
        session.input_string(&value);
    }
}

// chat_client_mode
pub fn session_send_chat(session_id: SessionID, text: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send_chat(text);
    }
}

// Terminal functions
pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.open_terminal(terminal_id, rows, cols);
    } else {
        log::error!(
            "[flutter_ffi] Session not found for session_id: {}",
            session_id
        );
    }
}

pub fn session_send_terminal_input(session_id: SessionID, terminal_id: i32, data: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send_terminal_input(terminal_id, data);
    }
}

pub fn session_resize_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.resize_terminal(terminal_id, rows, cols);
    }
}

pub fn session_close_terminal(session_id: SessionID, terminal_id: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.close_terminal(terminal_id);
    }
}

pub fn session_peer_option(session_id: SessionID, name: String, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.set_option(name, value);
    }
}

pub fn session_get_peer_option(session_id: SessionID, name: String) -> String {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        return session.get_option(name);
    }
    "".to_string()
}

pub fn session_input_os_password(session_id: SessionID, value: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.input_os_password(value, true);
    }
}

// File Action
pub fn session_read_remote_dir(session_id: SessionID, path: String, include_hidden: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.read_remote_dir(path, include_hidden);
    }
}

pub fn session_send_files(
    session_id: SessionID,
    act_id: i32,
    path: String,
    to: String,
    file_num: i32,
    include_hidden: bool,
    is_remote: bool,
    _is_dir: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send_files(
            act_id,
            fs::JobType::Generic.into(),
            path,
            to,
            file_num,
            include_hidden,
            is_remote,
        );
    }
}

pub fn session_set_confirm_override_file(
    session_id: SessionID,
    act_id: i32,
    file_num: i32,
    need_override: bool,
    remember: bool,
    is_upload: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload);
    }
}

pub fn session_remove_file(
    session_id: SessionID,
    act_id: i32,
    path: String,
    file_num: i32,
    is_remote: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.remove_file(act_id, path, file_num, is_remote);
    }
}

pub fn session_read_dir_to_remove_recursive(
    session_id: SessionID,
    act_id: i32,
    path: String,
    is_remote: bool,
    show_hidden: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.remove_dir_all(act_id, path, is_remote, show_hidden);
    }
}

pub fn session_remove_all_empty_dirs(
    session_id: SessionID,
    act_id: i32,
    path: String,
    is_remote: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.remove_dir(act_id, path, is_remote);
    }
}

pub fn session_cancel_job(session_id: SessionID, act_id: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.cancel_job(act_id);
    }
}

pub fn session_create_dir(session_id: SessionID, act_id: i32, path: String, is_remote: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.create_dir(act_id, path, is_remote);
    }
}

pub fn session_read_local_dir_sync(
    _session_id: SessionID,
    path: String,
    show_hidden: bool,
) -> String {
    #[cfg(target_os = "android")]
    if fs::android::is_path(&fs::get_path(&path)) {
        return match fs::read_dir(&fs::get_path(&path), show_hidden) {
            Ok(fd) => make_fd_to_json(fd.id, path, &fd.entries),
            Err(error) => serde_json::json!({"error": error.to_string()}).to_string(),
        };
    }
    if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
        return make_fd_to_json(fd.id, path, &fd.entries);
    }
    "".to_string()
}

pub fn session_read_local_empty_dirs_recursive_sync(
    _session_id: SessionID,
    path: String,
    include_hidden: bool,
) -> String {
    if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) {
        return make_vec_fd_to_json(&fds);
    }
    "".to_string()
}

pub fn session_read_remote_empty_dirs_recursive_sync(
    session_id: SessionID,
    path: String,
    include_hidden: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.read_empty_dirs(path, include_hidden);
    }
}

pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        return session.get_platform(is_remote);
    }
    "".to_string()
}

pub fn session_load_last_transfer_jobs(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        return session.load_last_jobs();
    } else {
        // a tip for flutter dev
        eprintln!(
            "cannot load last transfer job from non-existed session. Please ensure session \
        is connected before calling load last transfer jobs."
        );
    }
}

pub fn session_add_job(
    session_id: SessionID,
    act_id: i32,
    path: String,
    to: String,
    file_num: i32,
    include_hidden: bool,
    is_remote: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.add_job(
            act_id,
            fs::JobType::Generic.into(),
            path,
            to,
            file_num,
            include_hidden,
            is_remote,
        );
    }
}

pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.resume_job(act_id, is_remote);
    }
}

pub fn session_rename_file(
    session_id: SessionID,
    act_id: i32,
    path: String,
    new_name: String,
    is_remote: bool,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.rename_file(act_id, path, new_name, is_remote);
    }
}

pub fn session_elevate_direct(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.elevate_direct();
    }
}

pub fn session_elevate_with_logon(session_id: SessionID, username: String, password: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.elevate_with_logon(username, password);
    }
}

pub fn session_switch_sides(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.switch_sides();
    }
}

pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32, height: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.change_resolution(display, width, height);
    }
}

pub fn session_set_size(session_id: SessionID, display: usize, width: usize, height: usize) {
    super::flutter::session_set_size(session_id, display, width, height)
}

pub fn session_send_selected_session_id(session_id: SessionID, sid: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send_selected_session_id(sid);
    }
}

pub fn main_get_sound_inputs() -> Vec<String> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    return get_sound_inputs();
    #[cfg(any(target_os = "android", target_os = "ios"))]
    vec![String::from("")]
}

pub fn main_get_login_device_info() -> SyncReturn<String> {
    SyncReturn(get_login_device_info_json())
}

pub fn main_change_id(new_id: String) {
    change_id(new_id)
}

pub fn main_get_async_status() -> String {
    get_async_job_status()
}

pub fn main_get_http_status(url: String) -> Option<String> {
    get_async_http_status(url)
}

pub fn main_get_option(key: String) -> String {
    get_option(key)
}

pub fn main_get_option_sync(key: String) -> SyncReturn<String> {
    SyncReturn(get_option(key))
}

pub fn main_get_error() -> String {
    get_error()
}

pub fn main_set_option(key: String, value: String) -> hbb_common::anyhow::Result<()> {
    if key == "2fa"
        || key == keys::OPTION_ENABLE_TRUSTED_DEVICES
        || key == keys::OPTION_TRUSTED_DEVICE_EXPIRATION
    {
        return set_auth_option(key, value);
    }
    #[cfg(target_os = "android")]
    {
        let is_permission_option = key.eq(keys::OPTION_ENABLE_CLIPBOARD)
            || key.eq(keys::OPTION_ENABLE_FILE_TRANSFER)
            || key.eq(keys::OPTION_ENABLE_AUDIO);
        let allow_perm_change_in_accept_window = config::option2bool(
            keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
            &crate::get_builtin_option(keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
        );
        if is_permission_option
            && !allow_perm_change_in_accept_window
            && crate::ui_cm_interface::has_active_clients()
        {
            log::info!(
                "blocked main_set_option by policy, key={}, value={}",
                key,
                value
            );
            return Ok(());
        }
    }
    #[cfg(target_os = "android")]
    if key.eq(keys::OPTION_ENABLE_KEYBOARD) {
        crate::ui_cm_interface::switch_permission_all(
            "keyboard".to_owned(),
            config::option2bool(&key, &value),
        );
    }
    #[cfg(target_os = "android")]
    if key.eq(keys::OPTION_ENABLE_CLIPBOARD) {
        crate::ui_cm_interface::switch_permission_all(
            "clipboard".to_owned(),
            config::option2bool(&key, &value),
        );
    }

    // If `is_allow_tls_fallback` and https proxy is used, we need to restart rendezvous mediator.
    // No need to check if https proxy is used, because this option does not change frequently
    // and restarting mediator is safe even https proxy is not used.
    let is_allow_tls_fallback = key.eq(keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK);
    if is_allow_tls_fallback
        || key.eq("custom-rendezvous-server")
        || key.eq(keys::OPTION_ALLOW_WEBSOCKET)
        || key.eq(keys::OPTION_DISABLE_UDP)
        || key.eq("api-server")
    {
        if is_allow_tls_fallback {
            hbb_common::tls::reset_tls_cache();
        }
        set_option(key, value.clone());
        #[cfg(target_os = "android")]
        crate::rendezvous_mediator::RendezvousMediator::restart();
        #[cfg(any(target_os = "android", target_os = "ios"))]
        crate::common::test_rendezvous_server();
    } else {
        set_option(key, value.clone());
    }
    Ok(())
}

pub fn main_get_options() -> String {
    get_options()
}

pub fn main_get_options_sync() -> SyncReturn<String> {
    SyncReturn(get_options())
}

pub fn main_set_options(json: String) -> hbb_common::anyhow::Result<()> {
    let mut map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
    #[cfg(target_os = "android")]
    {
        let allow_perm_change_in_accept_window = config::option2bool(
            keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
            &crate::get_builtin_option(keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
        );
        if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() {
            for key in [
                keys::OPTION_ENABLE_CLIPBOARD,
                keys::OPTION_ENABLE_FILE_TRANSFER,
                keys::OPTION_ENABLE_AUDIO,
            ] {
                if let Some(value) = map.remove(key) {
                    log::info!(
                        "blocked main_set_options item by policy, key={}, value={}",
                        key,
                        value
                    );
                }
            }
        }
    }
    if !map.is_empty() {
        try_set_options(map)?;
    }
    Ok(())
}

pub fn main_test_if_valid_server(server: String, test_with_proxy: bool) -> String {
    test_if_valid_server(server, test_with_proxy)
}

pub fn main_set_socks(proxy: String, username: String, password: String) {
    set_socks(proxy, username, password)
}

pub fn main_get_proxy_status() -> bool {
    get_proxy_status()
}

pub fn main_get_socks() -> Vec<String> {
    get_socks()
}

pub fn main_get_app_name() -> String {
    get_app_name()
}

pub fn main_get_app_name_sync() -> SyncReturn<String> {
    SyncReturn(get_app_name())
}

pub fn main_uri_prefix_sync() -> SyncReturn<String> {
    SyncReturn(crate::get_uri_prefix())
}

pub fn main_get_license() -> String {
    get_license()
}

pub fn main_get_version() -> String {
    get_version()
}

pub fn main_get_fav() -> Vec<String> {
    get_fav()
}

pub fn main_store_fav(favs: Vec<String>) {
    store_fav(favs)
}

pub fn main_get_peer_sync(id: String) -> SyncReturn<String> {
    let conf = get_peer(id);
    SyncReturn(serde_json::to_string(&conf).unwrap_or("".to_string()))
}

pub fn main_get_lan_peers() -> String {
    serde_json::to_string(&get_lan_peers()).unwrap_or_default()
}

pub fn main_get_connect_status() -> String {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        serde_json::to_string(&get_connect_status()).unwrap_or("".to_string())
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        let mut state = hbb_common::config::get_online_state();
        if state > 0 {
            state = 1;
        }
        serde_json::json!({ "status_num": state }).to_string()
    }
}

pub fn main_check_connect_status() {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    start_option_status_sync(); // avoid multi calls
}

pub fn main_is_using_public_server() -> bool {
    crate::using_public_server()
}

pub fn main_discover() {
    discover();
}

pub fn main_get_api_server() -> String {
    get_api_server()
}

pub fn main_deploy_device(token: String, id: String) -> String {
    #[cfg(target_os = "android")]
    {
        let new_id = match id.trim() {
            "" => None,
            id => Some(id.to_owned()),
        };
        ui_interface::deploy_device(token, new_id).message()
    }
    #[cfg(not(target_os = "android"))]
    {
        let _ = (token, id);
        "Deployment is not supported on this platform.".to_owned()
    }
}

pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
    SyncReturn(resolve_avatar_url(avatar))
}

pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
    http_request(url, method, body, header)
}

pub fn main_get_local_option(key: String) -> SyncReturn<String> {
    SyncReturn(get_local_option(key))
}

pub fn main_get_use_texture_render() -> SyncReturn<bool> {
    SyncReturn(use_texture_render())
}

pub fn main_get_env(key: String) -> SyncReturn<String> {
    SyncReturn(std::env::var(key).unwrap_or_default())
}

// Dart does not support changing environment variables.
// `Platform.environment['MY_VAR'] = 'VAR';` will throw an error
// `Unsupported operation: Cannot modify unmodifiable map`.
//
// And we need to share the environment variables between rust and dart isolates sometimes.
pub fn main_set_env(key: String, value: Option<String>) -> SyncReturn<()> {
    let is_valid_key = !key.is_empty() && !key.contains('=') && !key.contains('\0');
    debug_assert!(is_valid_key, "Invalid environment variable key: {}", key);
    if !is_valid_key {
        log::error!("Invalid environment variable key: {}", key);
        return SyncReturn(());
    }

    match value {
        Some(v) => {
            let is_valid_value = !v.contains('\0');
            debug_assert!(is_valid_value, "Invalid environment variable value: {}", v);
            if !is_valid_value {
                log::error!("Invalid environment variable value: {}", v);
                return SyncReturn(());
            }
            std::env::set_var(key, v);
        }
        None => std::env::remove_var(key),
    }

    SyncReturn(())
}

pub fn main_set_local_option(key: String, value: String) {
    let is_texture_render_key = key.eq(keys::OPTION_TEXTURE_RENDER);
    let is_d3d_render_key = key.eq(keys::OPTION_ALLOW_D3D_RENDER);
    set_local_option(key, value.clone());
    let is_render_target =
        |session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
    if is_texture_render_key {
        let session_event = [("v", &value)];
        for session in sessions::get_sessions() {
            if !is_render_target(&session) {
                continue;
            }
            session.push_event("use_texture_render", &session_event, &[]);
            session.use_texture_render_changed();
            session.ui_handler.update_use_texture_render();
        }
    }
    if is_d3d_render_key {
        for session in sessions::get_sessions() {
            if !is_render_target(&session) {
                continue;
            }
            session.update_supported_decodings();
        }
    }
}

// We do use use `main_get_local_option` and `main_set_local_option`.
//
// 1. For get, the value is stored in the server process.
// 2. For clear, we need to need to return the error mmsg from the server process to flutter.
pub fn main_handle_wayland_screencast_restore_token(_key: String, _value: String) -> String {
    #[cfg(not(target_os = "linux"))]
    {
        return "".to_owned();
    }
    #[cfg(target_os = "linux")]
    if _value == "get" {
        match crate::ipc::get_wayland_screencast_restore_token(_key) {
            Ok(v) => v,
            Err(e) => {
                log::error!("Failed to get wayland screencast restore token, {}", e);
                "".to_owned()
            }
        }
    } else if _value == "clear" {
        match crate::ipc::clear_wayland_screencast_restore_token(_key.clone()) {
            Ok(true) => {
                set_local_option(_key, "".to_owned());
                "".to_owned()
            }
            Ok(false) => "Failed to clear, please try again.".to_owned(),
            Err(e) => format!("Failed to clear, {}", e),
        }
    } else {
        "".to_owned()
    }
}

pub fn main_get_input_source() -> SyncReturn<String> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    let input_source = get_cur_session_input_source();
    #[cfg(any(target_os = "android", target_os = "ios"))]
    let input_source = "".to_owned();
    SyncReturn(input_source)
}

pub fn main_set_input_source(session_id: SessionID, value: String) {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        change_input_source(session_id, value);
        if let Some(session) = sessions::get_session_by_session_id(&session_id) {
            try_sync_peer_option(&session, &session_id, "input_source", None);
        }
    }
}

/// Set cursor position (for pointer lock re-centering).
///
/// # Returns
/// - `true`: cursor position was successfully set
/// - `false`: operation failed or not supported
///
/// # Platform behavior
/// - Windows/macOS/Linux: attempts to move the cursor to (x, y)
/// - Android/iOS: no-op, always returns `false`
pub fn main_set_cursor_position(x: i32, y: i32) -> SyncReturn<bool> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        SyncReturn(crate::set_cursor_pos(x, y))
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        let _ = (x, y);
        SyncReturn(false)
    }
}

/// Clip cursor to a rectangle (for pointer lock).
///
/// When `enable` is true, the cursor is clipped to the rectangle defined by
/// `left`, `top`, `right`, `bottom`. When `enable` is false, the rectangle
/// values are ignored and the cursor is unclipped.
///
/// # Returns
/// - `true`: operation succeeded or no-op completed
/// - `false`: operation failed
///
/// # Platform behavior
/// - Windows: uses ClipCursor API to confine cursor to the specified rectangle
/// - macOS: uses CGAssociateMouseAndMouseCursorPosition for pointer lock effect;
///   the rect coordinates are ignored (only Some/None matters)
/// - Linux: no-op, always returns `true`; use pointer warping for similar effect
/// - Android/iOS: no-op, always returns `false`
pub fn main_clip_cursor(
    left: i32,
    top: i32,
    right: i32,
    bottom: i32,
    enable: bool,
) -> SyncReturn<bool> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        let rect = if enable {
            Some((left, top, right, bottom))
        } else {
            None
        };
        SyncReturn(crate::clip_cursor(rect))
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        let _ = (left, top, right, bottom, enable);
        SyncReturn(false)
    }
}

pub fn main_get_my_id() -> String {
    get_id()
}

pub fn main_get_uuid() -> String {
    get_uuid()
}

pub fn main_get_peer_option(id: String, key: String) -> String {
    get_peer_option(id, key)
}

pub fn main_get_peer_option_sync(id: String, key: String) -> SyncReturn<String> {
    SyncReturn(get_peer_option(id, key))
}

// Sometimes we need to get the flutter option of a peer by reading the file.
// Because the session may not be established yet.
pub fn main_get_peer_flutter_option_sync(id: String, k: String) -> SyncReturn<String> {
    SyncReturn(get_peer_flutter_option(id, k))
}

pub fn main_set_peer_flutter_option_sync(id: String, k: String, v: String) -> SyncReturn<()> {
    set_peer_flutter_option(id, k, v);
    SyncReturn(())
}

pub fn main_set_peer_option(id: String, key: String, value: String) {
    set_peer_option(id, key, value)
}

pub fn main_set_peer_option_sync(id: String, key: String, value: String) -> SyncReturn<bool> {
    set_peer_option(id, key, value);
    SyncReturn(true)
}

pub fn main_set_peer_alias(id: String, alias: String) {
    set_peer_option(id, "alias".to_owned(), alias)
}

pub fn main_get_new_stored_peers() -> String {
    let peers: Vec<String> = config::NEW_STORED_PEER_CONFIG
        .lock()
        .unwrap()
        .drain()
        .collect();
    serde_json::to_string(&peers).unwrap_or_default()
}

pub fn main_forget_password(id: String) {
    forget_password(id)
}

pub fn main_peer_has_password(id: String) -> bool {
    peer_has_password(id)
}

pub fn main_peer_exists(id: String) -> bool {
    peer_exists(&id)
}

// `Config::path` resolves through `APP_DIR` only on mobile, where loading peers before it is set
// would read the wrong place. Elsewhere the peer path never consults it, so an unset `APP_DIR`
// must not stop the peers from loading.
fn peer_config_path_ready() -> bool {
    if cfg!(any(target_os = "android", target_os = "ios")) {
        !config::APP_DIR.read().unwrap().is_empty()
    } else {
        true
    }
}

fn load_recent_peers(
    vec_id_modified_time_path: &Vec<(String, SystemTime, std::path::PathBuf)>,
    to_end: bool,
    all_peers: &mut Vec<HashMap<&str, String>>,
    from: usize,
) -> usize {
    let to = if to_end {
        Some(vec_id_modified_time_path.len())
    } else {
        None
    };
    let mut peers_next = PeerConfig::batch_peers(vec_id_modified_time_path, from, to);
    // There may be less peers than the batch size.
    // But no need to consider this case, because it is a rare case.
    let peers = peers_next.0.drain(..).map(|(id, _, p)| peer_to_map(id, p));
    all_peers.extend(peers);
    peers_next.1
}

pub fn main_load_recent_peers() {
    let push_to_flutter = |peers, ids| {
        let mut data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]);
        if let Some(ids) = ids {
            data.insert("ids", ids);
        }
        let _res = flutter::push_global_event(
            flutter::APP_TYPE_MAIN,
            serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
        );
    };

    if peer_config_path_ready() {
        let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None);
        if vec_id_modified_time_path.is_empty() {
            push_to_flutter("".to_owned(), None);
            return;
        }

        let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT
            && cfg!(target_os = "windows");
        let mut all_peers = vec![];
        if load_two_times {
            let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0);
            let rest_ids = if next_from < vec_id_modified_time_path.len() {
                Some(
                    vec_id_modified_time_path[next_from..]
                        .iter()
                        .map(|(id, _, _)| id.clone())
                        .collect::<Vec<_>>()
                        .join(", "),
                )
            } else {
                None
            };
            push_to_flutter(
                serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()),
                rest_ids,
            );
            let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from);
        } else {
            let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0);
        }
        // Don't check if `all_peers` is empty, because we need this message to update the state in the flutter side.
        push_to_flutter(
            serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()),
            None,
        );
    } else {
        push_to_flutter("".to_owned(), None)
    }
}

pub fn main_load_recent_peers_for_ab(filter: String) -> String {
    let id_filters = serde_json::from_str::<Vec<String>>(&filter).unwrap_or_default();
    let id_filters = if id_filters.is_empty() {
        None
    } else {
        Some(id_filters)
    };
    if peer_config_path_ready() {
        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers(id_filters)
            .drain(..)
            .map(|(id, _, p)| peer_to_map(id, p))
            .collect();
        return serde_json::ser::to_string(&peers).unwrap_or("".to_owned());
    }
    "".to_string()
}

pub fn main_load_fav_peers() {
    let push_to_flutter = |peers| {
        let data = HashMap::from([("name", "load_fav_peers".to_owned()), ("peers", peers)]);
        let _res = flutter::push_global_event(
            flutter::APP_TYPE_MAIN,
            serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
        );
    };
    if peer_config_path_ready() {
        let favs = get_fav();
        let mut recent = PeerConfig::peers(Some(favs.clone()));
        let mut lan = config::LanPeers::load()
            .peers
            .iter()
            .filter(|d| favs.contains(&d.id) && recent.iter().all(|r| r.0 != d.id))
            .map(|d| {
                (
                    d.id.clone(),
                    SystemTime::UNIX_EPOCH,
                    PeerConfig {
                        info: PeerInfoSerde {
                            username: d.username.clone(),
                            hostname: d.hostname.clone(),
                            platform: d.platform.clone(),
                        },
                        ..Default::default()
                    },
                )
            })
            .collect();
        recent.append(&mut lan);
        let peers: Vec<HashMap<&str, String>> = recent
            .into_iter()
            .map(|(id, _, p)| peer_to_map(id, p))
            .collect();

        push_to_flutter(serde_json::ser::to_string(&peers).unwrap_or("".to_owned()));
    } else {
        push_to_flutter("".to_owned());
    }
}

pub fn main_load_lan_peers() {
    let data = HashMap::from([
        ("name", "load_lan_peers".to_owned()),
        (
            "peers",
            serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
        ),
    ]);
    let _res = flutter::push_global_event(
        flutter::APP_TYPE_MAIN,
        serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
    );
}

pub fn main_remove_discovered(id: String) {
    remove_discovered(id);
}

fn main_broadcast_message(data: &HashMap<&str, &str>) {
    let event = serde_json::ser::to_string(&data).unwrap_or("".to_owned());
    for app in flutter::get_global_event_channels() {
        if app == flutter::APP_TYPE_MAIN || app == flutter::APP_TYPE_CM {
            continue;
        }
        let _res = flutter::push_global_event(&app, event.clone());
    }
}

pub fn main_change_theme(dark: String) {
    main_broadcast_message(&HashMap::from([("name", "theme"), ("dark", &dark)]));
    #[cfg(not(any(target_os = "ios")))]
    send_to_cm(&crate::ipc::Data::Theme(dark));
}

pub fn main_change_language(lang: String) {
    main_broadcast_message(&HashMap::from([("name", "language"), ("lang", &lang)]));
    #[cfg(not(any(target_os = "ios")))]
    send_to_cm(&crate::ipc::Data::Language(lang));
}

pub fn main_video_save_directory(root: bool) -> SyncReturn<String> {
    SyncReturn(video_save_directory(root))
}

pub fn main_set_user_default_option(key: String, value: String) {
    set_user_default_option(key, value);
}

pub fn main_get_user_default_option(key: String) -> SyncReturn<String> {
    SyncReturn(get_user_default_option(key))
}

pub fn main_handle_relay_id(id: String) -> String {
    handle_relay_id(&id).to_owned()
}

pub fn main_is_option_fixed(key: String) -> SyncReturn<bool> {
    SyncReturn(is_option_fixed(&key))
}

pub fn main_get_main_display() -> SyncReturn<String> {
    #[cfg(target_os = "ios")]
    let display_info = "".to_owned();
    #[cfg(not(target_os = "ios"))]
    let mut display_info = "".to_owned();
    #[cfg(not(target_os = "ios"))]
    {
        #[cfg(not(target_os = "linux"))]
        let is_linux_wayland = false;
        #[cfg(target_os = "linux")]
        let is_linux_wayland = !is_x11();

        if !is_linux_wayland {
            if let Ok(displays) = crate::display_service::try_get_displays() {
                // to-do: Need to detect current display index.
                if let Some(display) = displays.iter().next() {
                    display_info = serde_json::to_string(&HashMap::from([
                        ("w", display.width()),
                        ("h", display.height()),
                    ]))
                    .unwrap_or_default();
                }
            }
        }

        #[cfg(target_os = "linux")]
        if is_linux_wayland {
            let displays = scrap::wayland::display::get_displays();
            if let Some(display) = displays.displays.get(displays.primary) {
                let logical_size = display
                    .logical_size
                    .unwrap_or((display.width, display.height));
                display_info = serde_json::to_string(&HashMap::from([
                    ("w", logical_size.0),
                    ("h", logical_size.1),
                ]))
                .unwrap_or_default();
            }
        }
    }
    SyncReturn(display_info)
}

// No need to check if is on Wayland in this function.
// The Flutter side gets display information on Wayland using a different method.
pub fn main_get_displays() -> SyncReturn<String> {
    #[cfg(target_os = "ios")]
    let display_info = "".to_owned();
    #[cfg(not(target_os = "ios"))]
    let mut display_info = "".to_owned();
    #[cfg(not(target_os = "ios"))]
    if let Ok(displays) = crate::display_service::try_get_displays() {
        let displays = displays
            .iter()
            .map(|d| {
                HashMap::from([
                    ("x", d.origin().0),
                    ("y", d.origin().1),
                    ("w", d.width() as i32),
                    ("h", d.height() as i32),
                ])
            })
            .collect::<Vec<_>>();
        display_info = serde_json::to_string(&displays).unwrap_or_default();
    }
    SyncReturn(display_info)
}

pub fn session_add_port_forward(
    session_id: SessionID,
    local_port: i32,
    remote_host: String,
    remote_port: i32,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.add_port_forward(local_port, remote_host, remote_port);
    }
}

pub fn session_remove_port_forward(session_id: SessionID, local_port: i32) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.remove_port_forward(local_port);
    }
}

pub fn session_new_rdp(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.new_rdp();
    }
}

pub fn session_request_voice_call(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.request_voice_call();
    }
}

pub fn session_close_voice_call(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.close_voice_call();
    }
}

pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn<Option<String>> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.get_conn_token())
    } else {
        SyncReturn(None)
    }
}

pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) {
    crate::ui_cm_interface::handle_incoming_voice_call(id, accept);
}

pub fn cm_close_voice_call(id: i32) {
    crate::ui_cm_interface::close_voice_call(id);
}

pub fn set_voice_call_input_device(_is_cm: bool, _device: String) {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    if _is_cm {
        let _ = crate::ipc::set_config("voice-call-input", _device);
    } else {
        crate::audio_service::set_voice_call_input_device(Some(_device), true);
    }
}

pub fn get_voice_call_input_device(_is_cm: bool) -> String {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    if _is_cm {
        match crate::ipc::get_config("voice-call-input") {
            Ok(Some(device)) => device,
            _ => "".to_owned(),
        }
    } else {
        crate::audio_service::get_voice_call_input_device().unwrap_or_default()
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    "".to_owned()
}

pub fn main_get_last_remote_id() -> String {
    LocalConfig::get_remote_id()
}

pub fn main_get_software_update_url() {
    crate::common::check_software_update();
}

pub fn main_get_home_dir() -> String {
    fs::get_home_as_string()
}

pub fn main_get_langs() -> String {
    get_langs()
}

pub fn main_get_temporary_password() -> String {
    ui_interface::temporary_password()
}

pub fn main_set_permanent_password_with_result(password: String) -> bool {
    ui_interface::set_permanent_password_with_result(password)
}

pub fn main_get_fingerprint() -> String {
    get_fingerprint()
}

pub fn cm_get_clients_state() -> String {
    crate::ui_cm_interface::get_clients_state()
}

pub fn cm_check_clients_length(length: usize) -> Option<String> {
    if length != crate::ui_cm_interface::get_clients_length() {
        Some(crate::ui_cm_interface::get_clients_state())
    } else {
        None
    }
}

pub fn cm_get_clients_length() -> usize {
    crate::ui_cm_interface::get_clients_length()
}

pub fn main_init(app_dir: String, custom_client_config: String) {
    initialize(&app_dir, &custom_client_config);
    crate::keyboard::shortcuts::reload_from_config();
}

pub fn main_device_id(id: String) {
    *crate::common::DEVICE_ID.lock().unwrap() = id;
}

pub fn main_device_name(name: String) {
    *crate::common::DEVICE_NAME.lock().unwrap() = name;
}

pub fn main_remove_peer(id: String) {
    PeerConfig::remove(&id);
}

pub fn main_has_hwcodec() -> SyncReturn<bool> {
    SyncReturn(has_hwcodec())
}

pub fn main_has_vram() -> SyncReturn<bool> {
    SyncReturn(has_vram())
}

pub fn main_supported_hwdecodings() -> SyncReturn<String> {
    let decoding = supported_hwdecodings();
    let msg = HashMap::from([("h264", decoding.0), ("h265", decoding.1)]);

    SyncReturn(serde_json::ser::to_string(&msg).unwrap_or("".to_owned()))
}

pub fn main_is_root() -> bool {
    is_root()
}

pub fn get_double_click_time() -> SyncReturn<i32> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        return SyncReturn(crate::platform::get_double_click_time() as _);
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    SyncReturn(500i32)
}

pub fn main_start_dbus_server() {
    #[cfg(target_os = "linux")]
    {
        use crate::dbus::start_dbus_server;
        // spawn new thread to start dbus server
        std::thread::spawn(|| {
            let _ = start_dbus_server();
        });
    }
}

pub fn main_save_ab(json: String) {
    if json.len() > 1024 {
        std::thread::spawn(|| {
            config::Ab::store(json);
        });
    } else {
        config::Ab::store(json);
    }
}

pub fn main_clear_ab() {
    config::Ab::remove();
}

pub fn main_load_ab() -> String {
    serde_json::to_string(&config::Ab::load()).unwrap_or_default()
}

pub fn main_save_group(json: String) {
    if json.len() > 1024 {
        std::thread::spawn(|| {
            config::Group::store(json);
        });
    } else {
        config::Group::store(json);
    }
}

pub fn main_clear_group() {
    config::Group::remove();
}

pub fn main_load_group() -> String {
    serde_json::to_string(&config::Group::load()).unwrap_or_default()
}

pub fn session_send_pointer(session_id: SessionID, msg: String) {
    super::flutter::session_send_pointer(session_id, msg);
}

/// Send mouse event from Flutter to the remote peer.
///
/// # Relative Mouse Mode Message Contract
///
/// When the message contains a `relative_mouse_mode` field, this function validates
/// and filters activation/deactivation markers.
///
/// **Mode Authority:**
/// The Flutter InputModel is authoritative for relative mouse mode activation/deactivation.
/// The server (via `input_service.rs`) only consumes forwarded delta movements and tracks
/// relative movement processing state, but does NOT control mode activation/deactivation.
///
/// **Deactivation Markers are Local-Only:**
/// Deactivation markers (`relative_mouse_mode: "0"`) are NEVER forwarded to the server.
/// They are handled entirely on the client side to reset local UI state (cursor visibility,
/// pointer lock, etc.). The server does not rely on deactivation markers and should not
/// expect to receive them.
///
/// **Contract (Flutter side MUST adhere to):**
/// 1. `relative_mouse_mode` field is ONLY present on activation/deactivation marker messages,
///    NEVER on normal pointer events (move, button, scroll).
/// 2. Deactivation marker: `{"relative_mouse_mode": "0"}` - local-only, never forwarded.
/// 3. Activation marker: `{"relative_mouse_mode": "1", "type": "move_relative", "x": "0", "y": "0"}`
///    - MUST use `type="move_relative"` with `x="0"` and `y="0"` (safe no-op).
///    - Any other combination is dropped to prevent accidental cursor movement.
///
/// If these assumptions are violated (e.g., `relative_mouse_mode` is added to normal events),
/// legitimate mouse events may be silently dropped by the early-return logic below.
pub fn session_send_mouse(session_id: SessionID, msg: String) {
    if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
        // Relative mouse mode marker validation (Flutter-only).
        // This only validates and filters markers; the server tracks per-connection
        // relative-movement processing state but not mode activation/deactivation.
        // See doc comment above for the message contract.
        if let Some(v) = m.get("relative_mouse_mode") {
            let active = matches!(v.as_str(), "1" | "Y" | "on");

            // Disable marker: local-only, never forwarded to the server.
            // The server does not track mode deactivation; it simply stops receiving
            // relative move events when the client exits relative mouse mode.
            if !active {
                #[cfg(not(any(target_os = "android", target_os = "ios")))]
                crate::keyboard::set_relative_mouse_mode_state(false);
                return;
            }

            // Enable marker: validate BEFORE setting state to avoid desync.
            // This ensures we only mark as active if the marker will actually be forwarded.

            // Enable marker is allowed to go through only if it's a safe no-op relative move.
            // This avoids accidentally moving the remote cursor (e.g. if type/x/y are missing).
            let msg_type = m.get("type").map(|t| t.as_str());
            if msg_type != Some("move_relative") {
                log::warn!(
                    "relative_mouse_mode activation marker has invalid type: {:?}, expected 'move_relative'. Dropping.",
                    msg_type
                );
                return;
            }
            let x_marker = m
                .get("x")
                .map(|x| x.parse::<i32>().unwrap_or(0))
                .unwrap_or(0);
            let y_marker = m
                .get("y")
                .map(|y| y.parse::<i32>().unwrap_or(0))
                .unwrap_or(0);
            if x_marker != 0 || y_marker != 0 {
                log::warn!(
                    "relative_mouse_mode activation marker has non-zero coordinates: x={}, y={}. Dropping.",
                    x_marker, y_marker
                );
                return;
            }

            // Guard against unexpected fields that could turn this no-op into a real event.
            if m.contains_key("buttons")
                || m.contains_key("alt")
                || m.contains_key("ctrl")
                || m.contains_key("shift")
                || m.contains_key("command")
            {
                log::warn!(
                    "relative_mouse_mode activation marker contains unexpected fields (buttons/alt/ctrl/shift/command). Dropping."
                );
                return;
            }

            // All validation passed - marker will be forwarded as a no-op relative move.
            #[cfg(not(any(target_os = "android", target_os = "ios")))]
            crate::keyboard::set_relative_mouse_mode_state(true);
        }

        let alt = m.get("alt").is_some();
        let ctrl = m.get("ctrl").is_some();
        let shift = m.get("shift").is_some();
        let command = m.get("command").is_some();
        let x = m
            .get("x")
            .map(|x| x.parse::<i32>().unwrap_or(0))
            .unwrap_or(0);
        let y = m
            .get("y")
            .map(|x| x.parse::<i32>().unwrap_or(0))
            .unwrap_or(0);
        let mut mask = 0;
        if let Some(_type) = m.get("type") {
            mask = match _type.as_str() {
                "down" => MOUSE_TYPE_DOWN,
                "up" => MOUSE_TYPE_UP,
                "wheel" => MOUSE_TYPE_WHEEL,
                "trackpad" => MOUSE_TYPE_TRACKPAD,
                "move_relative" => MOUSE_TYPE_MOVE_RELATIVE,
                _ => 0,
            };
        }
        if let Some(buttons) = m.get("buttons") {
            mask |= match buttons.as_str() {
                "left" => MOUSE_BUTTON_LEFT,
                "right" => MOUSE_BUTTON_RIGHT,
                "wheel" => MOUSE_BUTTON_WHEEL,
                "back" => MOUSE_BUTTON_BACK,
                "forward" => MOUSE_BUTTON_FORWARD,
                _ => 0,
            } << 3;
        }
        if let Some(session) = sessions::get_session_by_session_id(&session_id) {
            session.send_mouse(mask, x, y, alt, ctrl, shift, command);
        }
    }
}

pub fn session_restart_remote_device(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.restart_remote_device();
    }
}

pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> SyncReturn<String> {
    let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.get_audit_server(typ)
    } else {
        "".to_owned()
    };
    SyncReturn(res)
}

pub fn session_send_note(session_id: SessionID, note: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.send_note(note)
    }
}

pub fn session_get_last_audit_note(session_id: SessionID) -> SyncReturn<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.last_audit_note.lock().unwrap().clone())
    } else {
        SyncReturn("".to_owned())
    }
}

pub fn session_set_audit_guid(session_id: SessionID, guid: String) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        *session.audit_guid.lock().unwrap() = guid;
    }
}

pub fn session_get_audit_guid(session_id: SessionID) -> SyncReturn<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.audit_guid.lock().unwrap().clone())
    } else {
        SyncReturn("".to_owned())
    }
}

pub fn session_get_conn_session_id(session_id: SessionID) -> SyncReturn<String> {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        SyncReturn(session.lc.read().unwrap().session_id.to_string())
    } else {
        SyncReturn("".to_owned())
    }
}

pub fn session_alternative_codecs(session_id: SessionID) -> String {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        let (vp8, av1, h264, h265) = session.alternative_codecs();
        let msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]);
        serde_json::ser::to_string(&msg).unwrap_or("".to_owned())
    } else {
        String::new()
    }
}

pub fn session_change_prefer_codec(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.update_supported_decodings();
    }
}

pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
    super::flutter::session_on_waiting_for_image_dialog_show(session_id);
}

pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: bool) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.toggle_virtual_display(index, on);
        flutter::session_update_virtual_display(&session, index, on);
    }
}

pub fn session_printer_response(
    session_id: SessionID,
    id: i32,
    path: String,
    printer_name: String,
) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.printer_response(id, path, printer_name);
    }
}

pub fn main_set_home_dir(_home: String) {
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        *config::APP_HOME_DIR.write().unwrap() = _home;
    }
}

// This is a temporary method to get data dir for ios
pub fn main_get_data_dir_ios(app_dir: String) -> SyncReturn<String> {
    *config::APP_DIR.write().unwrap() = app_dir;
    let data_dir = config::Config::path("data");
    if !data_dir.exists() {
        if let Err(e) = std::fs::create_dir_all(&data_dir) {
            log::warn!("Failed to create data dir {}", e);
        }
    }
    SyncReturn(data_dir.to_string_lossy().to_string())
}

pub fn main_stop_service() {
    #[cfg(target_os = "android")]
    {
        config::Config::set_option("stop-service".into(), "Y".into());
        crate::rendezvous_mediator::RendezvousMediator::restart();
    }
}

pub fn main_start_service() {
    #[cfg(target_os = "android")]
    {
        config::Config::set_option("stop-service".into(), "".into());
        crate::rendezvous_mediator::reset_needs_deploy_notification();
        crate::rendezvous_mediator::RendezvousMediator::restart();
    }
}

pub fn main_update_temporary_password() {
    update_temporary_password();
}

pub fn main_check_super_user_permission() -> bool {
    check_super_user_permission()
}

pub fn main_get_unlock_pin() -> SyncReturn<String> {
    SyncReturn(get_unlock_pin())
}

pub fn main_set_unlock_pin(pin: String) -> SyncReturn<String> {
    SyncReturn(set_unlock_pin(pin))
}

pub fn main_check_mouse_time() {
    check_mouse_time();
}

pub fn main_get_mouse_time() -> f64 {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        get_mouse_time()
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        0.0
    }
}

pub fn main_wol(id: String) {
    // TODO: move send_wol outside.
    #[cfg(not(any(target_os = "ios")))]
    crate::lan::send_wol(id)
}

pub fn main_create_shortcut(_id: String) {
    #[cfg(windows)]
    create_shortcut(_id);
}

pub fn cm_send_chat(conn_id: i32, msg: String) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::send_chat(conn_id, msg);
}

pub fn cm_login_res(conn_id: i32, res: bool) {
    #[cfg(not(any(target_os = "ios")))]
    if res {
        crate::ui_cm_interface::authorize(conn_id);
    } else {
        crate::ui_cm_interface::close(conn_id);
    }
}

pub fn cm_close_connection(conn_id: i32) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::close(conn_id);
}

/// The CM window closed. On Linux that is ambiguous - a logout closes it the same way a person
/// does - so it ends the session without the no-retry reason; elsewhere it is a plain close.
pub fn cm_close_connection_window(conn_id: i32) {
    #[cfg(target_os = "linux")]
    crate::ui_cm_interface::close_window(conn_id);
    #[cfg(all(not(target_os = "linux"), not(target_os = "ios")))]
    crate::ui_cm_interface::close(conn_id);
}

pub fn cm_remove_disconnected_connection(conn_id: i32) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::remove(conn_id);
}

pub fn cm_check_click_time(conn_id: i32) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::check_click_time(conn_id)
}

pub fn cm_get_click_time() -> f64 {
    #[cfg(not(any(target_os = "ios")))]
    return crate::ui_cm_interface::get_click_time() as _;
    #[cfg(any(target_os = "ios"))]
    return 0 as _;
}

pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::switch_permission(conn_id, name, enabled)
}

pub fn cm_can_elevate() -> SyncReturn<bool> {
    SyncReturn(crate::ui_cm_interface::can_elevate())
}

pub fn cm_elevate_portable(conn_id: i32) {
    #[cfg(not(any(target_os = "ios")))]
    crate::ui_cm_interface::elevate_portable(conn_id);
}

pub fn cm_switch_back(conn_id: i32) {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    crate::ui_cm_interface::switch_back(conn_id);
}

pub fn cm_get_config(name: String) -> String {
    #[cfg(not(target_os = "ios"))]
    {
        if let Ok(Some(v)) = crate::ipc::get_config(&name) {
            v
        } else {
            "".to_string()
        }
    }
    #[cfg(target_os = "ios")]
    {
        "".to_string()
    }
}

pub fn main_get_build_date() -> String {
    crate::BUILD_DATE.to_string()
}

pub fn translate(name: String, locale: String) -> SyncReturn<String> {
    SyncReturn(crate::client::translate_locale(name, &locale))
}

pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> SyncReturn<usize> {
    SyncReturn(super::flutter::session_get_rgba_size(session_id, display))
}

pub fn session_next_rgba(session_id: SessionID, display: usize) -> SyncReturn<()> {
    SyncReturn(super::flutter::session_next_rgba(session_id, display))
}

pub fn session_register_pixelbuffer_texture(
    session_id: SessionID,
    display: usize,
    ptr: usize,
) -> SyncReturn<()> {
    SyncReturn(super::flutter::session_register_pixelbuffer_texture(
        session_id, display, ptr,
    ))
}

pub fn session_register_gpu_texture(
    session_id: SessionID,
    display: usize,
    ptr: usize,
) -> SyncReturn<()> {
    SyncReturn(super::flutter::session_register_gpu_texture(
        session_id, display, ptr,
    ))
}

pub fn query_onlines(ids: Vec<String>) {
    let _ = flutter::async_tasks::query_onlines(ids);
}

pub fn version_to_number(v: String) -> SyncReturn<i64> {
    SyncReturn(hbb_common::get_version_number(&v))
}

pub fn option_synced() -> bool {
    crate::ui_interface::option_synced()
}

pub fn main_is_installed() -> SyncReturn<bool> {
    SyncReturn(is_installed())
}

pub fn main_init_input_source() -> SyncReturn<()> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    crate::keyboard::input_source::init_input_source();
    SyncReturn(())
}

pub fn main_reload_keyboard_shortcuts() -> SyncReturn<()> {
    crate::keyboard::shortcuts::reload_from_config();
    SyncReturn(())
}

pub fn main_get_default_keyboard_shortcuts() -> SyncReturn<String> {
    let bindings = crate::keyboard::shortcuts::default_bindings();
    let json = serde_json::to_string(&bindings).unwrap_or_default();
    SyncReturn(json)
}

pub fn main_is_installed_lower_version() -> SyncReturn<bool> {
    SyncReturn(is_installed_lower_version())
}

pub fn main_is_installed_daemon(prompt: bool) -> SyncReturn<bool> {
    SyncReturn(is_installed_daemon(prompt))
}

pub fn main_is_process_trusted(prompt: bool) -> SyncReturn<bool> {
    SyncReturn(is_process_trusted(prompt))
}

pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn<bool> {
    SyncReturn(is_can_screen_recording(prompt))
}

pub fn main_is_can_input_monitoring(prompt: bool) -> SyncReturn<bool> {
    SyncReturn(is_can_input_monitoring(prompt))
}

pub fn main_is_share_rdp() -> SyncReturn<bool> {
    SyncReturn(is_share_rdp())
}

pub fn main_set_share_rdp(enable: bool) {
    set_share_rdp(enable)
}

pub fn main_goto_install() -> SyncReturn<bool> {
    goto_install();
    SyncReturn(true)
}

pub fn main_get_new_version() -> SyncReturn<String> {
    SyncReturn(get_new_version())
}

pub fn main_update_me() -> SyncReturn<bool> {
    update_me("".to_owned());
    SyncReturn(true)
}

pub fn set_cur_session_id(session_id: SessionID) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        set_cur_session_id_(session_id, &session.get_keyboard_mode())
    }
}

fn set_cur_session_id_(session_id: SessionID, _keyboard_mode: &str) {
    super::flutter::set_cur_session_id(session_id);
    #[cfg(windows)]
    crate::keyboard::update_grab_get_key_name(_keyboard_mode);
}

pub fn install_show_run_without_install() -> SyncReturn<bool> {
    SyncReturn(show_run_without_install())
}

pub fn install_run_without_install() {
    run_without_install();
}

pub fn install_install_me(options: String, path: String) {
    install_me(options, path, false, false);
}

pub fn install_install_path() -> SyncReturn<String> {
    SyncReturn(install_path())
}

pub fn install_install_options() -> SyncReturn<String> {
    SyncReturn(install_options())
}

pub fn main_account_auth(op: String, remember_me: bool) {
    let id = get_id();
    let uuid = get_uuid();
    account_auth(op, id, uuid, remember_me);
}

pub fn main_account_auth_cancel() {
    account_auth_cancel()
}

pub fn main_account_auth_result() -> String {
    account_auth_result()
}

pub fn main_on_main_window_close() {
    // may called more than one times
    #[cfg(windows)]
    crate::portable_service::client::drop_portable_service_shared_memory();
}

pub fn main_current_is_wayland() -> SyncReturn<bool> {
    SyncReturn(current_is_wayland())
}

pub fn main_is_login_wayland() -> SyncReturn<bool> {
    SyncReturn(is_login_wayland())
}

pub fn main_hide_dock() -> SyncReturn<bool> {
    #[cfg(target_os = "macos")]
    crate::platform::macos::hide_dock();
    SyncReturn(true)
}

pub fn main_has_file_clipboard() -> SyncReturn<bool> {
    let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",));
    SyncReturn(ret)
}

pub fn main_has_gpu_texture_render() -> SyncReturn<bool> {
    SyncReturn(cfg!(feature = "vram"))
}

pub fn cm_init() {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    crate::flutter::connection_manager::cm_init();
}

/// Start an ipc server for receiving the url scheme.
///
/// * Should only be called in the main flutter window.
/// * macOS only
pub fn main_start_ipc_url_server() {
    #[cfg(target_os = "macos")]
    std::thread::spawn(move || crate::server::start_ipc_url_server());
}

pub fn main_test_wallpaper(_second: u64) {
    #[cfg(any(target_os = "windows", target_os = "linux"))]
    std::thread::spawn(move || match crate::platform::WallPaperRemover::new() {
        Ok(_remover) => {
            std::thread::sleep(std::time::Duration::from_secs(_second));
        }
        Err(e) => {
            log::info!("create wallpaper remover failed: {:?}", e);
        }
    });
}

pub fn main_support_remove_wallpaper() -> bool {
    support_remove_wallpaper()
}

pub fn is_incoming_only() -> SyncReturn<bool> {
    SyncReturn(config::is_incoming_only())
}

pub fn is_outgoing_only() -> SyncReturn<bool> {
    SyncReturn(config::is_outgoing_only())
}

pub fn is_custom_client() -> SyncReturn<bool> {
    SyncReturn(crate::common::is_custom_client())
}

pub fn is_disable_settings() -> SyncReturn<bool> {
    SyncReturn(config::is_disable_settings())
}

pub fn is_disable_ab() -> SyncReturn<bool> {
    SyncReturn(config::is_disable_ab())
}

pub fn is_disable_account() -> SyncReturn<bool> {
    SyncReturn(config::is_disable_account())
}

pub fn is_disable_group_panel() -> SyncReturn<bool> {
    SyncReturn(LocalConfig::get_option("disable-group-panel") == "Y")
}

// windows only
pub fn is_disable_installation() -> SyncReturn<bool> {
    SyncReturn(config::is_disable_installation())
}

pub fn is_preset_password() -> bool {
    // On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    return crate::ipc::is_permanent_password_preset();

    // On mobile, we have no service IPC; verify against local storage.
    #[cfg(any(target_os = "android", target_os = "ios"))]
    return config::Config::is_using_preset_password();
}

// Don't call this function for desktop version.
// We need this function because we want a sync return for mobile version.
pub fn is_preset_password_mobile_only() -> SyncReturn<bool> {
    SyncReturn(is_preset_password())
}

/// Send a url scheme through the ipc.
///
/// * macOS only
#[allow(unused_variables)]
pub fn send_url_scheme(_url: String) {
    #[cfg(target_os = "macos")]
    std::thread::spawn(move || crate::handle_url_scheme(_url));
}

pub fn is_support_multi_ui_session(version: String) -> SyncReturn<bool> {
    SyncReturn(crate::common::is_support_multi_ui_session(&version))
}

pub fn is_selinux_enforcing() -> SyncReturn<bool> {
    #[cfg(target_os = "linux")]
    {
        SyncReturn(crate::platform::linux::is_selinux_enforcing())
    }
    #[cfg(not(target_os = "linux"))]
    {
        SyncReturn(false)
    }
}

pub fn main_default_privacy_mode_impl() -> SyncReturn<String> {
    SyncReturn(crate::privacy_mode::DEFAULT_PRIVACY_MODE_IMPL.to_owned())
}

pub fn main_supported_privacy_mode_impls() -> SyncReturn<String> {
    SyncReturn(
        serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl())
            .unwrap_or_default(),
    )
}

pub fn main_supported_input_source() -> SyncReturn<String> {
    #[cfg(any(target_os = "android", target_os = "ios"))]
    {
        SyncReturn("".to_owned())
    }
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        SyncReturn(
            serde_json::to_string(&crate::keyboard::input_source::get_supported_input_source())
                .unwrap_or_default(),
        )
    }
}

pub fn main_generate2fa() -> String {
    generate2fa()
}

pub fn main_verify2fa(code: String) -> bool {
    verify2fa(code)
}

pub fn main_has_valid_2fa_sync() -> SyncReturn<bool> {
    SyncReturn(has_valid_2fa())
}

pub fn main_verify_bot(token: String) -> String {
    verify_bot(token)
}

pub fn main_has_valid_bot_sync() -> SyncReturn<bool> {
    SyncReturn(has_valid_bot())
}

pub fn main_get_hard_option(key: String) -> SyncReturn<String> {
    SyncReturn(get_hard_option(key))
}

pub fn main_get_buildin_option(key: String) -> SyncReturn<String> {
    SyncReturn(get_builtin_option(&key))
}

pub fn main_check_hwcodec() {
    check_hwcodec()
}

// The bridge uses ordinary worker threads; desktop IPC needs a runtime at this boundary.
pub fn main_get_trusted_devices() -> hbb_common::anyhow::Result<String> {
    #[cfg(any(target_os = "android", target_os = "ios"))]
    return crate::trusted_devices::devices();
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    hbb_common::tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?
        .block_on(crate::ipc::trusted_devices(authority::Request::List))
}

pub fn main_remove_trusted_devices(json: String) -> hbb_common::anyhow::Result<()> {
    let record_ids = serde_json::from_str::<Vec<[u8; crate::trusted_devices::TOKEN_BYTES]>>(&json)?;
    #[cfg(any(target_os = "android", target_os = "ios"))]
    return crate::trusted_devices::remove(&record_ids);
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    hbb_common::tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?
        .block_on(crate::ipc::trusted_devices(authority::Request::Remove(
            record_ids,
        )))
        .map(|_| ())
}

pub fn main_renew_trusted_devices(json: String) -> hbb_common::anyhow::Result<()> {
    let record_ids = serde_json::from_str::<Vec<[u8; crate::trusted_devices::TOKEN_BYTES]>>(&json)?;
    #[cfg(any(target_os = "android", target_os = "ios"))]
    return crate::trusted_devices::renew(&record_ids);
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    hbb_common::tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?
        .block_on(crate::ipc::trusted_devices(authority::Request::Renew(
            record_ids,
        )))
        .map(|_| ())
}

pub fn main_clear_trusted_devices() -> hbb_common::anyhow::Result<()> {
    #[cfg(any(target_os = "android", target_os = "ios"))]
    return crate::trusted_devices::clear();
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    hbb_common::tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?
        .block_on(crate::ipc::trusted_devices(authority::Request::Clear))
        .map(|_| ())
}

pub fn main_max_encrypt_len() -> SyncReturn<usize> {
    SyncReturn(max_encrypt_len())
}

pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) {
    if let Some(session) = sessions::get_session_by_session_id(&session_id) {
        session.request_init_msgs(display);
    }
}

pub fn main_audio_support_loopback() -> SyncReturn<bool> {
    #[cfg(target_os = "windows")]
    let is_surpport = true;
    #[cfg(feature = "screencapturekit")]
    let is_surpport = crate::audio_service::is_screen_capture_kit_available();
    #[cfg(not(any(target_os = "windows", feature = "screencapturekit")))]
    let is_surpport = false;
    SyncReturn(is_surpport)
}

pub fn main_get_printer_names() -> SyncReturn<String> {
    #[cfg(target_os = "windows")]
    return SyncReturn(
        serde_json::to_string(&crate::platform::windows::get_printer_names().unwrap_or_default())
            .unwrap_or_default(),
    );
    #[cfg(not(target_os = "windows"))]
    return SyncReturn("".to_owned());
}

pub fn main_get_common(key: String) -> String {
    if key == "is-printer-installed" {
        #[cfg(target_os = "windows")]
        {
            return match remote_printer::is_rd_printer_installed(&get_app_name()) {
                Ok(r) => r.to_string(),
                Err(e) => e.to_string(),
            };
        }
        #[cfg(not(target_os = "windows"))]
        return false.to_string();
    } else if key == "is-support-printer-driver" {
        #[cfg(target_os = "windows")]
        return crate::platform::is_win_10_or_greater().to_string();
        #[cfg(not(target_os = "windows"))]
        return false.to_string();
    } else if key == "transfer-job-id" {
        return base::fs::get_next_job_id().to_string();
    } else if key == "is-remote-modify-enabled-by-control-permissions" {
        return match is_remote_modify_enabled_by_control_permissions() {
            Some(true) => "true",
            Some(false) => "false",
            None => "",
        }
        .to_string();
    } else if key == "has-gnome-shortcuts-inhibitor-permission" {
        #[cfg(target_os = "linux")]
        return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
        #[cfg(not(target_os = "linux"))]
        return false.to_string();
    } else if key == "gnome-monitor-layout-mode" {
        #[cfg(target_os = "linux")]
        return match crate::platform::linux::gnome_monitor_layout_mode() {
            Some(mode) => mode.as_str().to_owned(),
            None => String::new(),
        };
        #[cfg(not(target_os = "linux"))]
        return String::new();
    } else if key == "permanent-password-set" {
        return ui_interface::is_permanent_password_set().to_string();
    } else if key == "local-permanent-password-set" {
        return ui_interface::is_local_permanent_password_set().to_string();
    } else {
        if key.starts_with("download-data-") {
            let id = key.replace("download-data-", "");
            match crate::hbbs_http::downloader::get_download_data(&id) {
                Ok(data) => serde_json::to_string(&data).unwrap_or_default(),
                Err(e) => {
                    format!("error:{}", e)
                }
            }
        } else if let Some(release_page_url) = key.strip_prefix("verified-download-url-") {
            #[cfg(any(target_os = "windows", target_os = "macos"))]
            {
                return match verified_update_artifact_for_release_page_url(release_page_url) {
                    Ok(artifact) => artifact.url,
                    Err(e) => format!("error:{}", e),
                };
            }
            #[cfg(not(any(target_os = "windows", target_os = "macos")))]
            {
                "error:unsupported".to_owned()
            }
        } else {
            "".to_owned()
        }
    }
}

pub fn main_get_common_sync(key: String) -> SyncReturn<String> {
    if let Some(args) = key.strip_prefix("resolve-api-server:") {
        let response = match serde_json::from_str::<(String, String)>(args) {
            Ok((api, custom)) => serde_json::json!({"url": crate::get_api_server(api, custom)}),
            Err(err) => {
                log::trace!("Invalid API server preview arguments: {err}");
                serde_json::json!({"error": err.to_string()})
            }
        };
        return SyncReturn(response.to_string());
    }
    // Metadata verification performs blocking network requests. Keep this key on
    // the asynchronous FFI path to avoid blocking the calling Flutter isolate.
    if key.starts_with("verified-download-url-") {
        return SyncReturn("error:unsupported".to_owned());
    }
    SyncReturn(main_get_common(key))
}

#[cfg(any(target_os = "windows", target_os = "macos"))]
fn push_update_me_error(error: String) {
    let data = HashMap::from([("name", "update-me".to_owned()), ("error", error)]);
    let _res = flutter::push_global_event(
        flutter::APP_TYPE_MAIN,
        serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
    );
}

#[cfg(any(target_os = "windows", target_os = "macos"))]
fn verified_update_artifact_for_release_page_url(
    release_page_url: &str,
) -> ResultType<crate::update_metadata::VerifiedUpdateArtifact> {
    #[cfg(target_os = "windows")]
    let update_msi = crate::platform::windows::is_msi_installed()? && !crate::is_custom_client();
    #[cfg(target_os = "macos")]
    let update_msi = false;
    let query = crate::updater::current_update_artifact_query(update_msi);
    crate::updater::verified_update_artifact_for_release_page_url(release_page_url, query)
}

pub fn main_set_common(_key: String, _value: String) {
    #[cfg(target_os = "windows")]
    if _key == "install-printer" && crate::platform::is_win_10_or_greater() {
        std::thread::spawn(move || {
            let (success, msg) = match remote_printer::install_update_printer(&get_app_name()) {
                Ok(_) => (true, "".to_owned()),
                Err(e) => {
                    let err = e.to_string();
                    log::error!("Failed to install/update rd printer: {}", &err);
                    (false, err)
                }
            };
            if success {
                // Use `ipc` to notify the server process to update the install option in the registry.
                // Because `install_update_printer()` may prompt for permissions, there is no need to prompt again here.
                if let Err(e) = crate::ipc::set_install_option(
                    crate::platform::REG_NAME_INSTALL_PRINTER.to_string(),
                    "1".to_string(),
                ) {
                    log::error!("Failed to set install printer option: {}", e);
                }
            }
            let data = HashMap::from([
                ("name", serde_json::json!("install-printer-res")),
                ("success", serde_json::json!(success)),
                ("msg", serde_json::json!(msg)),
            ]);
            let _res = flutter::push_global_event(
                flutter::APP_TYPE_MAIN,
                serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
            );
        });
    }
    #[cfg(any(target_os = "windows", target_os = "macos"))]
    {
        use crate::updater::get_update_download_file_from_url;
        if _key == "download-new-version" {
            let download_url = _value.clone();
            let event_key = "download-new-version".to_owned();
            let data =
                match crate::updater::verified_update_artifact_for_download_url(&download_url) {
                    Ok(artifact) => {
                        if let Some(download_file) =
                            get_update_download_file_from_url(&artifact.url)
                        {
                            let options = crate::hbbs_http::downloader::DownloadOptions {
                                expected_size: artifact.size,
                                artifact_sha256: artifact.sha256,
                            };
                            match crate::hbbs_http::downloader::download_file(
                                artifact.url,
                                Some(download_file),
                                options,
                            ) {
                                Ok(id) => HashMap::from([("name", event_key), ("id", id)]),
                                Err(e) => {
                                    HashMap::from([("name", event_key), ("error", e.to_string())])
                                }
                            }
                        } else {
                            HashMap::from([
                                ("name", event_key),
                                ("error", "Invalid download url".to_owned()),
                            ])
                        }
                    }
                    Err(e) => HashMap::from([
                        ("name", event_key),
                        ("error", format!("Failed to verify update metadata, {}", e)),
                    ]),
                };
            let _res = flutter::push_global_event(
                flutter::APP_TYPE_MAIN,
                serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
            );
        } else if _key == "update-me" {
            let artifact = match crate::updater::verified_update_artifact_for_download_url(&_value)
            {
                Ok(artifact) => artifact,
                Err(e) => {
                    let error = format!("Failed to verify update metadata, {}", e);
                    log::error!("{}", error);
                    push_update_me_error(error);
                    return;
                }
            };
            let Some(new_version_file) = get_update_download_file_from_url(&artifact.url) else {
                let error = "Failed to resolve downloaded update file path".to_owned();
                log::error!("{}", error);
                push_update_me_error(error);
                return;
            };
            let Some(new_version_file_str) = new_version_file.to_str() else {
                let error = "Invalid UTF-8 path for downloaded update file".to_owned();
                log::error!("{}", error);
                push_update_me_error(error);
                return;
            };
            log::debug!(
                "New version file is downloaded, update begin, {:?}",
                new_version_file_str
            );
            #[cfg(target_os = "windows")]
            let update_res = crate::platform::update_to_verified(
                new_version_file_str,
                &artifact.sha256,
                artifact.size,
            );
            #[cfg(target_os = "macos")]
            let update_res = crate::platform::macos::update_to_verified_dmg(
                new_version_file_str,
                &artifact.sha256,
                Some(artifact.size),
            );
            if update_res.is_err() {
                crate::updater::remove_update_file(&new_version_file);
            }
            match update_res {
                Ok(_) => log::info!("Update process is launched successfully!"),
                Err(e) => {
                    let error = format!("Failed to update to new version, {}", e);
                    log::error!("{}", error);
                    push_update_me_error(error);
                }
            }
        }
    }

    if _key == "remove-downloader" {
        crate::hbbs_http::downloader::remove(&_value);
    } else if _key == "cancel-downloader" {
        crate::hbbs_http::downloader::cancel(&_value);
    }

    #[cfg(target_os = "linux")]
    if _key == "clear-gnome-shortcuts-inhibitor-permission" {
        std::thread::spawn(move || {
            let (success, msg) =
                match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() {
                    Ok(_) => (true, "".to_owned()),
                    Err(e) => (false, e.to_string()),
                };
            let data = HashMap::from([
                (
                    "name",
                    serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"),
                ),
                ("success", serde_json::json!(success)),
                ("msg", serde_json::json!(msg)),
            ]);
            let _res = flutter::push_global_event(
                flutter::APP_TYPE_MAIN,
                serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
            );
        });
    }
}

pub fn session_set_common(session_id: SessionID, key: String, value: String) {
    if let Some(s) = sessions::get_session_by_session_id(&session_id) {
        if key == "continue-insecure-connection"
        {
            s.continue_insecure_connection(value == "Y");
            return;
        }
    }
}

pub fn session_get_common_sync(
    session_id: SessionID,
    key: String,
    param: String,
) -> SyncReturn<Option<String>> {
    SyncReturn(session_get_common(session_id, key, param))
}

pub fn session_get_common(
    session_id: SessionID,
    key: String,
    #[allow(unused_variables)] param: String,
) -> Option<String> {
    if let Some(s) = sessions::get_session_by_session_id(&session_id) {
        let v = if key == "is_screenshot_supported" {
            s.is_screenshot_supported().to_string()
        } else if key == "login_method" {
            s.lc.read().unwrap().get_login_method().to_owned()
        } else {
            "".to_owned()
        };
        Some(v)
    } else {
        None
    }
}

#[cfg(target_os = "android")]
pub mod server_side {
    use hbb_common::{config, log};
    use jni::{
        errors::{Error as JniError, Result as JniResult},
        objects::{JClass, JObject, JString},
        sys::{jboolean, jstring},
        JNIEnv,
    };

    use crate::start_server;

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_startServer(
        env: JNIEnv,
        _class: JClass,
        app_dir: JString,
        home_dir: JString,
        custom_client_config: JString,
    ) {
        log::debug!("startServer from jvm");
        let mut env = env;
        if let Ok(app_dir) = env.get_string(&app_dir) {
            *config::APP_DIR.write().unwrap() = app_dir.into();
        }
        if let Ok(home_dir) = env.get_string(&home_dir) {
            *config::APP_HOME_DIR.write().unwrap() = home_dir.into();
        }
        #[cfg(not(debug_assertions))]
        crate::platform::android_log::initialize();
        if let Ok(custom_client_config) = env.get_string(&custom_client_config) {
            if !custom_client_config.is_empty() {
                let custom_client_config: String = custom_client_config.into();
                crate::read_custom_client(&custom_client_config);
            }
        }
        std::thread::spawn(move || start_server(true));
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) {
        log::debug!("startService from jvm");
        config::Config::set_option("stop-service".into(), "".into());
        crate::rendezvous_mediator::reset_needs_deploy_notification();
        crate::rendezvous_mediator::RendezvousMediator::restart();
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_translateLocale(
        env: JNIEnv,
        _class: JClass,
        locale: JString,
        input: JString,
    ) -> jstring {
        let mut env = env;
        let res = if let (Ok(input), Ok(locale)) = (env.get_string(&input), env.get_string(&locale))
        {
            let input: String = input.into();
            let locale: String = locale.into();
            crate::client::translate_locale(input, &locale)
        } else {
            "".into()
        };
        return env.new_string(res).unwrap_or(input).into_raw();
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_refreshScreen(_env: JNIEnv, _class: JClass) {
        crate::server::video_service::refresh()
    }

    /// Close outgoing sessions when the UI goes away but the process may not,
    /// so a session cannot outlive the UI that is able to close it.
    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_closeAllSessions(_env: JNIEnv, _class: JClass) {
        let closed = crate::flutter::sessions::close_all_sessions();
        if closed > 0 {
            log::info!("closed {} outgoing session(s)", closed);
        }
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_getLocalOption(
        env: JNIEnv,
        _class: JClass,
        key: JString,
    ) -> jstring {
        let mut env = env;
        let res = if let Ok(key) = env.get_string(&key) {
            let key: String = key.into();
            super::get_local_option(key)
        } else {
            "".into()
        };
        return env.new_string(res).unwrap_or_default().into_raw();
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption(
        env: JNIEnv,
        _class: JClass,
        key: JString,
    ) -> jstring {
        let mut env = env;
        let res = if let Ok(key) = env.get_string(&key) {
            let key: String = key.into();
            super::get_builtin_option(&key)
        } else {
            "".into()
        };
        return env.new_string(res).unwrap_or_default().into_raw();
    }

    #[no_mangle]
    pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
        env: JNIEnv,
        _class: JClass,
    ) -> jboolean {
        jboolean::from(crate::server::is_clipboard_service_ok())
    }
}