using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace NScript.AndroidBot
{
public class Server
{
private static HashSet PortsUsed { get; set; } = new HashSet();
public static bool IsPortUsed(UInt16 port)
{
lock (PortsUsed)
return PortsUsed.Contains(port);
}
public static void SetPortUsed(UInt16 port)
{
lock (PortsUsed)
PortsUsed.Add(port);
}
public static void SetPortUnused(UInt16 port)
{
lock (PortsUsed)
PortsUsed.Remove(port);
}
public const String SCRCPY_SOCKET_NAME = "scrcpy";
public const String SCRCPY_SERVER_FILENAME = "scrcpy-server";
public const String SCRCPY_DEVICE_SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
public const String DEFAULT_SERVER_PATH = "/share/scrcpy/" + SCRCPY_SERVER_FILENAME;
public const String SCRCPY_VERSION = "1.18";
public const String SNDCPY_SOCKET_NAME = "sndcpy";
public const String SNDCPY_LOCAL_PATH = "./tools/sndcpy.apk";
public const String SNDCPY_DEVICE_SERVER_PATH = "/data/local/tmp/sndcpy.adk";
static readonly IPAddress IPV4_LOCALHOST = IPAddress.Parse("127.0.0.1");
const int DEVICE_NAME_FIELD_LENGTH = 64;
public String Serial { get; set; }
public Object syncRoot = new object();
public ProcessSession ScrcpyServer;
public Socket VideoSocket;
public Socket ControlSocket;
public UInt16 LocalScrcpyServerPort; // selected from port_range
public ProcessSession SndcpyServer;
public Socket AudioSocket;
public UInt16 LocalSndcpyServerPort = 28200;
public String DeviceName { get; private set; }
public System.Drawing.Size FrameSize { get; private set; }
public Action OnMsg { get; set; }
public Action OnVideoSocketConnected { get; set; }
public Action OnAudioSocketConnected { get; set; }
public Action OnControlSocketConnected { get; set; }
private AtxAgentServer AtxAgent { get; set; }
public double GetScale()
{
if (FrameSize == default(System.Drawing.Size) || ScreenHeight == 0) return 1;
return FrameSize.Height / (double) ScreenHeight;
}
public string GetScrcpyServerDeviceFileName()
{
return SCRCPY_SERVER_FILENAME;
}
public bool PushScrcpyServerToDevice()
{
String deviceFilePath = GetScrcpyServerDeviceFileName();
if (deviceFilePath == null)
{
return false;
}
ProcessSession process = AdbUtils.adb_push(Serial, deviceFilePath, SCRCPY_DEVICE_SERVER_PATH);
process.OnMsg = process.OnErr = this.OnMsg;
return AdbUtils.process_check_success(process, "adb push", true);
}
public bool DisableTunnelForward(String serial, UInt16 local_port)
{
SetPortUnused(local_port);
ProcessSession process = AdbUtils.adb_forward_remove(serial, local_port);
process.OnMsg = process.OnErr = this.OnMsg;
return AdbUtils.process_check_success(process, "adb forward --remove", true);
}
public bool EnableTunnelForward(String serial, UInt16 localPort, String remoteSocketName)
{
if (IsPortUsed(localPort)) return false;
ProcessSession process = AdbUtils.adb_forward(serial, localPort, remoteSocketName);
process.OnMsg = process.OnErr = this.OnMsg;
bool rtn = AdbUtils.process_check_success(process, "adb forward", true);
SetPortUsed(localPort);
return rtn;
}
public bool EnableTunnelForward(String serial, UInt16 localPort, UInt16 remotePort)
{
if (IsPortUsed(localPort)) return false;
ProcessSession process = AdbUtils.adb_forward(serial, localPort, remotePort);
process.OnMsg = process.OnErr = this.OnMsg;
bool rtn = AdbUtils.process_check_success(process, "adb forward", true);
SetPortUsed(localPort);
return rtn;
}
public void LOGE(String msg)
{
OnMsg?.Invoke(msg);
}
public void LOGW(String msg)
{
OnMsg?.Invoke(msg);
}
public void LOGD(String msg)
{
OnMsg?.Invoke(msg);
}
///
/// 将 UI 界面 dump 出来。结果为 xml 布局文件。
///
///
public String DumpUI()
{
String ui = null;
try
{
ui = this.AtxAgent.DumpUI();
}
catch(WebException ex)
{
// 再重联一次
this.ConnectAtxAgent();
ui = this.AtxAgent.DumpUI();
}
return ui;
}
internal static bool IsConnected(Socket socket)
{
if (socket == null) return false;
if (socket.Poll(-1, SelectMode.SelectError) == false) return true;
//if (socket.Poll(-1, SelectMode.SelectRead) || socket.Available > 0) return true;
else return false;
}
private void RunDaemon()
{
Task.Run(() => {
while(true)
{
System.Threading.Thread.Sleep(1000);
bool isVideoSocketConnected = IsConnected(VideoSocket);
bool isControlSocketConnected = IsConnected(ControlSocket);
bool isAudioSocketConnected = IsConnected(AudioSocket);
if(isVideoSocketConnected == false || isControlSocketConnected == false)
{
OnMsg?.Invoke($"[{Serial}][Daemon]: VideoSocket-{isVideoSocketConnected},ControlSocket-{isControlSocketConnected}");
ConnectScrcpy();
}
if(isAudioSocketConnected == false)
{
OnMsg?.Invoke($"[{Serial}][Daemon]: AudioSocket-{isAudioSocketConnected},ControlSocket-{isControlSocketConnected}");
ConnectSndcpy();
}
}
});
}
public void CreateAtxAgentServer(UInt16 portStart)
{
for(UInt16 port = portStart; port < 62300; port ++)
{
if (EnableTunnelForward(this.Serial, port, 7912)) // Remote Port: 7912
{
AtxAgent = new AtxAgentServer(port);
return;
}
}
LOGE($"CreateAtxAgentServer failed, could not forward UIAutomator port");
}
public bool EnableScrcpyForward(UInt16 portStart)
{
for (UInt16 port = portStart; port < 62300; port++)
{
if (EnableTunnelForward(this.Serial, port, SCRCPY_SOCKET_NAME))
{
this.LocalScrcpyServerPort = port;
return true;
}
}
LOGE($"Could not forward Scrcpy port");
return false;
}
public ProcessSession RunScrcpyServer(ServerParams serverParams)
{
String[] cmd = {
"shell",
"CLASSPATH=" + SCRCPY_DEVICE_SERVER_PATH,
"app_process",
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
AdbUtils.log_level_to_server_string(serverParams.log_level),
serverParams.max_size.ToString(),
serverParams.bit_rate.ToString(),
serverParams.max_fps.ToString(),
serverParams.lock_video_orientation.ToString(),
"true", // true - forward, false - reverse
serverParams.crop != null ? serverParams.crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
serverParams.control? "true" : "false",
serverParams.display_id.ToString(),
serverParams.show_touches ? "true" : "false",
serverParams.stay_awake ? "true" : "false",
serverParams.codec_options != null ? serverParams.codec_options : "-",
serverParams.encoder_name != null ? serverParams.encoder_name : "-",
serverParams.power_off_on_close ? "true" : "false",
};
return AdbUtils.adb_execute(Serial, cmd);
}
public int ScreenWidth { get; private set; }
public int ScreenHeight { get; private set; }
public void QueryScreenSize()
{
ProcessSession session = AdbUtils.adb_execute(Serial, new string[] { "shell","wm","size" });
session.OnMsg = (msg) => {
String[] terms = msg.Split(":");
if(terms.Length == 2)
{
terms = terms[1].Split("x");
if(terms.Length == 2)
{
String wStr = terms[0].Trim();
String hStr = terms[1].Trim();
int width = 0;
int height = 0;
int.TryParse(wStr, out width);
int.TryParse(hStr, out height);
ScreenWidth = width;
ScreenHeight = height;
}
}
};
session.Run();
}
public Socket ConnectAndReadByte(UInt16 port)
{
Socket socket = NetUtils.Connect(IPV4_LOCALHOST, port);
byte[] bytes = new byte[1];
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
if (socket.Receive(new Span(bytes)) != 1)
{
socket.Close();
// the server is not listening yet behind the adb tunnel
return null;
}
return socket;
}
public Socket ConnectToServer(UInt16 port, UInt32 attempts, UInt32 delay /*ms*/)
{
do
{
LOGD($"Remaining connection attempts: {attempts}");
Socket socket = ConnectAndReadByte(port);
if (socket != null)
{
// it worked!
return socket;
}
if (attempts > 0)
{
Thread.Sleep((int)delay);
}
} while (--attempts > 0);
return null;
}
private ServerParams StartServerParams;
public bool Start(ServerParams serverParams)
{
StartServerParams = serverParams;
this.Serial = serverParams.serial;
QueryScreenSize();
ConnectScrcpy();
ConnectSndcpy();
ConnectAtxAgent();
RunDaemon();
return true;
}
private void ConnectAtxAgent()
{
// 检查是否安装 AtxAgent
if (this.IsAtxAgentInstalled() == false)
{
this.InstallAtxAgent();
}
if (this.AtxAgent != null)
{
this.DisableTunnelForward(this.Serial, this.AtxAgent.Port);
}
this.CreateAtxAgentServer(this.StartServerParams.port_start);
if (this.AtxAgent.IsRunning() == false)
{
StopAtxAgent();
StartAtxAgent();
if (this.AtxAgent.WaitRunning(5000) == false)
{
this.InstallAtxAgent();
}
this.AtxAgent.WaitRunning(30000);
}
}
private void ConnectScrcpy()
{
lock (this)
{
this.StopScrcpySockets();
this.PushScrcpyServerToDevice();
this.EnableScrcpyForward(StartServerParams.port_start);
this.ScrcpyServer = this.RunScrcpyServer(StartServerParams);
this.ScrcpyServer.OnMsg = this.ScrcpyServer.OnErr = this.OnMsg;
this.ScrcpyServer.RunAtBackground();
try
{
ConnectScrcpySockets();
}
catch(Exception ex)
{
this.PushScrcpyServerToDevice();
this.EnableScrcpyForward(StartServerParams.port_start);
this.ScrcpyServer = this.RunScrcpyServer(StartServerParams);
this.ScrcpyServer.OnMsg = this.ScrcpyServer.OnErr = this.OnMsg;
this.ScrcpyServer.RunAtBackground();
ConnectScrcpySockets();
}
}
}
private void ConnectSndcpy()
{
lock (this)
{
this.StopSndcpySockets();
this.InstallSndcpy();
this.EnableSndcpyForward(StartServerParams.audio_port_start);
this.SndcpyServer = this.RunSndcpyServer(StartServerParams);
this.SndcpyServer.OnMsg = this.SndcpyServer.OnErr = this.OnMsg;
this.SndcpyServer.RunAtBackground();
try
{
ConnectSndcpySockets();
}
catch (Exception ex)
{
this.InstallSndcpy();
this.EnableSndcpyForward(StartServerParams.audio_port_start);
this.SndcpyServer = this.RunSndcpyServer(StartServerParams);
this.SndcpyServer.OnMsg = this.SndcpyServer.OnErr = this.OnMsg;
this.SndcpyServer.RunAtBackground();
ConnectSndcpySockets();
}
}
}
#region Scrcpy Methods
private void ConnectScrcpySockets() // server_connect_to()
{
UInt32 attempts = 1000;
UInt32 delay = 100; // ms
this.VideoSocket = this.ConnectToServer(LocalScrcpyServerPort, attempts, delay);
this.ControlSocket = NetUtils.Connect(IPV4_LOCALHOST, this.LocalScrcpyServerPort);
if (this.VideoSocket != null)
{
// The sockets will be closed on stop if device_read_info() fails
(string deviceName, int width, int height) = ReadDeviceInfo(this.VideoSocket);
this.DeviceName = deviceName;
this.FrameSize = new System.Drawing.Size(width, height);
OnVideoSocketConnected?.Invoke();
}
if (this.ControlSocket != null)
{
OnControlSocketConnected?.Invoke();
}
}
public void StopScrcpySockets()
{
//this.VideoSocket?.Close();
//this.ControlSocket?.Close();
this.DisableTunnelForward(Serial, LocalScrcpyServerPort);
this.ScrcpyServer?.Kill();
}
public (string, int, int) ReadDeviceInfo(Socket socket)
{
byte[] buf = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
if (NetUtils.RecvAll(socket, buf) < DEVICE_NAME_FIELD_LENGTH + 4)
{
throw new BotException("Could not retrieve device information");
}
int byteCount = 0;
for (int i = 0; i < buf.Length; i++)
{
if (buf[i] == '\0')
{
byteCount = i;
break;
}
}
String deviceName = System.Text.Encoding.ASCII.GetString(buf, 0, byteCount);
int width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8)
| buf[DEVICE_NAME_FIELD_LENGTH + 1];
int height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[DEVICE_NAME_FIELD_LENGTH + 3];
return (deviceName, width, height);
}
#endregion
#region AtxAgent Methods
static String AtxListenAddr = "127.0.0.1:7912";
string AtxAgentPath = "/data/local/tmp/atx-agent";
internal bool IsAtxAgentInstalled()
{
String rtn = AdbUtils.RunShell(Serial, "du", "-h", AtxAgentPath);
return rtn.StartsWith("du:") == false;
}
internal void InstallAtxAgent()
{
this.OnMsg?.Invoke($"[{Serial}] install atx agent");
InstallUIAutomatorApks();
StopAtxAgent();
this.OnMsg?.Invoke($"[{Serial}] adb shell pm install -r -t /data/local/tmp/atx-agent");
AdbUtils.RunShell(Serial, "pm", "install", "-r", "-t", Push("./tools/atx-agent"));
StartAtxAgent();
}
internal void InstallUIAutomatorApks()
{
this.OnMsg?.Invoke($"[{Serial}] adb shell pm uninstall com.github.uiautomator");
this.OnMsg?.Invoke($"[{Serial}] adb shell pm uninstall com.github.uiautomator.test");
this.OnMsg?.Invoke($"[{Serial}] adb shell pm install -r -t /data/local/tmp/app-uiautomator.apk");
this.OnMsg?.Invoke($"[{Serial}] adb shell pm install -r -t /data/local/tmp/app-uiautomator-test.apk");
AdbUtils.RunShell(Serial, "pm", "uninstall", "com.github.uiautomator");
AdbUtils.RunShell(Serial, "pm", "uninstall", "com.github.uiautomator.test");
AdbUtils.RunShell(Serial, "pm", "install", "-r", "-t", Push("./tools/app-uiautomator.apk", "0644"));
AdbUtils.RunShell(Serial, "pm", "install", "-r", "-t", Push("./tools/app-uiautomator-test.apk", "0644"));
}
internal void StopAtxAgent()
{
this.OnMsg?.Invoke($"[{Serial}] adb shell {AtxAgentPath} server --stop");
AdbUtils.RunShell(Serial, AtxAgentPath, "server", "--stop");
}
internal void StartAtxAgent()
{
GrantAtxPermissions();
// adb shell /data/local/tmp/atx-agent server --nouia -d --addr 127.0.0.1:7912
this.OnMsg?.Invoke($"[{Serial}] adb shell {AtxAgentPath} server --nouia -d --addr {AtxListenAddr}");
AdbUtils.RunShell(Serial, AtxAgentPath, "server", "--nouia", "-d", "--addr", AtxListenAddr);
}
internal void GrantAtxPermissions()
{
String[] permissions =
{
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.READ_PHONE_STATE"
};
foreach (var permission in permissions)
{
this.OnMsg?.Invoke($"[{Serial}] adb shell pm grant com.github.uiautomator {permission}");
AdbUtils.RunShell(Serial, "pm", "grant", "com.github.uiautomator", permission);
}
}
#endregion
#region
public void StopSndcpySockets()
{
this.DisableTunnelForward(Serial, LocalSndcpyServerPort);
this.SndcpyServer?.Kill();
}
public bool EnableSndcpyForward(UInt16 portStart = 28200)
{
for (UInt16 port = portStart; port < 62300; port++)
{
if (EnableTunnelForward(this.Serial, port, SNDCPY_SOCKET_NAME))
{
this.LocalSndcpyServerPort = port;
return true;
}
}
LOGE($"Could not forward sndcpy port");
return false;
}
public ProcessSession RunSndcpyServer(ServerParams serverParams)
{
String[] cmd = {
"shell",
"am",
"start",
"com.rom1v.sndcpy/.MainActivity"
};
return AdbUtils.adb_execute(Serial, cmd);
}
private void ConnectSndcpySockets() // server_connect_to()
{
UInt32 attempts = 1000;
UInt32 delay = 100; // ms
this.AudioSocket = this.ConnectToServer(LocalSndcpyServerPort, attempts, delay);
if (this.AudioSocket != null)
{
OnAudioSocketConnected?.Invoke();
}
}
internal void InstallSndcpy()
{
this.OnMsg?.Invoke($"[{Serial}] install sndcpy");
this.OnMsg?.Invoke($"[{Serial}] adb shell pm uninstall com.rom1v.sndcpy");
AdbUtils.RunShell(Serial, "uninstall", "com.rom1v.sndcpy");
// AdbUtils.RunShell(Serial, "install", "-t", "-g", Push("./tools/sndcpy.apk", "0644"));
AdbUtils.RunShell(Serial, "pm", "install", "-r", "-g", "-t", Push("./tools/sndcpy.apk"));
AdbUtils.RunShell(Serial, "appops", "set", "com.rom1v.sndcpy", "PROJECT_MEDIA","allow");
}
#endregion
internal String Push(String filePath, String permission = "0755", String dest = null)
{
System.IO.FileInfo fileInfo = new System.IO.FileInfo(filePath);
if (dest == null) dest = "/data/local/tmp/" + fileInfo.Name;
this.OnMsg?.Invoke($"[{Serial}] adb push {filePath} {dest}");
this.OnMsg?.Invoke($"[{Serial}] adb shell chmod {permission} {dest}");
AdbUtils.Run(AdbUtils.adb_push(Serial, filePath, dest));
AdbUtils.RunShell(Serial, "chmod", permission, dest);
return dest;
}
}
}