diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100755 index 0000000..8eec061 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/tauri.svg b/public/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a374580..b925820 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,4 +15,5 @@ tauri = { version = "2.0.0-beta", features = ["macos-private-api"] } tauri-plugin-shell = "2.0.0-beta" serde = { version = "1", features = ["derive"] } serde_json = "1" - +anyhow = "1.0" +tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] } diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/resource/adb b/src-tauri/resource/adb new file mode 100755 index 0000000..a826255 Binary files /dev/null and b/src-tauri/resource/adb differ diff --git a/src-tauri/resource/adb.exe b/src-tauri/resource/adb.exe new file mode 100644 index 0000000..b0e26e1 Binary files /dev/null and b/src-tauri/resource/adb.exe differ diff --git a/src-tauri/resource/scrcpy-server-v2.4 b/src-tauri/resource/scrcpy-server-v2.4 new file mode 100644 index 0000000..1d867f8 Binary files /dev/null and b/src-tauri/resource/scrcpy-server-v2.4 differ diff --git a/src-tauri/src/adb.rs b/src-tauri/src/adb.rs new file mode 100644 index 0000000..dbd4b79 --- /dev/null +++ b/src-tauri/src/adb.rs @@ -0,0 +1,162 @@ +use crate::resource::{ResHelper, ResourceName}; +use std::{ + io::BufRead, + path::PathBuf, + process::{Child, Command, Stdio}, +}; + +use anyhow::{Context, Ok, Result}; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Device { + pub id: String, + pub status: String, +} + +impl Device { + /// execute "adb push" to push file from src to des + pub fn cmd_push(res_dir: &PathBuf, id: &str, src: &str, des: &str) -> Result { + let mut adb_command = Adb::cmd_base(res_dir); + let res = adb_command + .args(&["-s", id, "push", src, des]) + .output() + .with_context(|| format!("Failed to execute 'adb push {} {}'", src, des))?; + Ok(String::from_utf8(res.stdout).unwrap()) + } + + /// execute "adb reverse" to reverse the device port to local port + pub fn cmd_reverse(res_dir: &PathBuf, id: &str, remote: &str, local: &str) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["-s", id, "reverse", remote, local]) + .output() + .with_context(|| format!("Failed to execute 'adb reverse {} {}'", remote, local))?; + Ok(()) + } + + /// execute "adb forward" to forward the local port to the device + pub fn cmd_forward(res_dir: &PathBuf, id: &str, local: &str, remote: &str) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["-s", id, "forward", local, remote]) + .output() + .with_context(|| format!("Failed to execute 'adb forward {} {}'", local, remote))?; + Ok(()) + } + + /// execute "adb shell" to execute shell command on the device + pub fn cmd_shell(res_dir: &PathBuf, id: &str, shell_args: &[&str]) -> Result { + let mut adb_command = Adb::cmd_base(res_dir); + let mut args = vec!["-s", id, "shell"]; + args.extend_from_slice(shell_args); + Ok(adb_command + .args(args) + .stdout(Stdio::piped()) + .spawn() + .context("Failed to execute 'adb shell'")?) + } + + pub fn cmd_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u16, u16)> { + let mut adb_command = Adb::cmd_base(res_dir); + let output = adb_command + .args(&["-s", id, "shell", "wm", "size"]) + .output() + .context("Failed to execute 'adb shell wm size'")?; + let lines = output.stdout.lines(); + let mut size = (0, 0); + for line in lines { + if let std::result::Result::Ok(s) = line { + println!("{}", s); + if s.starts_with("Physical size:") { + let mut iter = s.split_whitespace(); + iter.next(); + iter.next(); + let mut size_str = iter.next().unwrap().split('x'); + let width = size_str.next().unwrap().parse::().unwrap(); + let height = size_str.next().unwrap().parse::().unwrap(); + size = (width, height); + break; + } + } + } + Ok(size) + } +} + +pub struct Adb; + +/// Module to execute adb command and fetch output. +/// But some output of command won't be output, like adb service startup information. +impl Adb { + fn cmd_base(res_dir: &PathBuf) -> Command { + Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb)) + } + + /// execute "adb devices" and return devices list + pub fn cmd_devices(res_dir: &PathBuf) -> Result> { + let mut adb_command = Adb::cmd_base(res_dir); + let output = adb_command + .args(&["devices"]) + .output() + .context("Failed to execute 'adb devices'")?; + + let mut devices_vec: Vec = Vec::new(); + let mut lines = output.stdout.lines(); + // skip first line + lines.next(); + + // parse string to Device + for line in lines { + if let std::result::Result::Ok(s) = line { + let device_info: Vec<&str> = s.split('\t').collect(); + if device_info.len() == 2 { + devices_vec.push(Device { + id: device_info[0].to_string(), + status: device_info[1].to_string(), + }); + } + } + } + Ok(devices_vec) + } + + /// execute "adb kill-server" + pub fn cmd_kill_server(res_dir: &PathBuf) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["kill-server"]) + .output() + .context("Failed to execute 'adb kill-server'")?; + Ok(()) + } + + /// execute "adb reverse --remove-all" + pub fn cmd_reverse_remove(res_dir: &PathBuf) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["reverse", " --remove-all"]) + .output() + .context("Failed to execute 'adb reverse --remove-all'")?; + Ok(()) + } + + /// execute "adb forward --remove-all" + pub fn cmd_forward_remove(res_dir: &PathBuf) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["forward", " --remove-all"]) + .output() + .context("Failed to execute 'adb forward --remove-all'")?; + Ok(()) + } + + /// execute "adb start-server" + pub fn cmd_start_server(res_dir: &PathBuf) -> Result<()> { + let mut adb_command = Adb::cmd_base(res_dir); + adb_command + .args(&["start-server"]) + .output() + .context("Failed to execute 'adb start-server'")?; + Ok(()) + } +} diff --git a/src-tauri/src/binary.rs b/src-tauri/src/binary.rs new file mode 100644 index 0000000..404c7be --- /dev/null +++ b/src-tauri/src/binary.rs @@ -0,0 +1,73 @@ +pub fn write_16be(buf: &mut [u8], val: u16) { + buf[0] = (val >> 8) as u8; + buf[1] = val as u8; +} + +pub fn write_32be(buf: &mut [u8], val: u32) { + buf[0] = (val >> 24) as u8; + buf[1] = (val >> 16) as u8; + buf[2] = (val >> 8) as u8; + buf[3] = val as u8; +} + +pub fn write_64be(buf: &mut [u8], val: u64) { + buf[0] = (val >> 56) as u8; + buf[1] = (val >> 48) as u8; + buf[2] = (val >> 40) as u8; + buf[3] = (val >> 32) as u8; + buf[4] = (val >> 24) as u8; + buf[5] = (val >> 16) as u8; + buf[6] = (val >> 8) as u8; + buf[7] = val as u8; +} + +pub fn float_to_u16fp(mut f: f32) -> u16 { + if f < 0.0 || f > 1.0 { + f = 1.0; + } + let mut u: u32 = (f * (1 << 16) as f32) as u32; + if u >= 0xffff { + u = 0xffff; + } + u as u16 +} + +pub fn float_to_i16fp(f: f32) -> i16 { + assert!(f >= -1.0 && f <= 1.0); + let mut i: i32 = (f * (1 << 15) as f32) as i32; + assert!(i >= -0x8000); + if i >= 0x7fff { + assert_eq!(i, 0x8000); // for f == 1.0 + i = 0x7fff; + } + i as i16 +} + +pub fn write_posion(buf: &mut [u8], x: i32, y: i32, w: u16, h: u16) { + write_32be(buf, x as u32); + write_32be(&mut buf[4..8], y as u32); + write_16be(&mut buf[8..10], w); + write_16be(&mut buf[10..12], h); +} + +pub fn write_string(utf8: &str, max_len: usize, buf: &mut Vec) { + let len = str_utf8_truncation_index(utf8, max_len) as u32; + // first 4 bytes for length + let len_bytes = len.to_be_bytes(); + buf.extend_from_slice(&len_bytes); + // then [len] bytes for the string + buf.extend_from_slice(utf8.as_bytes()) +} + +// truncate utf8 string to max_len bytes +fn str_utf8_truncation_index(utf8: &str, max_len: usize) -> usize { + let len = utf8.len(); + if len <= max_len { + return len; + } + let mut len = max_len; + while utf8.is_char_boundary(len) { + len -= 1; + } + len +} diff --git a/src-tauri/src/client.rs b/src-tauri/src/client.rs new file mode 100644 index 0000000..0f8f521 --- /dev/null +++ b/src-tauri/src/client.rs @@ -0,0 +1,125 @@ +use anyhow::{Ok, Result}; +use std::{io::BufRead, path::PathBuf}; + +use crate::{ + adb::{Adb, Device}, + resource::{ResHelper, ResourceName}, +}; + +/** + * the client of scrcpy + */ +#[derive(Debug)] +pub struct ScrcpyClient { + pub device: Device, + pub version: String, + pub scid: String, + pub port: u16, +} + +impl ScrcpyClient { + pub fn get_scrcpy_version() -> String { + ResHelper::get_scrcpy_version() + } + + pub fn adb_devices(res_dir: &PathBuf) -> Result> { + Adb::cmd_devices(res_dir) + } + + pub fn adb_restart_server(res_dir: &PathBuf) -> Result<()> { + Adb::cmd_kill_server(res_dir)?; + Adb::cmd_start_server(res_dir)?; + Ok(()) + } + + pub fn adb_reverse_remove(res_dir: &PathBuf) -> Result<()> { + Adb::cmd_reverse_remove(res_dir) + } + + pub fn adb_forward_remove(res_dir: &PathBuf) -> Result<()> { + Adb::cmd_forward_remove(res_dir) + } + + /// get the screen size of the device + pub fn get_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u16, u16)> { + Device::cmd_screen_size(res_dir, id) + } + + /// push server file to current device + pub fn push_server_file(res_dir: &PathBuf, id: &str) -> Result<()> { + let info = Device::cmd_push( + res_dir, + id, + &ResHelper::get_file_path(res_dir, ResourceName::ScrcpyServer).to_string_lossy(), + "/data/local/tmp/scrcpy-server.jar", + )?; + + println!("{}\nSuccessfully push server files", info); + Ok(()) + } + + /// forward the local port to the device + pub fn forward_server_port(res_dir: &PathBuf, id: &str, scid: &str, port: u16) -> Result<()> { + Device::cmd_forward( + res_dir, + id, + &format!("tcp:{}", port), + &format!("localabstract:scrcpy_{}", scid), + )?; + println!("Successfully forward port"); + Ok(()) + } + + /// reverse the device port to the local port + pub fn reverse_server_port(res_dir: &PathBuf, id: &str, scid: &str, port: u16) -> Result<()> { + Device::cmd_reverse( + res_dir, + id, + &format!("localabstract:scrcpy_{}", scid), + &format!("tcp:{}", port), + )?; + println!("Successfully reverse port"); + Ok(()) + } + + /// spawn a new thread to start scrcpy server + pub fn shell_start_server( + res_dir: &PathBuf, + id: &str, + scid: &str, + version: &str, + ) -> Result<()> { + let mut child = Device::cmd_shell( + res_dir, + id, + &[ + "CLASSPATH=/data/local/tmp/scrcpy-server.jar", + "app_process", + "/", + "com.genymobile.scrcpy.Server", + version, + &format!("scid={}", scid), + "tunnel_forward=true", + "video=false", + "audio=false", + ], + )?; + + println!("Starting scrcpy server..."); + let out = child.stdout.take().unwrap(); + let mut out = std::io::BufReader::new(out); + let mut s = String::new(); + + while let core::result::Result::Ok(_) = out.read_line(&mut s) { + // break at the end of program + if let core::result::Result::Ok(Some(_)) = child.try_wait() { + break; + } + print!("{}", s); + // clear string to store new line only + s.clear(); + } + println!("Scrcpy server closed"); + Ok(()) + } +} diff --git a/src-tauri/src/control_msg.rs b/src-tauri/src/control_msg.rs new file mode 100644 index 0000000..d41b883 --- /dev/null +++ b/src-tauri/src/control_msg.rs @@ -0,0 +1,203 @@ +use crate::binary; +use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf}; + +pub const SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH: usize = 300; +pub const SC_CONTROL_MSG_MAX_SIZE: usize = 1 << 18; // 256k +pub const SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH: usize = SC_CONTROL_MSG_MAX_SIZE - 14; + +pub fn gen_ctrl_msg(ctrl_msg_type: ControlMsgType, payload: &serde_json::Value) -> Vec { + match ctrl_msg_type { + ControlMsgType::ControlMsgTypeInjectKeycode => gen_inject_key_ctrl_msg( + ctrl_msg_type as u8, + payload["action"].as_u64().unwrap() as u8, + payload["keycode"].as_u64().unwrap() as u32, + payload["repeat"].as_u64().unwrap() as u32, + payload["metastate"].as_u64().unwrap() as u32, + ), + ControlMsgType::ControlMsgTypeInjectText => { + let mut buf: Vec = vec![ctrl_msg_type as u8]; + let text = payload["text"].as_str().unwrap(); + binary::write_string(text, SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &mut buf); + buf + } + ControlMsgType::ControlMsgTypeInjectTouchEvent => gen_inject_touch_ctrl_msg( + ctrl_msg_type as u8, + payload["action"].as_u64().unwrap() as u8, + payload["pointerId"].as_u64().unwrap(), + payload["position"]["x"].as_i64().unwrap() as i32, + payload["position"]["y"].as_i64().unwrap() as i32, + payload["position"]["w"].as_i64().unwrap() as u16, + payload["position"]["h"].as_i64().unwrap() as u16, + binary::float_to_u16fp(payload["pressure"].as_f64().unwrap() as f32), + payload["actionButton"].as_u64().unwrap() as u32, + payload["buttons"].as_u64().unwrap() as u32, + ), + ControlMsgType::ControlMsgTypeInjectScrollEvent => { + let mut buf = vec![0; 21]; + buf[0] = ctrl_msg_type as u8; + binary::write_posion( + &mut buf[1..13], + payload["position"]["x"].as_i64().unwrap() as i32, + payload["position"]["y"].as_i64().unwrap() as i32, + payload["position"]["w"].as_i64().unwrap() as u16, + payload["position"]["h"].as_i64().unwrap() as u16, + ); + binary::write_16be( + &mut buf[13..15], + binary::float_to_i16fp(payload["hscroll"].as_f64().unwrap() as f32) as u16, + ); + binary::write_16be( + &mut buf[15..17], + binary::float_to_i16fp(payload["vscroll"].as_f64().unwrap() as f32) as u16, + ); + binary::write_32be( + &mut buf[17..21], + payload["buttons"].as_u64().unwrap() as u32, + ); + buf + } + ControlMsgType::ControlMsgTypeBackOrScreenOn => { + vec![ + ctrl_msg_type as u8, + payload["action"].as_u64().unwrap() as u8, + ] + } + ControlMsgType::ControlMsgTypeGetClipboard => { + vec![ + ctrl_msg_type as u8, + payload["copyKey"].as_u64().unwrap() as u8, + ] + } + ControlMsgType::ControlMsgTypeSetClipboard => { + let mut buf: Vec = vec![0; 10]; + buf[0] = ctrl_msg_type as u8; + binary::write_64be(&mut buf[1..9], payload["sequence"].as_u64().unwrap()); + buf[9] = payload["paste"].as_bool().unwrap_or(false) as u8; + let text = payload["text"].as_str().unwrap(); + binary::write_string(text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, &mut buf); + buf + } + ControlMsgType::ControlMsgTypeSetScreenPowerMode => { + vec![ctrl_msg_type as u8, payload["mode"].as_u64().unwrap() as u8] + } + ControlMsgType::ControlMsgTypeUhidCreate => { + let size = payload["reportDescSize"].as_u64().unwrap() as u16; + let mut buf: Vec = vec![0; 5]; + buf[0] = ctrl_msg_type as u8; + binary::write_16be(&mut buf[1..3], payload["id"].as_u64().unwrap() as u16); + binary::write_16be(&mut buf[3..5], size); + let report_desc = payload["reportDesc"].as_array().unwrap(); + let report_desc_u8: Vec = report_desc + .iter() + .map(|x| x.as_u64().unwrap() as u8) + .collect(); + buf.extend_from_slice(&report_desc_u8); + buf + } + ControlMsgType::ControlMsgTypeUhidInput => { + let size = payload["size"].as_u64().unwrap() as u16; + let mut buf: Vec = vec![0; 5]; + buf[0] = ctrl_msg_type as u8; + binary::write_16be(&mut buf[1..3], payload["id"].as_u64().unwrap() as u16); + binary::write_16be(&mut buf[3..5], size); + let data = payload["data"].as_array().unwrap(); + let data_u8: Vec = data.iter().map(|x| x.as_u64().unwrap() as u8).collect(); + buf.extend_from_slice(&data_u8); + buf + } + // other control message types do not have a payload + _ => { + vec![ctrl_msg_type as u8] + } + } +} + +pub fn gen_inject_key_ctrl_msg( + ctrl_msg_type: u8, + action: u8, + keycode: u32, + repeat: u32, + metastate: u32, +) -> Vec { + let mut buf = vec![0; 14]; + buf[0] = ctrl_msg_type; + buf[1] = action; + binary::write_32be(&mut buf[2..6], keycode); + binary::write_32be(&mut buf[6..10], repeat); + binary::write_32be(&mut buf[10..14], metastate); + buf +} + +pub fn gen_inject_touch_ctrl_msg( + ctrl_msg_type: u8, + action: u8, + pointer_id: u64, + x: i32, + y: i32, + w: u16, + h: u16, + pressure: u16, + action_button: u32, + buttons: u32, +) -> Vec { + let mut buf = vec![0; 32]; + buf[0] = ctrl_msg_type; + buf[1] = action; + binary::write_64be(&mut buf[2..10], pointer_id); + binary::write_posion(&mut buf[10..22], x, y, w, h); + binary::write_16be(&mut buf[22..24], pressure); + binary::write_32be(&mut buf[24..28], action_button); + binary::write_32be(&mut buf[28..32], buttons); + buf +} + +pub async fn send_ctrl_msg( + ctrl_msg_type: ControlMsgType, + payload: &serde_json::Value, + writer: &mut OwnedWriteHalf, +) { + let buf = gen_ctrl_msg(ctrl_msg_type, payload); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); +} + +pub enum ControlMsgType { + ControlMsgTypeInjectKeycode, //发送原始按键 + ControlMsgTypeInjectText, //发送文本,不知道是否能输入中文(估计只是把文本转为keycode的输入效果) + ControlMsgTypeInjectTouchEvent, //发送触摸事件 + ControlMsgTypeInjectScrollEvent, //发送滚动事件(类似接入鼠标后滚动滚轮的效果,不是通过触摸实现的) + ControlMsgTypeBackOrScreenOn, //应该就是发送返回键 + ControlMsgTypeExpandNotificationPanel, //打开消息面板 + ControlMsgTypeExpandSettingsPanel, //打开设置面板(就是消息面板右侧的) + ControlMsgTypeCollapsePanels, //折叠上述面板 + ControlMsgTypeGetClipboard, //获取剪切板 + ControlMsgTypeSetClipboard, //设置剪切板 + ControlMsgTypeSetScreenPowerMode, //设置屏幕电源模式,是关闭设备屏幕的(SC_SCREEN_POWER_MODE_OFF 和 SC_SCREEN_POWER_MODE_NORMAL ) + ControlMsgTypeRotateDevice, //旋转设备屏幕 + ControlMsgTypeUhidCreate, //创建虚拟设备?从而模拟真实的键盘、鼠标用的,目前没用 + ControlMsgTypeUhidInput, //同上转发键盘、鼠标的输入,目前没用 + ControlMsgTypeOpenHardKeyboardSettings, //打开设备的硬件键盘设置,目前没用 +} + +impl ControlMsgType { + pub fn from_i64(value: i64) -> Option { + match value { + 0 => Some(Self::ControlMsgTypeInjectKeycode), + 1 => Some(Self::ControlMsgTypeInjectText), + 2 => Some(Self::ControlMsgTypeInjectTouchEvent), + 3 => Some(Self::ControlMsgTypeInjectScrollEvent), + 4 => Some(Self::ControlMsgTypeBackOrScreenOn), + 5 => Some(Self::ControlMsgTypeExpandNotificationPanel), + 6 => Some(Self::ControlMsgTypeExpandSettingsPanel), + 7 => Some(Self::ControlMsgTypeCollapsePanels), + 8 => Some(Self::ControlMsgTypeGetClipboard), + 9 => Some(Self::ControlMsgTypeSetClipboard), + 10 => Some(Self::ControlMsgTypeSetScreenPowerMode), + 11 => Some(Self::ControlMsgTypeRotateDevice), + 12 => Some(Self::ControlMsgTypeUhidCreate), + 13 => Some(Self::ControlMsgTypeUhidInput), + 14 => Some(Self::ControlMsgTypeOpenHardKeyboardSettings), + _ => None, + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..e455c42 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,7 @@ +pub mod adb; +pub mod resource; +pub mod client; +pub mod socket; +pub mod binary; +pub mod control_msg; +pub mod scrcpy_mask_cmd; \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 96361fb..5ce3007 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,16 +1,145 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +use scrcpy_mask::{ + adb::{Adb, Device}, + client::ScrcpyClient, + resource::ResHelper, + socket::connect_socket, +}; +use std::sync::Arc; +use tauri::Manager; + #[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) +/// get devices info list +fn adb_devices(app: tauri::AppHandle) -> Result, String> { + let dir = app.path().resource_dir().unwrap().join("resource"); + match Adb::cmd_devices(&dir) { + Ok(devices) => Ok(devices), + Err(e) => Err(e.to_string()), + } } -fn main() { +#[tauri::command] +/// get screen size of the device +fn get_screen_size(id: String, app: tauri::AppHandle) -> Result<(u16, u16), String> { + let dir = app.path().resource_dir().unwrap().join("resource"); + match ScrcpyClient::get_screen_size(&dir, &id) { + Ok(size) => Ok(size), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +/// forward local port to the device port +fn forward_server_port( + app: tauri::AppHandle, + id: String, + scid: String, + port: u16, +) -> Result<(), String> { + let dir = app.path().resource_dir().unwrap().join("resource"); + + match ScrcpyClient::forward_server_port(&dir, &id, &scid, port) { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +/// push scrcpy-server file to the device +fn push_server_file(id: String, app: tauri::AppHandle) -> Result<(), String> { + let dir = app.path().resource_dir().unwrap().join("resource"); + match ScrcpyClient::push_server_file(&dir, &id) { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +/// start scrcpy server and connect to it +fn start_scrcpy_server( + id: String, + scid: String, + address: String, + app: tauri::AppHandle, +) -> Result<(), String> { + let dir = app.path().resource_dir().unwrap().join("resource"); + let version = ScrcpyClient::get_scrcpy_version(); + + // start scrcpy server + tokio::spawn(async move { + ScrcpyClient::shell_start_server(&dir, &id, &scid, &version).unwrap(); + }); + + // connect to scrcpy server + tokio::spawn(async move { + // wait 1 second for scrcpy-server to start + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let app = Arc::new(app); + + // create channel to transmit device reply to front + let share_app = app.clone(); + let (device_reply_sender, mut device_reply_receiver) = + tokio::sync::mpsc::channel::(16); + println!("device reply channel created"); + tokio::spawn(async move { + while let Some(reply) = device_reply_receiver.recv().await { + share_app.emit("device-reply", reply).unwrap(); + } + println!("device reply channel closed"); + }); + + // create channel to transmit front msg to TcpStream handler + let (front_msg_sender, front_msg_receiver) = tokio::sync::mpsc::channel::(16); + let share_app = app.clone(); + let listen_handler = share_app.listen("front-command", move |event| { + let sender = front_msg_sender.clone(); + println!("收到front-command: {}", event.payload()); + tokio::spawn(async move { + if let Err(e) = sender.send(event.payload().into()).await { + println!("front-command转发失败: {}", e); + }; + }); + }); + + // connect + let share_app = app.clone(); + tokio::spawn(connect_socket( + address, + front_msg_receiver, + device_reply_sender, + listen_handler, + share_app, + )); + }); + + Ok(()) +} + +#[tokio::main] +async fn main() { tauri::Builder::default() + .setup(|app| { + // check resource files + ResHelper::res_init( + &app.path() + .resource_dir() + .expect("failed to find resource") + .join("resource"), + ) + .unwrap(); + Ok(()) + }) .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![greet]) + .invoke_handler(tauri::generate_handler![ + adb_devices, + get_screen_size, + forward_server_port, + push_server_file, + start_scrcpy_server + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/resource.rs b/src-tauri/src/resource.rs new file mode 100644 index 0000000..86caefc --- /dev/null +++ b/src-tauri/src/resource.rs @@ -0,0 +1,41 @@ +use anyhow::{anyhow, Ok, Result}; +use std::path::PathBuf; + +pub enum ResourceName { + Adb, + ScrcpyServer, +} + +pub struct ResHelper { + pub res_dir: PathBuf, +} + +impl ResHelper { + pub fn res_init(res_dir: &PathBuf) -> Result<()> { + for name in [ResourceName::Adb, ResourceName::ScrcpyServer] { + let file_path = ResHelper::get_file_path(res_dir, name); + if !file_path.exists() { + return Err(anyhow!(format!( + "Resource missing! {}", + file_path.to_str().unwrap() + ))); + } + } + + Ok(()) + } + pub fn get_file_path(dir: &PathBuf, file_name: ResourceName) -> PathBuf { + match file_name { + #[cfg(target_os = "windows")] + ResourceName::Adb => dir.join("adb.exe"), + #[cfg(not(target_os = "windows"))] + ResourceName::Adb => dir.join("adb"), + + ResourceName::ScrcpyServer => dir.join("scrcpy-server-v2.4"), + } + } + + pub fn get_scrcpy_version() -> String { + String::from("2.4") + } +} diff --git a/src-tauri/src/scrcpy_mask_cmd.rs b/src-tauri/src/scrcpy_mask_cmd.rs new file mode 100644 index 0000000..e10d8ed --- /dev/null +++ b/src-tauri/src/scrcpy_mask_cmd.rs @@ -0,0 +1,296 @@ +use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf}; + +use crate::{ + binary, + control_msg::{gen_inject_key_ctrl_msg, gen_inject_touch_ctrl_msg, ControlMsgType}, +}; + +pub async fn handle_sm_cmd( + cmd_type: ScrcpyMaskCmdType, + payload: &serde_json::Value, + writer: &mut OwnedWriteHalf, +) { + match cmd_type { + ScrcpyMaskCmdType::SendKey => { + let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectKeycode as u8; + let keycode = payload["keycode"].as_u64().unwrap() as u32; + let metastate = match payload.get("metastate") { + Some(metastate) => metastate.as_u64().unwrap() as u32, + None => 0, // AMETA_NONE + }; + match payload["action"].as_u64().unwrap() { + // default + 0 => { + // down + let buf = gen_inject_key_ctrl_msg( + ctrl_msg_type, + 0, // AKEY_EVENT_ACTION_DOWN + keycode, + 0, + metastate, + ); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + // up + let buf = gen_inject_key_ctrl_msg( + ctrl_msg_type, + 0, // AKEY_EVENT_ACTION_DOWN + keycode, + 0, + metastate, + ); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); + } + // down + 1 => { + let buf = gen_inject_key_ctrl_msg( + ctrl_msg_type, + 1, // AKEY_EVENT_ACTION_UP + keycode, + 0, + metastate, + ); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); + } + // up + 2 => { + let buf = gen_inject_key_ctrl_msg( + ctrl_msg_type, + 1, // AKEY_EVENT_ACTION_UP + keycode, + 0, + metastate, + ); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); + } + _ => {} + }; + } + ScrcpyMaskCmdType::Touch => { + let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectTouchEvent as u8; + let pointer_id = payload["pointerId"].as_u64().unwrap(); + let w = payload["screen"]["w"].as_u64().unwrap() as u16; + let h = payload["screen"]["h"].as_u64().unwrap() as u16; + let x = payload["pos"]["x"].as_i64().unwrap() as i32; + let y = payload["pos"]["y"].as_i64().unwrap() as i32; + match payload["action"].as_u64().unwrap() { + // default + 0 => { + // down + touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await; + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + // up + touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await; + } + // down + 1 => { + touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await; + } + // up + 2 => { + touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await; + } + // move + 3 => { + touch(ctrl_msg_type, pointer_id, x, y, w, h, 2, writer).await; + } + _ => {} + } + } + ScrcpyMaskCmdType::Swipe => { + let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectTouchEvent as u8; + let pointer_id = payload["pointerId"].as_u64().unwrap(); + let w = payload["screen"]["w"].as_u64().unwrap() as u16; + let h = payload["screen"]["h"].as_u64().unwrap() as u16; + let pos_arr = payload["pos"].as_array().unwrap(); + let pos_arr: Vec<(i32, i32)> = pos_arr + .iter() + .map(|pos| { + ( + pos["x"].as_i64().unwrap() as i32, + pos["y"].as_i64().unwrap() as i32, + ) + }) + .collect(); + let interval_between_pos = payload["intervalBetweenPos"].as_u64().unwrap(); + match payload["action"].as_u64().unwrap() { + // default + 0 => { + swipe( + ctrl_msg_type, + pointer_id, + w, + h, + pos_arr, + interval_between_pos, + writer, + true, + true, + ) + .await; + } + // no up + 1 => { + swipe( + ctrl_msg_type, + pointer_id, + w, + h, + pos_arr, + interval_between_pos, + writer, + true, + false, + ) + .await; + } + // no down + 2 => { + swipe( + ctrl_msg_type, + pointer_id, + w, + h, + pos_arr, + interval_between_pos, + writer, + false, + true, + ) + .await; + } + _ => {} + }; + } + ScrcpyMaskCmdType::Shutdown => {} + } +} + +pub async fn touch( + ctrl_msg_type: u8, + pointer_id: u64, + x: i32, + y: i32, + w: u16, + h: u16, + action: u8, // 0: down, 1: up, 2: move + writer: &mut OwnedWriteHalf, +) { + let pressure = binary::float_to_u16fp(0.8); + let action_button: u32 = 1; + let buttons: u32 = 1; + let buf = gen_inject_touch_ctrl_msg( + ctrl_msg_type, + action, + pointer_id, + x, + y, + w, + h, + pressure, + action_button, + buttons, + ); + writer.write_all(&buf).await.unwrap(); + writer.flush().await.unwrap(); +} + +/// Determine the number of segments based on the distance between two points +fn get_divide_num(x1: i32, y1: i32, x2: i32, y2: i32, segment_length: i32) -> i32 { + let dx = (x2 - x1).abs(); + let dy = (y2 - y1).abs(); + let d = (dx.pow(2) + dy.pow(2)) as f64; + let d = d.sqrt(); + let divide_num = (d / segment_length as f64).ceil() as i32; + divide_num +} + +pub async fn swipe( + ctrl_msg_type: u8, + pointer_id: u64, + w: u16, + h: u16, + pos_arr: Vec<(i32, i32)>, + interval_between_pos: u64, + writer: &mut OwnedWriteHalf, + down_flag: bool, + up_flag: bool, +) { + // down + if down_flag { + touch( + ctrl_msg_type, + pointer_id, + pos_arr[0].0, + pos_arr[0].1, + w, + h, + 0, + writer, + ) + .await; + } + + // move + let mut cur_index = 1; + while cur_index < pos_arr.len() { + let (x, y) = pos_arr[cur_index]; + let (prev_x, prev_y) = pos_arr[cur_index - 1]; + // divide it into several segments + let segment_length = 100; + let divide_num = get_divide_num(prev_x, prev_y, x, y, segment_length); + let dx = (x - prev_x) / divide_num; + let dy = (y - prev_y) / divide_num; + let d_interval = interval_between_pos / (divide_num as u64); + + for i in 1..divide_num + 1 { + let nx = prev_x + dx * i; + let ny = prev_y + dy * i; + touch(ctrl_msg_type, pointer_id, nx, ny, w, h, 2, writer).await; + if d_interval > 0 { + tokio::time::sleep(tokio::time::Duration::from_millis(d_interval)).await; + } + } + + cur_index += 1; + } + + // up + if up_flag { + touch( + ctrl_msg_type, + pointer_id, + pos_arr[pos_arr.len() - 1].0, + pos_arr[pos_arr.len() - 1].1, + w, + h, + 1, + writer, + ) + .await; + } +} + +#[derive(Debug)] +pub enum ScrcpyMaskCmdType { + SendKey, + Touch, + Swipe, + Shutdown, +} + +impl ScrcpyMaskCmdType { + pub fn from_i64(value: i64) -> Option { + match value { + 15 => Some(Self::SendKey), + 16 => Some(Self::Touch), + 17 => Some(Self::Swipe), + 18 => Some(Self::Shutdown), + _ => None, + } + } +} diff --git a/src-tauri/src/socket.rs b/src-tauri/src/socket.rs new file mode 100644 index 0000000..4e9420a --- /dev/null +++ b/src-tauri/src/socket.rs @@ -0,0 +1,225 @@ +use std::sync::Arc; + +use anyhow::Context; +use serde_json::json; +use tokio::{ + io::AsyncReadExt, + net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpStream, + }, +}; + +use crate::{ + control_msg::{self, ControlMsgType}, + scrcpy_mask_cmd::{self, ScrcpyMaskCmdType}, +}; + +pub async fn connect_socket( + address: String, + front_msg_receiver: tokio::sync::mpsc::Receiver, + device_reply_sender: tokio::sync::mpsc::Sender, + listen_handler: u32, + app: Arc, +) -> anyhow::Result<()> { + + let client = TcpStream::connect(address) + .await + .context("Socket connect failed")?; + + println!("成功连接scrcpy-server:{:?}", client.local_addr()); + + let (read_half, write_half) = client.into_split(); + + // 开启线程读取设备发送的信息,并通过通道传递到与前端通信的线程,最后与前端通信的线程发送全局事件,告知前端设备发送的信息 + tokio::spawn(async move { + read_socket(read_half, device_reply_sender).await; + }); + + // 开启线程接收通道消息,其中通道消息来自前端发送的事件 + tokio::spawn(async move { + recv_front_msg(write_half, front_msg_receiver, listen_handler, app).await; + }); + anyhow::Ok(()) +} + +// 从客户端读取 +async fn read_socket( + mut reader: OwnedReadHalf, + device_reply_sender: tokio::sync::mpsc::Sender, +) { + // read dummy byte + let mut buf: [u8; 1] = [0; 1]; + if let Err(_e) = reader.read_exact(&mut buf).await { + eprintln!("failed to read dummy byte"); + return; + } + + // read metadata (device name) + let mut buf: [u8; 64] = [0; 64]; + match reader.read(&mut buf).await { + Err(_e) => { + eprintln!("failed to read metadata"); + return; + } + Ok(0) => { + eprintln!("failed to read metadata"); + return; + } + Ok(n) => { + let mut end = n; + while buf[end - 1] == 0 { + end -= 1; + } + let device_name = std::str::from_utf8(&buf[..end]).unwrap(); + let msg = json!({ + "type": "MetaData", + "deviceName": device_name, + }) + .to_string(); + device_reply_sender.send(msg).await.unwrap(); + } + }; + + loop { + match reader.read_u8().await { + Err(e) => { + eprintln!( + "Failed to read from scrcpy server, maybe it was closed. Error:{}", + e + ); + println!("Drop TcpStream reader"); + drop(reader); + return; + } + Ok(message_type) => { + let message_type = match DeviceMsgType::from_u8(message_type) { + Some(t) => t, + None => { + println!("Ignore unkonw message type: {}", message_type); + continue; + } + }; + if let Err(e) = + handle_device_message(message_type, &mut reader, &device_reply_sender).await + { + eprintln!("Failed to handle device message: {}", e); + } + } + } + } +} + +async fn handle_device_message( + message_type: DeviceMsgType, + reader: &mut OwnedReadHalf, + device_reply_sender: &tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + match message_type { + // 设备剪切板变动 + DeviceMsgType::DeviceMsgTypeClipboard => { + let text_length = reader.read_u32().await?; + let mut buf: Vec = vec![0; text_length as usize]; + reader.read_exact(&mut buf).await?; + let cb = String::from_utf8(buf)?; + let msg = json!({ + "type": "ClipboardChanged", + "clipboard": cb + }) + .to_string(); + device_reply_sender.send(msg).await?; + } + // 设备剪切板设置成功的回复 + DeviceMsgType::DeviceMsgTypeAckClipboard => { + let sequence = reader.read_u64().await?; + let msg = json!({ + "type": "ClipboardSetAck", + "sequence": sequence + }) + .to_string(); + device_reply_sender.send(msg).await?; + } + // 虚拟设备输出,仅读取但不做进一步处理 + DeviceMsgType::DeviceMsgTypeUhidOutput => { + let _id = reader.read_u16().await?; + let size = reader.read_u16().await?; + let mut buf: Vec = vec![0; size as usize]; + reader.read_exact(&mut buf).await?; + } + }; + anyhow::Ok(()) +} + +// 接收前端发送的消息,执行相关操作 +async fn recv_front_msg( + mut write_half: OwnedWriteHalf, + mut front_msg_receiver: tokio::sync::mpsc::Receiver, + listen_handler: u32, + app: Arc, +) { + while let Some(msg) = front_msg_receiver.recv().await { + match serde_json::from_str::(&msg) { + Err(_e) => { + println!("无法解析的Json数据: {}", msg); + } + Ok(payload) => { + if let Some(front_msg_type) = payload["msgType"].as_i64() { + // 发送原始控制信息 + if front_msg_type >= 0 && front_msg_type <= 14 { + let ctrl_msg_type = ControlMsgType::from_i64(front_msg_type).unwrap(); + control_msg::send_ctrl_msg( + ctrl_msg_type, + &payload["msgData"], + &mut write_half, + ) + .await; + println!("控制信息发送完成!"); + continue; + } else { + // 处理Scrcpy Mask命令 + if let Some(cmd_type) = ScrcpyMaskCmdType::from_i64(front_msg_type) { + if let ScrcpyMaskCmdType::Shutdown = cmd_type { + drop(write_half); + println!("Drop TcpStream writer"); + app.unlisten(listen_handler); + println!("front msg channel closed"); + return; + } + + scrcpy_mask_cmd::handle_sm_cmd( + cmd_type, + &payload["msgData"], + &mut write_half, + ) + .await + } + } + } + else{ + eprintln!("fc-command非法"); + eprintln!("{:?}", payload); + } + } + }; + } + + println!("font msg channel closed"); +} + +#[derive(Debug)] +enum DeviceMsgType { + DeviceMsgTypeClipboard, + DeviceMsgTypeAckClipboard, + DeviceMsgTypeUhidOutput, +} + +impl DeviceMsgType { + fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::DeviceMsgTypeClipboard), + 1 => Some(Self::DeviceMsgTypeAckClipboard), + 2 => Some(Self::DeviceMsgTypeUhidOutput), + _ => None, + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a52e350..2e4dcaf 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,7 +25,10 @@ "active": true, "targets": "all", "icon": [ - "icons/32x32.png" + "icons/icon.icns" + ], + "resources":[ + "resource/*" ] } } diff --git a/src/App.vue b/src/App.vue index 91f7b0f..7b4a681 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,52 +1,40 @@ - diff --git a/src/assets/vue.svg b/src/assets/vue.svg deleted file mode 100644 index 770e9d3..0000000 --- a/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Device.vue b/src/components/Device.vue new file mode 100644 index 0000000..21b9381 --- /dev/null +++ b/src/components/Device.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/src/components/Greet.vue b/src/components/Greet.vue deleted file mode 100644 index d05167c..0000000 --- a/src/components/Greet.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/src/components/Header.vue b/src/components/Header.vue new file mode 100644 index 0000000..8fde180 --- /dev/null +++ b/src/components/Header.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/Mask.vue b/src/components/Mask.vue new file mode 100644 index 0000000..03f51db --- /dev/null +++ b/src/components/Mask.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue new file mode 100644 index 0000000..1f82b67 --- /dev/null +++ b/src/components/Sidebar.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/src/components/keyboard/KeyBoard.vue b/src/components/keyboard/KeyBoard.vue new file mode 100644 index 0000000..21adaf7 --- /dev/null +++ b/src/components/keyboard/KeyBoard.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/setting/Basic.vue b/src/components/setting/Basic.vue new file mode 100644 index 0000000..a0c95d4 --- /dev/null +++ b/src/components/setting/Basic.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/components/setting/Mask.vue b/src/components/setting/Mask.vue new file mode 100644 index 0000000..e09d92a --- /dev/null +++ b/src/components/setting/Mask.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/components/setting/Script.vue b/src/components/setting/Script.vue new file mode 100644 index 0000000..0d6bfea --- /dev/null +++ b/src/components/setting/Script.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/src/components/setting/Setting.vue b/src/components/setting/Setting.vue new file mode 100644 index 0000000..6e85ace --- /dev/null +++ b/src/components/setting/Setting.vue @@ -0,0 +1,45 @@ + + +