在上一篇我简单的了解了一下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 runCMD(String cmd) {
Map map = new HashMap();
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 children = new ArrayList();
private List canTouchViewNodes = new ArrayList();
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
List
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 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 children = clickNode.children;
currentListContainItem = children.size();
itemCountOfList = clickNode.itemCount;
startIndexOfList = clickNode.firstIndex;
int n = 1;
for (ViewNode item : children) {
// analyze
List needToDeleteNodesFromItem = new ArrayList();
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 categories = new ArrayList();
categories.add("android.intent.category.LAUNCHER");
device.startActivity(null, action, null, null, categories, new HashMap(),
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 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 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)