android自動化測試中hierarchyviewer和uiautomatorviewer獲取控制元件資訊的方式比對(2)
在上一篇我簡單的瞭解了一下hierarchyviewer和uiautomatorviewer,如需訪問,點選以下連結:
通過對hierarchyview的原始碼分析,我嘗試用java寫了一個測試工具,該測試工具簡單的實現了連線ViewServer獲取控制元件資訊,然後根據控制元件資訊的座標屬性來點選按鈕。
1.RunTime執行CMD命令,連線ViewServer。
2.獲取控制元件資訊以後,得到可點選的按鈕。
3.Java呼叫Monkeyrunner API對按鈕進行操作。
4.判斷點選後的檢視型別。
第一節 Runtime執行CMD命令
因為我要連線ViewServer,所以得實現執行cmd命令。方法如下:
public boolean preCofig() { boolean flag = false; String cmd = "adb -s " + deviceId + " forward tcp:" + port + " tcp:4939"; CMDUtils.runCMD(cmd, null); cmd = "adb -s " + deviceId + " shell service call window 3"; String result = CMDUtils.runCMD(cmd, null); int index = result.indexOf("1"); if (index > -1) { flag = true; } else { cmd = "adb -s " + deviceId + " shell service call window 1 i32 " + port; result = CMDUtils.runCMD(cmd, null); index = result.indexOf("1"); if (index > -1) { flag = true; } } return flag; } public boolean connectDevice() { boolean flag = false; if (preCofig() == true) { try { socket = new Socket(); socket.connect(new InetSocketAddress("127.0.0.1", port), 40000); if (socket.isConnected()) { out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8")); try { fw = new FileWriter( new File(Const.LOCA_PATH + "/" + deviceId + "_dump.txt")); } catch (IOException e) { e.printStackTrace(); } flag = true; } } catch (Exception e) { e.printStackTrace(); } } return flag; }
這樣,給不同的裝置對映不同的埠,然後通過socket訪問。這2個方法主要是2個目的:
1.確定viewServer是否開啟,如果沒開啟,執行開啟命令。
2.確定viewServer開啟後,執行socket連線操作,獲得寫入寫出物件,等待命令的發出與讀取。
上面呼叫了CMDUtils類中的方法runCMD()。
public static String runCMD(String cmd, String flag) { BufferedReader in = null; String result = null; Process process = null; try { process = Runtime.getRuntime().exec(cmd); in = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8")); } catch (IOException e) { e.printStackTrace(); } String line = null; try { while ((line = in.readLine()) != null) { if (null != flag) { int index = line.indexOf(flag); if (index != -1) result = line; } else result += line; } } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); process.destroy(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return result; }
通過這個方法,呼叫java的Runtime環境執行cmd方法,得到返回結果。
到這一步結束,我們就通過執行了CMD命令,連線了Viewserver。
其實簡單就是你在dos下執行下面3個命令:
adb -s emulator-5554 forward tcp:4939 tcp:4939 :對映埠到本地。
adb -s emulator-5554 shell service call window 3 :判斷viewserver是否開啟。
adb -s emulator-5554 shell service call window 1 i32 4939 :開啟viewserver。
連線ViewServer以後,我們就要獲取資料啦。
第二節 獲取控制元件資訊以後,得到可點選的按鈕。
這個我直接用Hierarchyviewer裡的方法,不多解釋了。
/*
* 獲取控制元件資訊
*/
public ViewNode parseViewHierarchy() {
if (socket == null || socket.isConnected() == false) {
connectDevice();
}
try {
out.write("DUMP -1");
out.newLine();
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
ViewNode currentNode = null;
int currentDepth = -1;
String line;
try {
while ((line = in.readLine()) != null && !"DONE.".equalsIgnoreCase(line)) {
// System.out.println(line);
int depth;
for (depth = 0; line.charAt(depth) == ' '; depth++)
;
for (; depth <= currentDepth; currentDepth--)
if (currentNode != null)
currentNode = currentNode.parent;
fw.write(line + "\n");
currentNode = new ViewNode(currentNode, line.substring(depth));
currentDepth = depth;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close();
}
if (currentNode == null)
return null;
for (; currentNode.parent != null; currentNode = currentNode.parent)
;
return currentNode;
}
得到這些控制元件資訊以後,我們要把它儲存在一個檢視物件中,這樣轉換為對當前檢視物件進行操作。
可以通過命令:adb shell dumpsys window,從得到的資料中提取有用的資訊。
..............
Display: init=480x854 base=480x854 cur=480x854 app=480x854 raw=480x854
mCurConfiguration={1.0 460mcc2mnc zh_CN layoutdir=0 sw320dp w320dp h544dp nrml long port finger -keyb/v/h -nav/h s.5}
mCurrentFocus=Window{4189d1d0 com.android.settings/com.android.settings.SubSettings paused=false}
mFocusedApp=AppWindowToken{4167cac0 token=Token{4184ffc8 ActivityRecord{418f6a60 com.android.settings/.SubSettings}}}
mInputMethodTarget=Window{41719db8 新增網路 paused=false}
mInTouchMode=true mLayoutSeq=186
在資訊的最後一段裡,發現了2個有用的屬性:mCurrentFocus和mFocusedApp,這兩個屬性分別代表當前Window的資訊和activity資訊;然後根據window的hascode值可以得到當前視窗的其他資訊。
Window #4 Window{4189d1d0 com.android.settings/com.android.settings.SubSettings paused=false}:
mSession=Session{4179f4e8 uid 1000} [email protected]
mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#110 ty=1 fl=#810100 pfl=0x8 wanim=0x1030298}
Requested w=480 h=854 mLayoutSeq=186
Surface: shown=true layer=21020 alpha=1.0 rect=(0.0,0.0) 480.0 x 854.0
mShownFrame=[0.0,0.0][480.0,854.0]
這樣方便我們以後使用這些屬性,我們同樣需要執行cmd命令然後刪選這些資訊。
public static Map<String, String> runCMD(String cmd) {
Map<String, String> map = new HashMap<String, String>();
BufferedReader in = null;
Process process = null;
String result = null;
try {
process = Runtime.getRuntime().exec(cmd);
in = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
String line = null;
try {
while ((line = in.readLine()) != null) {
int index = line.indexOf("mCurrentFocus");
if (index > -1) {
index = line.indexOf("=");
line = line.substring(index + 1);
System.out.println("CMDUtils----------------------------------window:" + line);
map.put("window", line);
}
index = line.indexOf("mFocusedApp");
if (index > -1) {
index = line.indexOf("ActivityRecord");
int startIndex = line.indexOf("{", index);
int endIndex = line.indexOf("}", index);
line = line.substring(startIndex + 1, endIndex);
System.out.println("CMDUtils----------------------------------activity:" + line);
map.put("activity", line);
}
result += line;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
process.destroy();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
int index = result.indexOf(map.get("window") + ":");
result = result.substring(index + 1);
index = result.indexOf("mShownFrame", index);
int startIndex = result.indexOf("[", index);
index = result.indexOf("]", startIndex);
String startPoint = result.substring(startIndex + 1, index);
System.out.println("CMDUtils----------------------------------startPoint:" + startPoint);
int endIndex = result.indexOf("]", index + 1);
String endPoint = result.substring(index + 2, endIndex);
System.out.println("CMDUtils----------------------------------endPoint:" + endPoint);
map.put("startPoint", startPoint);
map.put("endPoint", endPoint);
return map;
}
這樣我們就得到了我們需要的資訊,測試一下,命令列輸出如下:
CMDUtils----------------------------------activity:420ce328 u0 com.android.launcher3/.Launcher t1
CMDUtils----------------------------------window:Window{420cd0c8 u0 com.android.launcher3/com.android.launcher3.Launcher}
CMDUtils----------------------------------activity:420ce328 u0 com.android.launcher3/.Launcher t1
CMDUtils----------------------------------startPoint:0.0,0.0
CMDUtils----------------------------------endPoint:480.0,854.0
有的人會疑惑,我們取這些資訊有什麼用。
window:唯一標識當前介面;activity並不能唯一標識,因為彈出框的activity和父檢視的activity是一樣的。
activity:可以區分當前視窗是否是新視窗。
startPoint和endPoint可以獲得視窗的座標和範圍,因為彈出框的起始座標不是以裝置的左上頂點為起始座標的;在我們獲得控制元件資訊時得到的座標,如果是彈出框,它無法確定準確的座標值,因為它把自己的邊界當成了起始座標點。這樣我們點選的時候就會出現問題;通過這個startPoint和endPoint可以在原來的基礎上加上起始值,這樣得到的座標點才是正確的。
在獲得這些資訊以後,加上上面Viewserver獲得的控制元件資訊,我們就可以建立View物件啦。
private ViewNode rootViewNode;
private IChimpImage iChimpImage;
private View parent;
private String window;
private String activity;
private List<View> children = new ArrayList<View>();
private List<ViewNode> canTouchViewNodes = new ArrayList<ViewNode>();
private ViewNode FromViewNode;
private Point startPoint = new Point();
private Point endPoint = new Point();
public View(View view, ViewNode viewNode, IChimpImage iChimpImage) {
this.parent = view;
this.rootViewNode = viewNode;
this.iChimpImage = iChimpImage;
if (parent != null) {
parent.children.add(this);
}
if (rootViewNode != null) {
getCanTouchWidgets(rootViewNode);
}
}
public void getCanTouchWidgets(ViewNode viewNode) {
if (viewNode.width * viewNode.height != 0 && viewNode.isClickable == true) {
canTouchViewNodes.add(viewNode);
}
if (viewNode.children.size() != 0) {
for (ViewNode sonNode : viewNode.children) {
getCanTouchWidgets(sonNode);
}
}
}
在View類中,我定義了很多屬性。
ViewNode rootViewNode:檢視中控制元件的跟節點。
IChimpImage iChimpImage: 當前介面的截圖,為了以後生成報告的時候用,還可以用圖片比對。
View parent:父檢視。
String window:介面ID。
String activity:activity名。
List<View> children:子檢視。
List<ViewNode> canTouchViewNodes:存放可點選的控制元件。
ViewNode fromViewNode:該檢視是點選父檢視的那個按鈕出現的,可以繪製軌跡。
在方法getCanTouchWidgets中遞迴迴圈得到可點選的控制元件,必須是可見且isclickable的屬性為true的。
得到這些以後,我們就可以以控制元件名為關鍵字分類處理:
public void getAllViewForApp(View view) {
// ListView
boolean hasListView = false;
int currentListContainItem = 0;
int itemCountOfList = 0;
int startIndexOfList = 0;
ViewNode listViewNode = null;
View currentView = view;
List<ViewNode> clickNodes = currentView.getCanTouchViewNodes();
int size = clickNodes.size();
for (int i = 0; i < size; i++) {
ViewNode clickNode = clickNodes.get(i);
String clickNodeName = clickNode.widgetName;
// System.out.println("ViewClient ----------" +clickNodeName);
int x = clickNode.xPoint + clickNode.width / 2;
int y = clickNode.yPoint + clickNode.height / 2;
clickNode.hasClick = true;
switch (clickNodeName) {
case "EditText":
System.out
.println("ViewClient---------------------------------WidgetName:EditText");
break;
case "TextView":
System.out
.println("ViewClient---------------------------------WidgetName:TextView");
break;
case "Button":
System.out.println("ViewClient---------------------------------WidgetName:Button");
break;
case "ListView":
hasListView = true;
listViewNode = clickNode;
List<ViewNode> children = clickNode.children;
currentListContainItem = children.size();
itemCountOfList = clickNode.itemCount;
startIndexOfList = clickNode.firstIndex;
int n = 1;
for (ViewNode item : children) {
// analyze
List<ViewNode> needToDeleteNodesFromItem = new ArrayList<ViewNode>();
for (int j = i + 1; j < size; j++) {
ViewNode viewNode = clickNodes.get(j);
for (; viewNode.parent != null; viewNode = viewNode.parent) {
if (viewNode.parent.equals(item)) {
System.out
.println("ViewClient---------------------------------contains other clickable widget");
needToDeleteNodesFromItem.add(viewNode);
}
}
}
if (needToDeleteNodesFromItem.size() != 0) {
Point touchPoint = toDeleteNodesFromItem(item, needToDeleteNodesFromItem);
x = touchPoint.x;
y = touchPoint.y;
} else {
x = item.xPoint + item.width / 2;
y = item.yPoint + item.height / 2;
}
x = x <= deviceManager.getWidth() ? x : deviceManager.getWidth();
y = y <= deviceManager.getHeight() ? y : deviceManager.getHeight();
deviceManager.touch(x, y);
System.out
.println("ViewClient---------------------------------current Click No:"
+ n + "/" + currentListContainItem);
getActionType(currentView);
n++;
}
System.out.println("ViewClient---------------------------------finish clicked:"
+ currentListContainItem + "/" + itemCountOfList);
break;
case "CheckBox":
System.out
.println("ViewClient---------------------------------WidgetName:CheckBox");
break;
case "Spinner":
System.out.println("ViewClient---------------------------------WidgetName:Spinner");
break;
case "Switch":
System.out.println("ViewClient---------------------------------WidgetName:Switch");
if (clickNode.isChecked == true) {
deviceManager.touch(x, y);
deviceManager.touch(x, y);
} else {
deviceManager.touch(x, y);
}
break;
case "ImageView":
System.out
.println("ViewClient---------------------------------WidgetName:ImageView");
break;
case "LinearLayout":
System.out.println(x + "," + y);
System.out
.println("ViewClient---------------------------------WidgetName:LinearLayout:"
+ clickNode.width + ",:" + clickNode.height);
deviceManager.touch(x, y);
getActionType(currentView);
break;
default:
System.out.println("ViewClient---------------------------------error WidgetName:"
+ clickNodeName);
break;
}
}
上面的方法中,我只列舉了一些常見的控制元件,其中實現的只有ListView控制元件;其實這裡需要一個演算法,可以判斷介面的型別,然後得到點選的順序,但是我做的是最簡單的;邏輯也簡單,所以已經暫停了(安心做最簡單的dump研究啦。)。
上面的方法中用到了deviceManager.touch和type方法,DeviceManager是我呼叫MonkeyRunner的類。
第三節 Java呼叫Monkeyrunner API對按鈕進行操作
DeviceManager.java:
private AdbChimpDevice device;
private AdbBackend adb;
private int width;
private int height;
public DeviceManager(String deviceId) {
if (adb == null) {
adb = new AdbBackend();
device = (AdbChimpDevice) adb.waitForConnection(8000, deviceId);
this.width = Integer.parseInt(device.getProperty("display.width"));
this.height = Integer.parseInt(device.getProperty("display.height"));
System.out.println("DeviceManager------------------------------device width:"
+ device.getProperty("display.width"));
}
}
public boolean startActivity(String activity) throws Throwable {
boolean flag = false;
String action = "android.intent.action.MAIN";
Collection<String> categories = new ArrayList<String>();
categories.add("android.intent.category.LAUNCHER");
device.startActivity(null, action, null, null, categories, new HashMap<String, Object>(),
activity, 0);
sleep(3000);
flag = true;
return flag;
}
public void touch(int x, int y) {
device.touch(x, y, TouchPressType.DOWN_AND_UP);
sleep(3000);
}
public void drag(int startX, int startY, int endX, int endY) {
device.drag(startX, startY, endX, endY, 1, 10);
}
public void press(String keycode) {
device.press(keycode, TouchPressType.DOWN_AND_UP);
}
這裡面簡單封裝了touch,type,press,drag方法,沒做過多的處理,這也是在網上查找了一些前人的教程得到的,其中用到的4個jar包。
之前試過自己本地的jar包,但是可能因為版本不一樣,裡面有的類缺少,所以如果你的jar不對,可以留郵箱,我傳給你。
第四節 判斷點選後的檢視型別
在點選一個控制元件以後,我們需要判斷點選後發生了什麼,因為我們要深度遍歷一個APP裡所有的檢視的。
public void getActionType(View currentView) {
Map<String, String> map = CMDUtils.runCMD(windowMsg);
String window = map.get("window");
String activity = map.get("activity");
// hold on current view
if (window.equals(currentView.getWindow())) {
System.out.println("ViewClient---------------------------------no action");
} else {
System.out.println("ViewClient---------------------------------different window");
// different window but same activity:dialog
if (activity.equals(currentView.getActivity())) {
System.out.println("ViewClient---------------------------------dialog");
deviceManager.press("KEYCODE_BACK");
} else { // different activity
boolean goNew = true;
// back to father View
View view = currentView;
for (; view.getParent() != null; view = view.getParent()) {
if (view.getParent().getWindow().equals(window)) {
System.out.println("ViewClient---------------------------------back to father view");
goNew = false;
}
}
// same son view
if (currentView.getChildren().size() != 0) {
List<View> children = currentView.getChildren();
for (View sonView : children) {
if (sonView.getWindow().equals(window)) {
System.out.println("ViewClient---------------------------------this view has showed");
goNew = false;
}
}
}
// new view
if (goNew == true) {
System.out.println("ViewClient---------------------------------this view is new");
deviceManager.press("KEYCODE_BACK");
}
}
}
}
首先判斷View物件裡的window屬性和當前檢視的window是否一樣,如果一樣,毫無疑問點選無反應,至少沒動,點選開關按鈕啊,拖拉ListView這些操作。
如果window不同,我們得判斷activity是否一樣,如果activity一樣,說明有彈出框或者對話方塊。如果activity不一樣。我們還要做判斷:
1.是否返回進入到父檢視。
2.是否之前點擊出現過。
3.是否是新檢視。
總之越深入判斷越繁瑣啊。
在我寫到這些的時候,總之被論證HierarchyViewer不適合做這個工具,我對比了一下總結如下:
總結
告一段落,繼續往下研究。
下一篇: