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

       在上一篇我简单的了解了一下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的。

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

 

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



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

      

        之前试过自己本地的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自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(2)_第2张图片

       

        告一段落,继续往下研究。

下一篇:

Android自动化测试中AccessibilityService获取控件信息(1)

 

你可能感兴趣的:(android自动化,hierarchyviewer)