之前寫過兩篇關於Android中模擬用戶操作的博客(其實用一篇是轉載的),現在就來講講用shell腳本來模擬用戶按鍵操作。本次的目標是用shell腳本打開微信並在其搜索框中搜索相關內容。
本文的模擬功能主要是用adb的input命令來實現,如果你adb的環境變量配置正確的話,在cmd中輸入 adb shell input 就可以看見input的用法了。
usage: input ...
input text //輸入文字(中文不支持)
input keyevent //keyevent按鍵
input [touchscreen|touchpad|touchnavigation] tap <x> <y>//點擊屏幕
input [touchscreen|touchpad|touchnavigation] swipe <x1> <y1> <x2> <y2> //屏幕滑動
input trackball press //滾球已經不用了
input trackball roll //滾球已經不用了
input rotationevent 0 1->90 2->180 3->270> //順時針旋轉
下面直接上安卓用戶操作的代碼,就一個MainActivity而已,UI、Mainfest都不用配置(可能需要root權限)
1 package com.lsj.adb; 2 3 import java.io.DataOutputStream; 4 5 import android.app.Activity; 6 import android.os.Bundle; 7 import android.view.Menu; 8 9 public class MainActivity extends Activity { 10 11 private String[] search = { 12 "input keyevent 3",// 返回到主界面,數值與按鍵的對應關系可查閱KeyEvent 13 "sleep 1",// 等待1秒 14 "am start -n com.tencent.mm/com.tencent.mm.ui.LauncherUI",// 打開微信的啟動界面,am命令的用法可自行百度、Google 15 "sleep 3",// 等待3秒 16 "am start -n com.tencent.mm/com.tencent.mm.plugin.search.ui.SearchUI",// 打開微信的搜索 17 "input text 123",// 像搜索框中輸入123,但是input不支持中文,蛋疼,而且這邊沒做輸入法處理,默認會自動彈出輸入法 18 }; 19 20 @Override 21 protected void onCreate(Bundle savedInstanceState) { 22 super.onCreate(savedInstanceState); 23 setContentView(R.layout.activity_main); 24 //如果input text中有中文,可以將中文轉成unicode進行input,沒有測試,只是覺得這個思路是可行的 25 search[5] = chineseToUnicode(search[5]); 26 execShell(search); 27 } 28 29 /** 30 * 執行Shell命令 31 * 32 * @param commands 33 * 要執行的命令數組 34 */ 35 public void execShell(String[] commands) { 36 // 獲取Runtime對象 37 Runtime runtime = Runtime.getRuntime(); 38 39 DataOutputStream os = null; 40 try { 41 // 獲取root權限,這里大量申請root權限會導致應用卡死,可以把Runtime和Process放在Application中初始化 42 Process process = runtime.exec("su"); 43 os = new DataOutputStream(process.getOutputStream()); 44 for (String command : commands) { 45 if (command == null) { 46 continue; 47 } 48 49 // donnot use os.writeBytes(commmand), avoid chinese charset 50 // error 51 os.write(command.getBytes()); 52 os.writeBytes("\n"); 53 os.flush(); 54 } 55 os.writeBytes("exit\n"); 56 os.flush(); 57 process.waitFor(); 58 } catch (Exception e) { 59 e.printStackTrace(); 60 } 61 } 62 63 /** 64 * 把中文轉成Unicode碼 65 * @param str 66 * @return 67 */ 68 public String chineseToUnicode(String str){ 69 String result=""; 70 for (int i = 0; i < str.length(); i++){ 71 int chr1 = (char) str.charAt(i); 72 if(chr1>=19968&&chr1<=171941){//漢字范圍 \u4e00-\u9fa5 (中文) 73 result+="\\u" + Integer.toHexString(chr1); 74 }else{ 75 result+=str.charAt(i); 76 } 77 } 78 return result; 79 } 80 81 /** 82 * 判斷是否為中文字符 83 * @param c 84 * @return 85 */ 86 public boolean isChinese(char c) { 87 Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); 88 if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS 89 || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS 90 || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A 91 || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION 92 || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION 93 || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) { 94 return true; 95 } 96 return false; 97 } 98 99 100 }
效果圖:
模擬用戶打開微信,並進行搜索就這么完成了。其實這里用shell命令模擬用戶操作還是有些問題的,比如說控件長按(sendevent),好難理解,而且需要跟其中傳遞的控件坐標參數應該要跟屏幕分辨率聯系起來,實際應用范圍不是很廣泛。PS:那種大量需要重復操作的除外,如:自動化測試,游戲刷圖(PS:騰訊的仙劍手游把我大仙劍毀了啊,麻花騰你妹啊,你全家都是麻花騰)。
最后,其實可以參考下按鍵精靈,這款應用做的還不錯,除了不給root權限就崩外....
補充:以上模擬用戶操作的代碼在交互不頻繁的情況下是完全沒有問題的,但是如果使用頻繁的話,會發生多次申請root權限,導致系統卡死的現象,后面在google上找到了一個開源項目,可以解決這個問題(它使用的是單例模式),代碼如下:

1 /** 2 * 類名 RootContext.java 說明 獲取root權限 創建日期 2012-8-21 作者 LiWenLong Email 3 * lendylongli@gmail.com 更新時間 $Date$ 最后更新者 $Author$ 4 */ 5 public class RootContext { 6 private static RootContext instance = null; 7 private static Object mLock = new Object(); 8 String mShell; 9 OutputStream o; 10 Process p; 11 12 private RootContext(String cmd) throws Exception { 13 this.mShell = cmd; 14 init(); 15 } 16 17 public static RootContext getInstance() { 18 if (instance != null) { 19 return instance; 20 } 21 synchronized (mLock) { 22 try { 23 instance = new RootContext("su"); 24 } catch (Exception e) { 25 while (true) 26 try { 27 instance = new RootContext("/system/xbin/su"); 28 } catch (Exception e2) { 29 try { 30 instance = new RootContext("/system/bin/su"); 31 } catch (Exception e3) { 32 e3.printStackTrace(); 33 } 34 } 35 } 36 return instance; 37 } 38 } 39 40 private void init() throws Exception { 41 if ((this.p != null) && (this.o != null)) { 42 this.o.flush(); 43 this.o.close(); 44 this.p.destroy(); 45 } 46 this.p = Runtime.getRuntime().exec(this.mShell); 47 this.o = this.p.getOutputStream(); 48 system("LD_LIBRARY_PATH=/vendor/lib:/system/lib "); 49 } 50 51 private void system(String cmd) { 52 try { 53 this.o.write((cmd + "\n").getBytes("ASCII")); 54 return; 55 } catch (Exception e) { 56 while (true) 57 try { 58 init(); 59 } catch (Exception e1) { 60 e1.printStackTrace(); 61 } 62 } 63 } 64 65 public void runCommand(String cmd) { 66 system(cmd); 67 } 68 69 /** 70 * 判斷是否已經root了 71 * */ 72 public static boolean hasRootAccess(Context ctx) { 73 final StringBuilder res = new StringBuilder(); 74 try { 75 if (runCommandAsRoot(ctx, "exit 0", res) == 0) 76 return true; 77 } catch (Exception e) { 78 } 79 return false; 80 } 81 82 /** 83 * 以root的權限運行命令 84 * */ 85 public static int runCommandAsRoot(Context ctx, String script, 86 StringBuilder res) { 87 final File file = new File(ctx.getCacheDir(), "secopt.sh"); 88 final ScriptRunner runner = new ScriptRunner(file, script, res); 89 runner.start(); 90 try { 91 runner.join(40000); 92 if (runner.isAlive()) { 93 runner.interrupt(); 94 runner.join(150); 95 runner.destroy(); 96 runner.join(50); 97 } 98 } catch (InterruptedException ex) { 99 } 100 return runner.exitcode; 101 } 102 103 private static final class ScriptRunner extends Thread { 104 private final File file; 105 private final String script; 106 private final StringBuilder res; 107 public int exitcode = -1; 108 private Process exec; 109 110 public ScriptRunner(File file, String script, StringBuilder res) { 111 this.file = file; 112 this.script = script; 113 this.res = res; 114 } 115 116 @Override 117 public void run() { 118 try { 119 file.createNewFile(); 120 final String abspath = file.getAbsolutePath(); 121 Runtime.getRuntime().exec("chmod 777 " + abspath).waitFor(); 122 final OutputStreamWriter out = new OutputStreamWriter( 123 new FileOutputStream(file)); 124 if (new File("/system/bin/sh").exists()) { 125 out.write("#!/system/bin/sh\n"); 126 } 127 out.write(script); 128 if (!script.endsWith("\n")) 129 out.write("\n"); 130 out.write("exit\n"); 131 out.flush(); 132 out.close(); 133 134 exec = Runtime.getRuntime().exec("su"); 135 DataOutputStream os = new DataOutputStream( 136 exec.getOutputStream()); 137 os.writeBytes(abspath); 138 os.flush(); 139 os.close(); 140 141 InputStreamReader r = new InputStreamReader( 142 exec.getInputStream()); 143 final char buf[] = new char[1024]; 144 int read = 0; 145 while ((read = r.read(buf)) != -1) { 146 if (res != null) 147 res.append(buf, 0, read); 148 } 149 150 r = new InputStreamReader(exec.getErrorStream()); 151 read = 0; 152 while ((read = r.read(buf)) != -1) { 153 if (res != null) 154 res.append(buf, 0, read); 155 } 156 157 if (exec != null) 158 this.exitcode = exec.waitFor(); 159 } catch (InterruptedException ex) { 160 if (res != null) 161 res.append("\nOperation timed-out"); 162 } catch (Exception ex) { 163 if (res != null) 164 res.append("\n" + ex); 165 } finally { 166 destroy(); 167 } 168 } 169 170 public synchronized void destroy() { 171 if (exec != null) 172 exec.destroy(); 173 exec = null; 174 } 175 } 176 }
作者:登天路