在上一篇我简单的了解了一下hierarchyviewer和uiautomatorviewer,如需访问,点击以下链接:
android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(1)
通过对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} 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的。
得到这些以后,我们就可以以控件名为关键字分类处理:
上面的方法中,我只列举了一些常见的控件,其中实现的只有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不适合做这个工具,我对比了一下总结如下:
总结
