在上一篇我简单的了解了一下hierarchyviewer和uiautomatorviewer,如需访问,点击以下链接:
android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(1)
通过对hierarchyview的源码分析,我尝试用java写了一个测试工具,该测试工具简单的实现了连接ViewServer获取控件信息,然后根据控件信息的坐标属性来点击按钮。
1.RunTime执行CMD命令,连接ViewServer。
2.获取控件信息以后,得到可点击的按钮。
3.Java调用Monkeyrunner API对按钮进行操作。
4.判断点击后的视图类型。
因为我要连接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} mClient=android.os.BinderProxy@41953720 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的类。
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不适合做这个工具,我对比了一下总结如下:
告一段落,继续往下研究。
下一篇:
Android自动化测试中AccessibilityService获取控件信息(1)