您的位置:首页 > 产品设计 > UI/UE

android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(2)

2015-01-19 23:19 363 查看
在上一篇我简单的了解了一下hierarchyviewer和uiautomatorviewer,如需访问,点击以下链接:

android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(1)

通过对hierarchyview的源码分析,我尝试用java写了一个测试工具,该测试工具简单的实现了连接ViewServer获取控件信息,然后根据控件信息的坐标属性来点击按钮。

1.RunTime执行CMD命令,连接ViewServer。

2.获取控件信息以后,得到可点击的按钮。

3.Java调用Monkeyrunner API对按钮进行操作。

4.判断点击后的视图类型。

第一节 Runtime执行CMD命令

因为我要连接ViewServer,所以得实现执行cmd命令。方法如下:

[java] view
plaincopy





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()。

[java] view
plaincopy





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里的方法,不多解释了。

[java] view
plaincopy





/*

* 获取控件信息

*/

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,从得到的数据中提取有用的信息。

[html] view
plaincopy





..............

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值可以得到当前窗口的其他信息。

[java] view
plaincopy





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命令然后删选这些信息。

[java] view
plaincopy





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;

}

这样我们就得到了我们需要的信息,测试一下,命令行输出如下:

[java] view
plaincopy





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对象啦。

[java] view
plaincopy





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的。

得到这些以后,我们就可以以控件名为关键字分类处理:

[java] view
plaincopy





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:

[java] view
plaincopy





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里所有的视图的。

[java] view
plaincopy





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不适合做这个工具,我对比了一下总结如下:

总结



转自:http://blog.csdn.net/itfootball/article/details/21788573
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐