前幾天接到一個需求,我們的客戶需要對手機網絡接入點進行可用性測試,簡單點說就是需要實現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 // Home input keyevent 4 // Back input keyevent 19 //Up input keyevent 20 //Down input keyevent 21 //Left input keyevent 22 //Right input keyevent 23 //Select/Ok input 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; } }
這玩意模擬鍵盤輸入,所以得記住屏幕位置。
這玩意模擬鍵盤輸入,所以不能錄入中文。