前几天接到一个需求,我们的客户需要对手机网络接入点进行可用性测试,简单点说就是需要实现Android上的APN配置的添加,APN切换网络模式4G/3G/2G切换,我要调研下写个demo。
因为是要实现自动化测试,而且得合并到现有的拨测系统(C#项目)成为其中的一个模块,就需要用C#来驱动Android测试。交互方式上首先想到的是撸个代码放Android上,定时从服务端获取任务命令然后执行,嗯,OWIN实现个webapi进行数据交互分分钟的事情,貌似可行。 不过又想到,我们测试万一网络切换坏了,就不能联网了那就完了。这样的话,就不能进行任何手机天线端的网络操作了。接着就想到USB交互 然后找到了这个命令:adb forward tcp:PCPort tcp:Androidport 作用是将当前环境的某个端口与Android的某个端口绑定。这样Android 内部请求Androidport端口号就和请求PC上的PCPort端口一样,反之亦然,手机需要打开USB调试。准备写的时候我又想到,我们做的是无人值守的主动测试,Android一会儿跑过来问问有没有执行命令,一会儿跑过来问问 感觉有点不大好,麻烦别人还得别人惦记着不是我的性格。。。 balabala一番思想斗争后决定用socket交互,Android端做服务端,要做啥 过来说下~~
Android的Server端通讯简要讯码:
SCServer :接收连接过来的客户端,并且保存到ClientManager中
public class SCServer implements Runnable { static Boolean Startd = false; static Integer Port; static ServerSocket serverSocket = null; ClientManager clientManager = new ClientManager(); public SCServer(int port) { Port = port; } @Override public void run() { if (!Startd) { try { serverSocket = new ServerSocket(Port); Startd = true; System.out.println("Startd :" + Port); } catch (IOException e) { e.printStackTrace(); } try { while (Startd) { Socket socket = serverSocket.accept(); clientManager.AddClient(socket); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void RegistCallBack(String comm, CallBack callBack) { CommManager.Add(comm, callBack); } public void UnRegistCallBack(String comm) { CommManager.Remove(comm); } public void Send(Integer clientID, String comm, Map<String, String> msgDatas) { clientManager.SendMsg(clientID, comm, msgDatas); }}
ClientManager:保存所有客户端,分配唯一编号,线程运行客户端监听消息,根据编号找到客户端Client 发送消息。
public class ClientManager { static Integer ClientID=0; static Map<Integer, Client> Clients = new HashMap<>(); public void AddClient(Socket socket) { Integer clientID= ClientID++; Client clinet = new Client(socket,clientID); new Thread(clinet).start(); Clients.put(clientID, clinet); } public void SendMsg(Integer clientID, String comm, Map<String, String> msgDatas) { if (Clients.containsKey(clientID)) { Client client = Clients.get(clientID); client.SendMsg(comm, msgDatas); } }}
Client:数据收发,命令解析。消息的载体是json格式FastJson处理。数据类容转换为Map<String,String>对应的为C#的Dictionary<string, string>
public class Client implements Runnable { private Socket socket; private DataOutputStream dos = null; private BufferedReader brIs = null; private boolean bConnected = false; public Integer ClientID = -1; public Client(Socket socket, int id) { this.socket = socket; this.ClientID = id; } @Override public void run() { try { brIs = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); dos = new DataOutputStream(socket.getOutputStream()); System.out.println(this.ClientID + " Start"); bConnected = true; while (bConnected) { String str = brIs.readLine(); if(str!=null){ System.out.println("-------->" + str); JSONObject jb = JSON.parseObject(str); String msgComm = jb.getString("MsgComm"); CallBack cb = CommManager.Get(msgComm); if (cb != null) { String msgCBComm = jb.getString("MsgCBComm"); Map<String, String> msgDatas = (Map<String, String>) JSON.parse(jb.getString("MsgDatas")); cb.execute(ClientID, msgCBComm, msgDatas); } else { System.out.println("--->MsgComm:[" + msgComm+ "] Can't Find!"); }} } } catch (IOException e) { e.printStackTrace(); } } public void SendMsg(String comm, String callBackComm, Map<String, String> msgDatas) { Message msg = new Message(); msg.MsgCBComm = callBackComm; msg.MsgComm = comm; msg.MsgDatas = msgDatas; String StrJson = JSON.toJSONString(msg); System.out.println("<--------"+StrJson); try { this.dos.writeUTF(StrJson); this.dos.flush(); } catch (IOException e) { e.printStackTrace(); } } public void SendMsg(String comm, Map<String, String> msgDatas) { SendMsg(comm,"",msgDatas); }}
CommManager:消息命令管理,保存命令关键字与回调的处理方法。
public class CommManager { static Map<String, CallBack> Comms = new HashMap<String, CallBack>(); public static void Add(String comm, CallBack callBack) { Comms.put(comm, callBack); } public static CallBack Get(String comm) { if (Comms.containsKey(comm)) { CallBack callBack = Comms.get(comm); return callBack; } else { return null; } } public static void Remove(String comm) { Comms.remove(comm); }}
CallBack:回调接口,返回客户端ID,消息返回命令,接收的消息
public interface CallBack { public void execute(Integer clientID, String callBackComm, Map<String, String> msgDatas); }
Message:交互的消息
public class Message { public String MsgComm; //传过来的命令 public String MsgCBComm;//回应的命令 public Map<String,String> MsgDatas=new HashMap<String, String>();//数据}
调用方式:
1 final SCServer sc = new SCServer(57641); 2 3 sc.RegistCallBack("DoSth", new CallBack() { 4 @Override 5 public void execute(Integer clientID, String callBackComm,Map<String, String> msgDatas) { 6 // 执行代码 7 msgDatas.clear(); 8 msgDatas.put("Result", "OK"); 9 sc.Send(clientID, callBackComm, msgDatas);10 }11 });
C#的Client端通讯简要代码
using Newtonsoft.Json;using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;namespace LiteSocket{ public class SocketClient { public bool IsConnected = false; private static byte[] result = new byte[2048]; string IP; int Port; Thread t_Server; Socket clientSocket; Dictionary<string, Action<string, Dictionary<string, string>>> Comms = new Dictionary<string, Action<string, Dictionary<string, string>>>(); public SocketClient(string ip, int port) { IP = ip; Port = port; } public void Close() { clientSocket.Close(); t_Server.Abort(); } public bool Connect() { try { IPAddress ip = IPAddress.Parse(IP); clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); clientSocket.Connect(new IPEndPoint(ip, Port)); //配置服务器IP与端口 t_Server = new Thread(() => { while (clientSocket.Connected) { try { int receiveLength = clientSocket.Receive(result); if (receiveLength > 0) { //接收数据处理 string msgStr = Encoding.UTF8.GetString(result, 2, receiveLength - 2); Console.WriteLine(msgStr); Message msg = JsonConvert.DeserializeObject<Message>(msgStr); Action<string, Dictionary<string, string>> action = null; if (!Comms.TryGetValue(msg.MsgComm, out action)) { Console.WriteLine("MsgComm :" + msg.MsgComm + " 不存在"); } else { action(msg.MsgCBComm, msg.MsgDatas); //回调 } } } catch (Exception ex) { } } }); t_Server.IsBackground = false; t_Server.Start(); } catch (Exception ex) { Console.WriteLine(ex.Message); } IsConnected = clientSocket.Connected; return IsConnected; } /// <summary> /// 注册回调方法 /// </summary> /// <param name="Comm">消息命令</param> /// <param name="CallBack">回调方法</param> public void RegistComm(string Comm, Action<string/*返回消息命令*/, Dictionary<string, string>> CallBack) { if (!Comms.ContainsKey(Comm)) { Comms.Add(Comm, CallBack); } else { Comms[Comm] = CallBack; } } public void UnRegistComm(string Comm) { if (Comms.ContainsKey(Comm)) { Comms.Remove(Comm); } } /// <summary> /// 发送数据给服务端,需要返回,回调响应 /// </summary> /// <param name="comm">命令消息</param> /// <param name="callBackComm">返回消息</param> /// <param name="msgDatas">消息内容</param> public void PostData(string comm, string callBackComm, Dictionary<string, string> msgDatas) { Message m = new Message(); m.MsgComm = comm; m.MsgCBComm = callBackComm; m.MsgDatas = msgDatas; string json = JsonConvert.SerializeObject(m); Console.WriteLine(json); if (clientSocket.Connected) { clientSocket.Send(Encoding.UTF8.GetBytes(json + "\n")); } else { Console.WriteLine("Connected Is Broken"); } } /// <summary> /// 发送命令给服务端,不需要返回数据 /// </summary> /// <param name="comm"></param> /// <param name="msgDatas"></param> public void PostData(string comm, Dictionary<string, string> msgDatas) { PostData(comm, "", msgDatas); } /// <summary> /// 发送命令给服务端,并等待返回的消息。 /// </summary> /// <param name="comm"></param> /// <param name="waitSeconds">命令执行超时时间 默认60s</param> /// <returns></returns> public Dictionary<string, string> SendData(string comm, int waitSeconds = 60) { return SendData(comm, new Dictionary<string, string>(), waitSeconds); } /// <summary> /// 发送命令和数据给服务端,并等待返回的消息。 /// </summary> /// <param name="comm"></param> /// <param name="msgDatas"></param> /// <param name="waitSeconds">命令执行超时时间 默认60s</param> /// <returns></returns> public Dictionary<string, string> SendData(string comm, Dictionary<string, string> msgDatas, int waitSeconds = 60) { DateTime waitTime = DateTime.Now.AddSeconds(waitSeconds); Dictionary<string, string> returnMsgDatas = null; string RdComm = RandomStr(8); //随机生成返回消息命令 RegistComm(RdComm, (cbkey, data) => { returnMsgDatas = data; }); Message m = new Message(); m.MsgComm = comm; m.MsgCBComm = RdComm; m.MsgDatas = msgDatas; string json = JsonConvert.SerializeObject(m); if (clientSocket.Connected) { clientSocket.Send(Encoding.UTF8.GetBytes(json + "\n")); } else { Console.WriteLine("Connect Is Broken"); } //等待返回数据 double wait = 0.00; while (returnMsgDatas == null && wait<=0) { Thread.Sleep(500); wait = (DateTime.Now - waitTime).TotalSeconds; } UnRegistComm(RdComm); //注销命令 return returnMsgDatas; } public static string CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /// <summary> /// 真·随机字符串 /// </summary> /// <param name="lenght">长度</param> /// <returns></returns> public string RandomStr(int lenght) { StringBuilder sb = new StringBuilder(); Random r = new Random(Guid.NewGuid().GetHashCode()); for (int i = 0; i < lenght; i++) { sb.Append(CHAR[r.Next(25)]); } return sb.ToString(); } }}
Message:
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace LiteSocket{ public class Message { public string MsgComm { set; get; } public string MsgCBComm { set; get; } private Dictionary<string, string> _MsgDatas = new Dictionary<string, string>(); public Dictionary<string, string> MsgDatas { get { return _MsgDatas; } set { _MsgDatas = value; } } }}
调用方法:
SocketClient SC = new SocketClient(ip, port); Dictionary<string, string> Dic_doSth = new Dictionary<string, string>(); Dic_doSth.Add("somethingKey", "somethingValue"); var result = SC.SendData("DoSth", Dic_doSth);//发送并接收返回数据 //OR SC.RegistComm("SthOver", (rekey, value) => { //处理返回数据 }); SC.PostData("DoSth", "SthOver", Dic_doSth); //发送 异步处理返回数据
以上的交互完成了,后面就是业务代码了。APN添加切换 网络模式切换
网上搜了下,得到一个例子:Android开发之APN网络切换 心中暗喜:有前辈给出了解决方案,还有代码实例,这实现起来还不简单么。照猫画虎。。。后发现出了个错:
No permission to write APN settings:
查询了一翻发现android 4.0以上对这一权限进行回收了。我们的测试机为小米4,按照网上说的方法进行了 重新系统签名,系统权限设置均无效,依然会有权限错误,中间为了得到android4.4.4的platform.pk8文件还下载了8G的android 4.4.4源码。可能是MIUI的与android原生的系统签名不一样 总是就是要不没权限 要不安装不上。 网上还有一种方法是 MM编译,得在Linux环境下;Eclipse+NDK配置又是很多的配置,看着教程实在感受不到爱了。。。 索性就放弃了这方案 曲线救国的方式来实现需求-----模拟用户屏幕操作。 adb有个Input命令,可以模拟键盘输入,屏幕点击,屏幕滑动。
adb shell input keyevent “value”usage: input ... input text <string> input keyevent <key code number or name> input tap <x> <y> input swipe <x1> <y1> <x2> <y2>
常用键:
input keyevent 3 // Homeinput keyevent 4 // Backinput keyevent 19 //Upinput keyevent 20 //Downinput keyevent 21 //Leftinput keyevent 22 //Rightinput keyevent 23 //Select/Okinput keyevent 24 //Volume+input keyevent 25 // Volume-input keyevent 82 // Menu 菜单
抄个这段代码,Android上执行终端命令,Root权限?小米4:—_—
public static void execShellCmd(String cmd) { try { // 申请获取root权限 Process process = Runtime.getRuntime().exec("su"); OutputStream outputStream = process.getOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream( outputStream); dataOutputStream.writeBytes(cmd); dataOutputStream.flush(); dataOutputStream.close(); outputStream.close(); } catch (Throwable t) { t.printStackTrace(); } }
那么,当我需要添加一个APN的时候:
Android:
final SCServer sc = new SCServer(57641); sc.RegistCallBack("AddApn", new CallBack() { @Override public void execute(Integer clientID, String callBackComm, Map<String, String> msgDatas) { Intent intent = new Intent(Settings.ACTION_APN_SETTINGS); startActivity(intent);
SystemClock.Sleep(1000); for (int i = 0; i < msgDatas.values().size(); i++) { String strDo = msgDatas.get(i + ""); FoolHand.execShellCmd(strDo); Log.d("strDo", strDo); SystemClock.sleep(1000); } msgDatas.clear(); msgDatas.put("Result", "OK"); sc.Send(clientID, callBackComm, msgDatas); } });
C#:
public bool AddApn(string Name, string APN) { Dictionary<string, string> doSth = new Dictionary<string, string>(); int i = 0; doSth.Add((i++).ToString(), "input tap 463 1810");//点击新建 doSth.Add((i++).ToString(), "input tap 650 290"); //点击名称 doSth.Add((i++).ToString(), "input text " + Name); //输入名称 doSth.Add((i++).ToString(), "input tap 846 1040"); //点击确定 doSth.Add((i++).ToString(), "input tap 650 470"); //点击APN doSth.Add((i++).ToString(), "input text " + APN); //输入APN doSth.Add((i++).ToString(), "input tap 846 1040"); //点击确定 doSth.Add((i++).ToString(), "input keyevent 4"); //退出 (弹出保存确认框) doSth.Add((i++).ToString(), "input tap 730 1780"); // 确认保存 var result = SC.SendData("AddApn", doSth); if (result["Result"] == "OK") { return true; } else { return false; } }
效果:
sc.RegistCallBack("SetNetMode", new CallBack() { @Override public void execute(Integer clientID, String callBackComm, Map<String, String> msgDatas) { Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); startActivity(intent); for (int i = 0; i < msgDatas.values().size(); i++) { String strDo = msgDatas.get(i + ""); FoolHand.execShellCmd(strDo); Log.d("strDo", strDo); SystemClock.sleep(1000); } msgDatas.clear(); msgDatas.put("Result", "OK"); sc.Send(clientID, callBackComm, msgDatas); } });
public bool ChangeNetMode(string NetMode) { Dictionary<string, string> doSth = new Dictionary<string, string>(); int i = 0; doSth.Add((i++).ToString(), "input swipe 640 550 640 1440"); //滑到最顶端 doSth.Add((i++).ToString(), "input tap 640 430"); doSth.Add((i++).ToString(), "input tap 640 1040"); switch (NetMode) { case "4G": doSth.Add((i++).ToString(), "input tap 640 260");//选择4G break; case "3G": doSth.Add((i++).ToString(), "input tap 640 430");//选择3G break; case "2G": doSth.Add((i++).ToString(), "input tap 640 600");//点击2G break; default: break; } doSth.Add((i++).ToString(), "input keyevent 4"); doSth.Add((i++).ToString(), "input keyevent 4"); // 640 260 430 var result = SC.SendData("SetNetMode", doSth); if (result["Result"] == "OK") { return true; } else { return false; } }
这玩意模拟键盘输入,所以得记住屏幕位置。
这玩意模拟键盘输入,所以不能录入中文。
- 4楼韩之一
- 像2楼说的,换个机型就挂了,可以把设置点击坐标的方法可以由用户自己设定来解决这个问题
- Re: yesicoo
- @韩之一,是的,可以弄成按键精灵了,,public bool AddApn(string Name, string APN) { Dictionarylt;string, stringgt; doSth = new Dictionarylt;string, stringgt;(); int i = 0; doSth.Add((i++).ToString(), quot;input tap 463 1810quot;);//点击新建 doSth.Add((i++).ToString(), quot;input tap 650 290quot;); //点击名称 doSth.Add((i++).ToString(), quot;input text quot; + Name); //输入名称 doSth.Add((i++).ToString(), quot;input tap 846 1040quot;); //点击确定 doSth.Add((i++).ToString(), quot;input tap 650 470quot;); //点击APN doSth.Add((i++).ToString(), quot;input text quot; + APN); //输入APN doSth.Add((i++).ToString(), quot;input tap 846 1040quot;); //点击确定 doSth.Add((i++).ToString(), quot;input keyevent 4quot;); //退出 (弹出保存确认框) doSth.Add((i++).ToString(), quot;input tap 730 1780quot;); // 确认保存 var result = SC.SendData(quot;AddApnquot;, doSth); if (result[quot;Resultquot;] == quot;OKquot;) { return true;
- 3楼cherver
- 挺好的,收藏了。
- Re: yesicoo
- @cherver,谢谢
- 2楼Martell XO
- 这个,换个机型就死翘翘了。
- Re: yesicoo
- @Martell XO,是的 屏幕点击嘛~
- 1楼韩之一
- 这个是不是可以当android版的按键精灵来用?,就按楼主的方式 来,,在自己用winform写和套用来定义执行什么操作的语法,用可视化操作的方式来设定,设定完成后,执行,就发到手机上了,执行,执行效果就像图上显示的那样,,好像在自己执行