1. 程式人生 > >android自動化測試中hierarchyviewer和uiautomatorviewer獲取控制元件資訊的方式比對(2)

android自動化測試中hierarchyviewer和uiautomatorviewer獲取控制元件資訊的方式比對(2)

       在上一篇我簡單的瞭解了一下hierarchyviewer和uiautomatorviewer,如需訪問,點選以下連結:

       通過對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} [email protected]

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



        之前試過自己本地的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不適合做這個工具,我對比了一下總結如下:

  總結


        告一段落,繼續往下研究。

下一篇: