aboutsummaryrefslogtreecommitdiffstats
path: root/uiautomatorviewer
diff options
context:
space:
mode:
authorGuang Zhu <guangzhu@google.com>2012-07-27 15:55:35 -0700
committerGuang Zhu <guangzhu@google.com>2012-07-27 15:55:35 -0700
commit428f10724739d2cc7f4bbbd7add6bdf8229d1294 (patch)
treefb90801f360dc2505120e4656653c37e2a8c567a /uiautomatorviewer
parent3d1fd6f05a58463a8d56f7ba374240a093a061d0 (diff)
downloadsdk-428f10724739d2cc7f4bbbd7add6bdf8229d1294.zip
sdk-428f10724739d2cc7f4bbbd7add6bdf8229d1294.tar.gz
sdk-428f10724739d2cc7f4bbbd7add6bdf8229d1294.tar.bz2
moving uiautomatorviewer into sdk
It's currently under frameworks/testing Change-Id: I8444f5c375a605c8c6480a33cee19a6d3d5ad7ca
Diffstat (limited to 'uiautomatorviewer')
-rw-r--r--uiautomatorviewer/Android.mk36
-rw-r--r--uiautomatorviewer/etc/Android.mk21
-rw-r--r--uiautomatorviewer/etc/manifest.txt2
-rwxr-xr-xuiautomatorviewer/etc/uiautomatorviewer100
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/OpenDialog.java233
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/UiAutomatorModel.java240
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/UiAutomatorViewer.java406
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/Utils.java32
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/actions/ExpandAllAction.java43
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/actions/ImageHelper.java42
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/actions/OpenFilesAction.java48
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/actions/ScreenshotAction.java302
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/actions/ToggleNafAction.java42
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/AttributePair.java26
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNode.java114
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java63
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/RootWindowNode.java42
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/UiHierarchyXmlLoader.java139
-rw-r--r--uiautomatorviewer/src/com/android/uiautomator/tree/UiNode.java123
-rw-r--r--uiautomatorviewer/src/images/expandall.pngbin0 -> 268 bytes
-rw-r--r--uiautomatorviewer/src/images/open-folder.pngbin0 -> 383 bytes
-rw-r--r--uiautomatorviewer/src/images/screenshot.pngbin0 -> 1226 bytes
-rw-r--r--uiautomatorviewer/src/images/warning.pngbin0 -> 147 bytes
23 files changed, 2054 insertions, 0 deletions
diff --git a/uiautomatorviewer/Android.mk b/uiautomatorviewer/Android.mk
new file mode 100644
index 0000000..a5bc768
--- /dev/null
+++ b/uiautomatorviewer/Android.mk
@@ -0,0 +1,36 @@
+# Copyright (C) 2012 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_JAVA_RESOURCE_DIRS := src
+
+LOCAL_JAR_MANIFEST := etc/manifest.txt
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_JAVA_LIBRARIES := \
+ swt \
+ org.eclipse.jface_3.6.2.M20110210-1200 \
+ org.eclipse.core.commands_3.6.0.I20100512-1500 \
+ org.eclipse.equinox.common_3.6.0.v20100503
+
+LOCAL_MODULE := uiautomatorviewer
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/uiautomatorviewer/etc/Android.mk b/uiautomatorviewer/etc/Android.mk
new file mode 100644
index 0000000..55f326d
--- /dev/null
+++ b/uiautomatorviewer/etc/Android.mk
@@ -0,0 +1,21 @@
+# Copyright (C) 2012 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PREBUILT_EXECUTABLES := uiautomatorviewer
+include $(BUILD_HOST_PREBUILT)
diff --git a/uiautomatorviewer/etc/manifest.txt b/uiautomatorviewer/etc/manifest.txt
new file mode 100644
index 0000000..a606962
--- /dev/null
+++ b/uiautomatorviewer/etc/manifest.txt
@@ -0,0 +1,2 @@
+Main-Class: com.android.uiautomatorviewer.UiAutomatorViewer
+Class-Path: org.eclipse.jface_3.6.2.M20110210-1200.jar org.eclipse.core.commands_3.6.0.I20100512-1500.jar org.eclipse.equinox.common_3.6.0.v20100503.jar
diff --git a/uiautomatorviewer/etc/uiautomatorviewer b/uiautomatorviewer/etc/uiautomatorviewer
new file mode 100755
index 0000000..605b81c
--- /dev/null
+++ b/uiautomatorviewer/etc/uiautomatorviewer
@@ -0,0 +1,100 @@
+#!/bin/sh
+# Copyright 2012, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+
+prog="$0"
+while [ -h "${prog}" ]; do
+ newProg=`/bin/ls -ld "${prog}"`
+ newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+ if expr "x${newProg}" : 'x/' >/dev/null; then
+ prog="${newProg}"
+ else
+ progdir=`dirname "${prog}"`
+ prog="${progdir}/${newProg}"
+ fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=uiautomatorviewer.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/tools/lib
+ libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/framework
+ libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ echo `basename "$prog"`": can't find $jarfile"
+ exit 1
+fi
+
+javaCmd="java"
+
+# Mac OS X needs an additional arg, or you get an "illegal thread" complaint.
+if [ `uname` = "Darwin" ]; then
+ os_opts="-XstartOnFirstThread"
+else
+ os_opts=
+fi
+
+if [ `uname` = "Linux" ]; then
+ export GDK_NATIVE_WINDOWS=true
+fi
+
+jarpath="$frameworkdir/$jarfile"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+ swtpath="$ANDROID_SWT"
+else
+ vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+ if [ -n "$ANDROID_BUILD_TOP" ]; then
+ osname=`uname -s | tr A-Z a-z`
+ swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+ else
+ swtpath="${frameworkdir}/${vmarch}"
+ fi
+fi
+
+if [ ! -d "$swtpath" ]; then
+ echo "SWT folder '${swtpath}' does not exist."
+ echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+ exit 1
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec "$javaCmd" \
+ -Xmx512M $os_opts $java_debug \
+ -classpath "$jarpath:$swtpath/swt.jar" \
+ com.android.uiautomator.UiAutomatorViewer "$@"
diff --git a/uiautomatorviewer/src/com/android/uiautomator/OpenDialog.java b/uiautomatorviewer/src/com/android/uiautomator/OpenDialog.java
new file mode 100644
index 0000000..a2a042d
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/OpenDialog.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/**
+ * Implements a file selection dialog for both screen shot and xml dump file
+ *
+ * "OK" button won't be enabled unless both files are selected
+ * It also has a convenience feature such that if one file has been picked, and the other
+ * file path is empty, then selection for the other file will start from the same base folder
+ *
+ */
+public class OpenDialog extends Dialog {
+
+ private static final int FIXED_TEXT_FIELD_WIDTH = 300;
+ private static final int DEFAULT_LAYOUT_SPACING = 10;
+ private Text mScreenshotText;
+ private Text mXmlText;
+ private File mScreenshotFile;
+ private File mXmlDumpFile;
+ private boolean mFileChanged = false;
+ private Button mOkButton;
+
+ /**
+ * Create the dialog.
+ * @param parentShell
+ */
+ public OpenDialog(Shell parentShell) {
+ super(parentShell);
+ setShellStyle(SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Create contents of the dialog.
+ * @param parent
+ */
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ loadDataFromModel();
+
+ Composite container = (Composite) super.createDialogArea(parent);
+ GridLayout gl_container = new GridLayout(1, false);
+ gl_container.verticalSpacing = DEFAULT_LAYOUT_SPACING;
+ gl_container.horizontalSpacing = DEFAULT_LAYOUT_SPACING;
+ gl_container.marginWidth = DEFAULT_LAYOUT_SPACING;
+ gl_container.marginHeight = DEFAULT_LAYOUT_SPACING;
+ container.setLayout(gl_container);
+
+ Group openScreenshotGroup = new Group(container, SWT.NONE);
+ openScreenshotGroup.setLayout(new GridLayout(2, false));
+ openScreenshotGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ openScreenshotGroup.setText("Screenshot");
+
+ mScreenshotText = new Text(openScreenshotGroup, SWT.BORDER | SWT.READ_ONLY);
+ if (mScreenshotFile != null) {
+ mScreenshotText.setText(mScreenshotFile.getAbsolutePath());
+ }
+ GridData gd_screenShotText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+ gd_screenShotText.minimumWidth = FIXED_TEXT_FIELD_WIDTH;
+ gd_screenShotText.widthHint = FIXED_TEXT_FIELD_WIDTH;
+ mScreenshotText.setLayoutData(gd_screenShotText);
+
+ Button openScreenshotButton = new Button(openScreenshotGroup, SWT.NONE);
+ openScreenshotButton.setText("...");
+ openScreenshotButton.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ handleOpenScreenshotFile();
+ }
+ });
+
+ Group openXmlGroup = new Group(container, SWT.NONE);
+ openXmlGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ openXmlGroup.setText("UI XML Dump");
+ openXmlGroup.setLayout(new GridLayout(2, false));
+
+ mXmlText = new Text(openXmlGroup, SWT.BORDER | SWT.READ_ONLY);
+ mXmlText.setEditable(false);
+ if (mXmlDumpFile != null) {
+ mXmlText.setText(mXmlDumpFile.getAbsolutePath());
+ }
+ GridData gd_xmlText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+ gd_xmlText.minimumWidth = FIXED_TEXT_FIELD_WIDTH;
+ gd_xmlText.widthHint = FIXED_TEXT_FIELD_WIDTH;
+ mXmlText.setLayoutData(gd_xmlText);
+
+ Button openXmlButton = new Button(openXmlGroup, SWT.NONE);
+ openXmlButton.setText("...");
+ openXmlButton.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ handleOpenXmlDumpFile();
+ }
+ });
+
+ return container;
+ }
+
+ /**
+ * Create contents of the button bar.
+ * @param parent
+ */
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+ updateButtonState();
+ }
+
+ /**
+ * Return the initial size of the dialog.
+ */
+ @Override
+ protected Point getInitialSize() {
+ return new Point(368, 233);
+ }
+
+ @Override
+ protected void configureShell(Shell newShell) {
+ super.configureShell(newShell);
+ newShell.setText("Open UI Dump Files");
+ }
+
+ private void loadDataFromModel() {
+ mScreenshotFile = UiAutomatorModel.getModel().getScreenshotFile();
+ mXmlDumpFile = UiAutomatorModel.getModel().getXmlDumpFile();
+ }
+
+ private void handleOpenScreenshotFile() {
+ FileDialog fd = new FileDialog(getShell(), SWT.OPEN);
+ fd.setText("Open Screenshot File");
+ File initialFile = mScreenshotFile;
+ // if file has never been selected before, try to base initial path on the mXmlDumpFile
+ if (initialFile == null && mXmlDumpFile != null && mXmlDumpFile.isFile()) {
+ initialFile = mXmlDumpFile.getParentFile();
+ }
+ if (initialFile != null) {
+ if (initialFile.isFile()) {
+ fd.setFileName(initialFile.getAbsolutePath());
+ } else if (initialFile.isDirectory()) {
+ fd.setFilterPath(initialFile.getAbsolutePath());
+ }
+ }
+ String[] filter = {"*.png"};
+ fd.setFilterExtensions(filter);
+ String selected = fd.open();
+ if (selected != null) {
+ mScreenshotFile = new File(selected);
+ mScreenshotText.setText(selected);
+ mFileChanged = true;
+ }
+ updateButtonState();
+ }
+
+ private void handleOpenXmlDumpFile() {
+ FileDialog fd = new FileDialog(getShell(), SWT.OPEN);
+ fd.setText("Open UI Dump XML File");
+ File initialFile = mXmlDumpFile;
+ // if file has never been selected before, try to base initial path on the mScreenshotFile
+ if (initialFile == null && mScreenshotFile != null && mScreenshotFile.isFile()) {
+ initialFile = mScreenshotFile.getParentFile();
+ }
+ if (initialFile != null) {
+ if (initialFile.isFile()) {
+ fd.setFileName(initialFile.getAbsolutePath());
+ } else if (initialFile.isDirectory()) {
+ fd.setFilterPath(initialFile.getAbsolutePath());
+ }
+ }
+ String initialPath = mXmlText.getText();
+ if (initialPath.isEmpty() && mScreenshotFile != null && mScreenshotFile.isFile()) {
+ initialPath = mScreenshotFile.getParentFile().getAbsolutePath();
+ }
+ String[] filter = {"*.xml"};
+ fd.setFilterExtensions(filter);
+ String selected = fd.open();
+ if (selected != null) {
+ mXmlDumpFile = new File(selected);
+ mXmlText.setText(selected);
+ mFileChanged = true;
+ }
+ updateButtonState();
+ }
+
+ private void updateButtonState() {
+ mOkButton.setEnabled(mScreenshotFile != null && mXmlDumpFile != null
+ && mScreenshotFile.isFile() && mXmlDumpFile.isFile());
+ }
+
+ public boolean hasFileChanged() {
+ return mFileChanged;
+ }
+
+ public File getScreenshotFile() {
+ return mScreenshotFile;
+ }
+
+ public File getXmlDumpFile() {
+ return mXmlDumpFile;
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorModel.java b/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorModel.java
new file mode 100644
index 0000000..0a1fab0
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorModel.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator;
+
+import com.android.uiautomator.tree.BasicTreeNode;
+import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener;
+import com.android.uiautomator.tree.UiHierarchyXmlLoader;
+import com.android.uiautomator.tree.UiNode;
+
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UiAutomatorModel {
+
+ private static UiAutomatorModel inst = null;
+
+ private File mScreenshotFile, mXmlDumpFile;
+ private UiAutomatorViewer mView;
+ private Image mScreenshot;
+ private BasicTreeNode mRootNode;
+ private BasicTreeNode mSelectedNode;
+ private Rectangle mCurrentDrawingRect;
+ private List<Rectangle> mNafNodes;
+ private List<File> mTmpDirs;
+
+ // determines whether we lookup the leaf UI node on mouse move of screenshot image
+ private boolean mExploreMode = true;
+
+ private boolean mShowNafNodes = false;
+
+ private UiAutomatorModel(UiAutomatorViewer view) {
+ mView = view;
+ mTmpDirs = new ArrayList<File>();
+ }
+
+ public static UiAutomatorModel createInstance(UiAutomatorViewer view) {
+ if (inst != null) {
+ throw new IllegalStateException("instance already created!");
+ }
+ inst = new UiAutomatorModel(view);
+ return inst;
+ }
+
+ public static UiAutomatorModel getModel() {
+ if (inst == null) {
+ throw new IllegalStateException("instance not created yet!");
+ }
+ return inst;
+ }
+
+ public File getScreenshotFile() {
+ return mScreenshotFile;
+ }
+
+ public File getXmlDumpFile() {
+ return mXmlDumpFile;
+ }
+
+ public boolean loadScreenshotAndXmlDump(File screenshotFile, File xmlDumpFile) {
+ if (screenshotFile != null && xmlDumpFile != null
+ && screenshotFile.isFile() && xmlDumpFile.isFile()) {
+ ImageData[] data = null;
+ Image img = null;
+ try {
+ // use SWT's ImageLoader to read png from path
+ data = new ImageLoader().load(screenshotFile.getAbsolutePath());
+ } catch (SWTException e) {
+ e.printStackTrace();
+ return false;
+ }
+ // "data" is an array, probably used to handle images that has multiple frames
+ // i.e. gifs or icons, we just care if it has at least one here
+ if (data.length < 1) return false;
+ UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader();
+ BasicTreeNode rootNode = loader.parseXml(xmlDumpFile
+ .getAbsolutePath());
+ if (rootNode == null) {
+ System.err.println("null rootnode after parsing.");
+ return false;
+ }
+ mNafNodes = loader.getNafNodes();
+ try {
+ // Image is tied to ImageData and a Display, so we only need to create once
+ // per new image
+ img = new Image(mView.getShell().getDisplay(), data[0]);
+ } catch (SWTException e) {
+ e.printStackTrace();
+ return false;
+ }
+ // only update screenhot and xml if both are loaded successfully
+ if (mScreenshot != null) {
+ mScreenshot.dispose();
+ }
+ mScreenshot = img;
+ if (mRootNode != null) {
+ mRootNode.clearAllChildren();
+ }
+ // TODO: we should verify here if the coordinates in the XML matches the png
+ // or not: think loading a phone screenshot with a tablet XML dump
+ mRootNode = rootNode;
+ mScreenshotFile = screenshotFile;
+ mXmlDumpFile = xmlDumpFile;
+ mExploreMode = true;
+ mView.loadScreenshotAndXml();
+ return true;
+ }
+ return false;
+ }
+
+ public BasicTreeNode getXmlRootNode() {
+ return mRootNode;
+ }
+
+ public Image getScreenshot() {
+ return mScreenshot;
+ }
+
+ public BasicTreeNode getSelectedNode() {
+ return mSelectedNode;
+ }
+
+ /**
+ * change node selection in the Model recalculate the rect to highlight,
+ * also notifies the View to refresh accordingly
+ *
+ * @param node
+ */
+ public void setSelectedNode(BasicTreeNode node) {
+ mSelectedNode = node;
+ if (mSelectedNode != null && mSelectedNode instanceof UiNode) {
+ UiNode uiNode = (UiNode) mSelectedNode;
+ mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height);
+ } else {
+ mCurrentDrawingRect = null;
+ }
+ mView.updateScreenshot();
+ if (mSelectedNode != null) {
+ mView.loadAttributeTable();
+ }
+ }
+
+ public Rectangle getCurrentDrawingRect() {
+ return mCurrentDrawingRect;
+ }
+
+ /**
+ * Do a search in tree to find a leaf node or deepest parent node containing the coordinate
+ *
+ * @param x
+ * @param y
+ */
+ public void updateSelectionForCoordinates(int x, int y) {
+ if (mRootNode == null)
+ return;
+ MinAreaFindNodeListener listener = new MinAreaFindNodeListener();
+ boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener);
+ if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) {
+ mView.updateTreeSelection(listener.mNode);
+ }
+ }
+
+ public boolean isExploreMode() {
+ return mExploreMode;
+ }
+
+ public void toggleExploreMode() {
+ mExploreMode = !mExploreMode;
+ mView.updateScreenshot();
+ }
+
+ public void setExploreMode(boolean exploreMode) {
+ mExploreMode = exploreMode;
+ }
+
+ private static class MinAreaFindNodeListener implements IFindNodeListener {
+ BasicTreeNode mNode = null;
+ @Override
+ public void onFoundNode(BasicTreeNode node) {
+ if (mNode == null) {
+ mNode = node;
+ } else {
+ if ((node.height * node.width) < (mNode.height * mNode.width)) {
+ mNode = node;
+ }
+ }
+ }
+ }
+
+ public List<Rectangle> getNafNodes() {
+ return mNafNodes;
+ }
+
+ public void toggleShowNaf() {
+ mShowNafNodes = !mShowNafNodes;
+ mView.updateScreenshot();
+ }
+
+ public boolean shouldShowNafNodes() {
+ return mShowNafNodes;
+ }
+
+ /**
+ * Registers a temporary directory for deletion when app exists
+ *
+ * @param tmpDir
+ */
+ public void registerTempDirectory(File tmpDir) {
+ mTmpDirs.add(tmpDir);
+ }
+
+ /**
+ * Performs cleanup tasks when the app is exiting
+ */
+ public void cleanUp() {
+ for (File dir : mTmpDirs) {
+ Utils.deleteRecursive(dir);
+ }
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorViewer.java b/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorViewer.java
new file mode 100644
index 0000000..9f758ae
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/UiAutomatorViewer.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator;
+
+import com.android.uiautomator.actions.ExpandAllAction;
+import com.android.uiautomator.actions.OpenFilesAction;
+import com.android.uiautomator.actions.ScreenshotAction;
+import com.android.uiautomator.actions.ToggleNafAction;
+import com.android.uiautomator.tree.AttributePair;
+import com.android.uiautomator.tree.BasicTreeNode;
+import com.android.uiautomator.tree.BasicTreeNodeContentProvider;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.window.ApplicationWindow;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Tree;
+
+public class UiAutomatorViewer extends ApplicationWindow {
+
+ private static final int IMG_BORDER = 2;
+
+ private Canvas mScreenshotCanvas;
+ private TreeViewer mTreeViewer;
+
+ private Action mOpenFilesAction;
+ private Action mExpandAllAction;
+ private Action mScreenshotAction;
+ private Action mToggleNafAction;
+ private TableViewer mTableViewer;
+
+ private float mScale = 1.0f;
+ private int mDx, mDy;
+
+ /**
+ * Create the application window.
+ */
+ public UiAutomatorViewer() {
+ super(null);
+ UiAutomatorModel.createInstance(this);
+ createActions();
+ }
+
+ /**
+ * Create contents of the application window.
+ *
+ * @param parent
+ */
+ @Override
+ protected Control createContents(Composite parent) {
+ SashForm baseSash = new SashForm(parent, SWT.HORIZONTAL | SWT.NONE);
+ // draw the canvas with border, so the divider area for sash form can be highlighted
+ mScreenshotCanvas = new Canvas(baseSash, SWT.BORDER);
+ mScreenshotCanvas.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ UiAutomatorModel.getModel().toggleExploreMode();
+ }
+ });
+ mScreenshotCanvas.setBackground(
+ getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
+ mScreenshotCanvas.addPaintListener(new PaintListener() {
+ @Override
+ public void paintControl(PaintEvent e) {
+ Image image = UiAutomatorModel.getModel().getScreenshot();
+ if (image != null) {
+ updateScreenshotTransformation();
+ // shifting the image here, so that there's a border around screen shot
+ // this makes highlighting red rectangles on the screen shot edges more visible
+ Transform t = new Transform(e.gc.getDevice());
+ t.translate(mDx, mDy);
+ t.scale(mScale, mScale);
+ e.gc.setTransform(t);
+ e.gc.drawImage(image, 0, 0);
+ // this resets the transformation to identity transform, i.e. no change
+ // we don't use transformation here because it will cause the line pattern
+ // and line width of highlight rect to be scaled, causing to appear to be blurry
+ e.gc.setTransform(null);
+ if (UiAutomatorModel.getModel().shouldShowNafNodes()) {
+ // highlight the "Not Accessibility Friendly" nodes
+ e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
+ e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
+ for (Rectangle r : UiAutomatorModel.getModel().getNafNodes()) {
+ e.gc.setAlpha(50);
+ e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
+ getScaledSize(r.width), getScaledSize(r.height));
+ e.gc.setAlpha(255);
+ e.gc.setLineStyle(SWT.LINE_SOLID);
+ e.gc.setLineWidth(2);
+ e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
+ getScaledSize(r.width), getScaledSize(r.height));
+ }
+ }
+ // draw the mouseover rects
+ Rectangle rect = UiAutomatorModel.getModel().getCurrentDrawingRect();
+ if (rect != null) {
+ e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED));
+ if (UiAutomatorModel.getModel().isExploreMode()) {
+ // when we highlight nodes dynamically on mouse move,
+ // use dashed borders
+ e.gc.setLineStyle(SWT.LINE_DASH);
+ e.gc.setLineWidth(1);
+ } else {
+ // when highlighting nodes on tree node selection,
+ // use solid borders
+ e.gc.setLineStyle(SWT.LINE_SOLID);
+ e.gc.setLineWidth(2);
+ }
+ e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y),
+ getScaledSize(rect.width), getScaledSize(rect.height));
+ }
+ }
+ }
+ });
+ mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() {
+ @Override
+ public void mouseMove(MouseEvent e) {
+ if (UiAutomatorModel.getModel().isExploreMode()) {
+ UiAutomatorModel.getModel().updateSelectionForCoordinates(
+ getInverseScaledSize(e.x - mDx),
+ getInverseScaledSize(e.y - mDy));
+ }
+ }
+ });
+
+ // right sash is split into 2 parts: upper-right and lower-right
+ // both are composites with borders, so that the horizontal divider can be highlighted by
+ // the borders
+ SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL);
+
+ // upper-right base contains the toolbar and the tree
+ Composite upperRightBase = new Composite(rightSash, SWT.BORDER);
+ upperRightBase.setLayout(new GridLayout(1, false));
+ ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+ toolBarManager.add(mOpenFilesAction);
+ toolBarManager.add(mExpandAllAction);
+ toolBarManager.add(mScreenshotAction);
+ toolBarManager.add(mToggleNafAction);
+ toolBarManager.createControl(upperRightBase);
+
+ mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE);
+ mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider());
+ // default LabelProvider uses toString() to generate text to display
+ mTreeViewer.setLabelProvider(new LabelProvider());
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ if (event.getSelection().isEmpty()) {
+ UiAutomatorModel.getModel().setSelectedNode(null);
+ } else if (event.getSelection() instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+ Object o = selection.toArray()[0];
+ if (o instanceof BasicTreeNode) {
+ UiAutomatorModel.getModel().setSelectedNode((BasicTreeNode)o);
+ }
+ }
+ }
+ });
+ Tree tree = mTreeViewer.getTree();
+ tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+ // move focus so that it's not on tool bar (looks weird)
+ tree.setFocus();
+
+ // lower-right base contains the detail group
+ Composite lowerRightBase = new Composite(rightSash, SWT.BORDER);
+ lowerRightBase.setLayout(new FillLayout());
+ Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE);
+ grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL));
+ grpNodeDetail.setText("Node Detail");
+
+ Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE);
+
+ TableColumnLayout columnLayout = new TableColumnLayout();
+ tableContainer.setLayout(columnLayout);
+
+ mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION);
+ Table table = mTableViewer.getTable();
+ table.setLinesVisible(true);
+ // use ArrayContentProvider here, it assumes the input to the TableViewer
+ // is an array, where each element represents a row in the table
+ mTableViewer.setContentProvider(new ArrayContentProvider());
+
+ TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE);
+ TableColumn tblclmnKey = tableViewerColumnKey.getColumn();
+ tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() {
+ @Override
+ public String getText(Object element) {
+ if (element instanceof AttributePair) {
+ // first column, shows the attribute name
+ return ((AttributePair)element).key;
+ }
+ return super.getText(element);
+ }
+ });
+ columnLayout.setColumnData(tblclmnKey,
+ new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true));
+
+ TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE);
+ tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer));
+ TableColumn tblclmnValue = tableViewerColumnValue.getColumn();
+ columnLayout.setColumnData(tblclmnValue,
+ new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true));
+ tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() {
+ @Override
+ public String getText(Object element) {
+ if (element instanceof AttributePair) {
+ // second column, shows the attribute value
+ return ((AttributePair)element).value;
+ }
+ return super.getText(element);
+ }
+ });
+ // sets the ratio of the vertical split: left 5 vs right 3
+ baseSash.setWeights(new int[]{5, 3});
+ return baseSash;
+ }
+
+ /**
+ * Create the actions.
+ */
+ private void createActions() {
+ mOpenFilesAction = new OpenFilesAction(this);
+ mExpandAllAction = new ExpandAllAction(this);
+ mScreenshotAction = new ScreenshotAction(this);
+ mToggleNafAction = new ToggleNafAction();
+ }
+
+ /**
+ * Launch the application.
+ *
+ * @param args
+ */
+ public static void main(String args[]) {
+ try {
+ UiAutomatorViewer window = new UiAutomatorViewer();
+ window.setBlockOnOpen(true);
+ window.open();
+ UiAutomatorModel.getModel().cleanUp();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Configure the shell.
+ *
+ * @param newShell
+ */
+ @Override
+ protected void configureShell(Shell newShell) {
+ super.configureShell(newShell);
+ newShell.setText("UI Automator Viewer");
+ }
+
+
+ /**
+ * Asks the Model for screenshot and xml tree data, then populates the screenshot
+ * area and tree view accordingly
+ */
+ public void loadScreenshotAndXml() {
+ mScreenshotCanvas.redraw();
+ // load xml into tree
+ BasicTreeNode wrapper = new BasicTreeNode();
+ // putting another root node on top of existing root node
+ // because Tree seems to like to hide the root node
+ wrapper.addChild(UiAutomatorModel.getModel().getXmlRootNode());
+ mTreeViewer.setInput(wrapper);
+ mTreeViewer.getTree().setFocus();
+ }
+
+ /*
+ * Causes a redraw of the canvas.
+ *
+ * The drawing code of canvas will handle highlighted nodes and etc based on data
+ * retrieved from Model
+ */
+ public void updateScreenshot() {
+ mScreenshotCanvas.redraw();
+ }
+
+ public void expandAll() {
+ mTreeViewer.expandAll();
+ }
+
+ public void updateTreeSelection(BasicTreeNode node) {
+ mTreeViewer.setSelection(new StructuredSelection(node), true);
+ }
+
+ public void loadAttributeTable() {
+ // udpate the lower right corner table to show the attributes of the node
+ mTableViewer.setInput(
+ UiAutomatorModel.getModel().getSelectedNode().getAttributesArray());
+ }
+
+ @Override
+ protected Point getInitialSize() {
+ return new Point(800, 600);
+ }
+
+ private void updateScreenshotTransformation() {
+ Rectangle canvas = mScreenshotCanvas.getBounds();
+ Rectangle image = UiAutomatorModel.getModel().getScreenshot().getBounds();
+ float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float)image.width;
+ float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float)image.height;
+ // use the smaller scale here so that we can fit the entire screenshot
+ mScale = Math.min(scaleX, scaleY);
+ // calculate translation values to center the image on the canvas
+ mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER;
+ mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER;
+ }
+
+ private int getScaledSize(int size) {
+ if (mScale == 1.0f) {
+ return size;
+ } else {
+ return new Double(Math.floor((size * mScale))).intValue();
+ }
+ }
+
+ private int getInverseScaledSize(int size) {
+ if (mScale == 1.0f) {
+ return size;
+ } else {
+ return new Double(Math.floor((size / mScale))).intValue();
+ }
+ }
+
+ private class AttributeTableEditingSupport extends EditingSupport {
+
+ private TableViewer mViewer;
+
+ public AttributeTableEditingSupport(TableViewer viewer) {
+ super(viewer);
+ mViewer = viewer;
+ }
+
+ @Override
+ protected boolean canEdit(Object arg0) {
+ return true;
+ }
+
+ @Override
+ protected CellEditor getCellEditor(Object arg0) {
+ return new TextCellEditor(mViewer.getTable());
+ }
+
+ @Override
+ protected Object getValue(Object o) {
+ return ((AttributePair)o).value;
+ }
+
+ @Override
+ protected void setValue(Object arg0, Object arg1) {
+ }
+
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/Utils.java b/uiautomatorviewer/src/com/android/uiautomator/Utils.java
new file mode 100644
index 0000000..5306fe3
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/Utils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator;
+
+import java.io.File;
+
+public class Utils {
+ public static void deleteRecursive(File file) {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ for (File child : children) {
+ if (!child.getName().startsWith("."))
+ deleteRecursive(child);
+ }
+ }
+ file.delete();
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/actions/ExpandAllAction.java b/uiautomatorviewer/src/com/android/uiautomator/actions/ExpandAllAction.java
new file mode 100644
index 0000000..3c73fdc
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/actions/ExpandAllAction.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.actions;
+
+import com.android.uiautomator.UiAutomatorViewer;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+public class ExpandAllAction extends Action {
+
+ UiAutomatorViewer mWindow;
+
+ public ExpandAllAction(UiAutomatorViewer window) {
+ super("&Expand All");
+ mWindow = window;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return ImageHelper.loadImageDescriptorFromResource("images/expandall.png");
+ }
+
+ @Override
+ public void run() {
+ mWindow.expandAll();
+ }
+
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/actions/ImageHelper.java b/uiautomatorviewer/src/com/android/uiautomator/actions/ImageHelper.java
new file mode 100644
index 0000000..c22f1fd
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/actions/ImageHelper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.actions;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+
+import java.io.InputStream;
+
+public class ImageHelper {
+
+ public static ImageDescriptor loadImageDescriptorFromResource(String path) {
+ InputStream is = ImageHelper.class.getClassLoader().getResourceAsStream(path);
+ if (is != null) {
+ ImageData[] data = null;
+ try {
+ data = new ImageLoader().load(is);
+ } catch (SWTException e) {
+ }
+ if (data != null && data.length > 0) {
+ return ImageDescriptor.createFromImageData(data[0]);
+ }
+ }
+ return null;
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/actions/OpenFilesAction.java b/uiautomatorviewer/src/com/android/uiautomator/actions/OpenFilesAction.java
new file mode 100644
index 0000000..3232857
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/actions/OpenFilesAction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.actions;
+
+import com.android.uiautomator.OpenDialog;
+import com.android.uiautomator.UiAutomatorModel;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.window.ApplicationWindow;
+
+public class OpenFilesAction extends Action {
+
+ ApplicationWindow mWindow;
+
+ public OpenFilesAction(ApplicationWindow window) {
+ super("&Open");
+ mWindow = window;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return ImageHelper.loadImageDescriptorFromResource("images/open-folder.png");
+ }
+
+ @Override
+ public void run() {
+ OpenDialog d = new OpenDialog(mWindow.getShell());
+ if (d.open() == OpenDialog.OK) {
+ UiAutomatorModel.getModel().loadScreenshotAndXmlDump(
+ d.getScreenshotFile(), d.getXmlDumpFile());
+ }
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/actions/ScreenshotAction.java b/uiautomatorviewer/src/com/android/uiautomator/actions/ScreenshotAction.java
new file mode 100644
index 0000000..7d1eaa3
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/actions/ScreenshotAction.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.actions;
+
+import com.android.uiautomator.UiAutomatorModel;
+import com.android.uiautomator.UiAutomatorViewer;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ScreenshotAction extends Action {
+
+ UiAutomatorViewer mViewer;
+
+ public ScreenshotAction(UiAutomatorViewer viewer) {
+ super("&Device Screenshot");
+ mViewer = viewer;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png");
+ }
+
+ @Override
+ public void run() {
+ ProgressMonitorDialog dialog = new ProgressMonitorDialog(mViewer.getShell());
+ try {
+ dialog.run(true, false, new IRunnableWithProgress() {
+ private void showError(final String msg, final Throwable t,
+ IProgressMonitor monitor) {
+ monitor.done();
+ mViewer.getShell().getDisplay().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ Status s = new Status(IStatus.ERROR, "Screenshot", msg, t);
+ ErrorDialog.openError(
+ mViewer.getShell(), "Error", "Cannot take screenshot", s);
+ }
+ });
+ }
+
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException,
+ InterruptedException {
+ ProcRunner procRunner = null;
+ String serial = System.getenv("ANDROID_SERIAL");
+ File tmpDir = null;
+ File xmlDumpFile = null;
+ File screenshotFile = null;
+ int retCode = -1;
+ try {
+ tmpDir = File.createTempFile("uiautomatorviewer_", "");
+ tmpDir.delete();
+ if (!tmpDir.mkdirs())
+ throw new IOException("Failed to mkdir");
+ xmlDumpFile = File.createTempFile("dump_", ".xml", tmpDir);
+ screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir);
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Cannot get temp directory", e, monitor);
+ return;
+ }
+ UiAutomatorModel.getModel().registerTempDirectory(tmpDir);
+
+ // boiler plates to do a bunch of adb stuff to take XML snapshot and screenshot
+ monitor.beginTask("Getting UI status dump from device...",
+ IProgressMonitor.UNKNOWN);
+ monitor.subTask("Detecting device...");
+ procRunner = getAdbRunner(serial, "shell", "ls", "/system/bin/uiautomator");
+ try {
+ retCode = procRunner.run(30000);
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to detect device", e, monitor);
+ return;
+ }
+ if (retCode != 0) {
+ showError("No device or multiple devices connected. "
+ + "Use ANDROID_SERIAL environment variable "
+ + "if you have multiple devices", null, monitor);
+ return;
+ }
+ if (procRunner.getOutputBlob().indexOf("No such file or directory") != -1) {
+ showError("/system/bin/uiautomator not found on device", null, monitor);
+ return;
+ }
+ monitor.subTask("Deleting old UI XML snapshot ...");
+ procRunner = getAdbRunner(serial,
+ "shell", "rm", "/sdcard/uidump.xml");
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException(
+ "Non-zero return code from \"rm\" xml dump command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to execute \"rm\" xml dump command.", e, monitor);
+ return;
+ }
+
+ monitor.subTask("Taking UI XML snapshot...");
+ procRunner = getAdbRunner(serial,
+ "shell", "/system/bin/uiautomator", "dump", "/sdcard/uidump.xml");
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException("Non-zero return code from dump command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to execute dump command.", e, monitor);
+ return;
+ }
+ procRunner = getAdbRunner(serial,
+ "pull", "/sdcard/uidump.xml", xmlDumpFile.getAbsolutePath());
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException("Non-zero return code from pull command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to pull dump file.", e, monitor);
+ return;
+ }
+
+ monitor.subTask("Deleting old device screenshot...");
+ procRunner = getAdbRunner(serial,
+ "shell", "rm", "/sdcard/screenshot.png");
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException(
+ "Non-zero return code from \"rm\" screenshot command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to execute \"rm\" screenshot command.", e, monitor);
+ return;
+ }
+
+ monitor.subTask("Taking device screenshot...");
+ procRunner = getAdbRunner(serial,
+ "shell", "screencap", "-p", "/sdcard/screenshot.png");
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException("Non-zero return code from screenshot command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to execute screenshot command.", e, monitor);
+ return;
+ }
+ procRunner = getAdbRunner(serial,
+ "pull", "/sdcard/screenshot.png", screenshotFile.getAbsolutePath());
+ try {
+ retCode = procRunner.run(30000);
+ if (retCode != 0) {
+ throw new IOException("Non-zero return code from pull command:\n"
+ + procRunner.getOutputBlob());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ showError("Failed to pull dump file.", e, monitor);
+ return;
+ }
+ final File png = screenshotFile, xml = xmlDumpFile;
+ if(png.length() == 0) {
+ showError("Screenshot file size is 0", null, monitor);
+ return;
+ } else {
+ mViewer.getShell().getDisplay().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ UiAutomatorModel.getModel().loadScreenshotAndXmlDump(png, xml);
+ }
+ });
+ }
+ monitor.done();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /*
+ * Convenience function to construct an 'adb' command, e.g. use 'adb' or 'adb -s NNN'
+ */
+ private ProcRunner getAdbRunner(String serial, String... command) {
+ List<String> cmd = new ArrayList<String>();
+ cmd.add("adb");
+ if (serial != null) {
+ cmd.add("-s");
+ cmd.add(serial);
+ }
+ for (String s : command) {
+ cmd.add(s);
+ }
+ return new ProcRunner(cmd);
+ }
+
+ /**
+ * Convenience class to run external process.
+ *
+ * Always redirects stderr into stdout, has timeout control
+ *
+ */
+ private static class ProcRunner {
+
+ ProcessBuilder mProcessBuilder;
+
+ List<String> mOutput = new ArrayList<String>();
+
+ public ProcRunner(List<String> command) {
+ mProcessBuilder = new ProcessBuilder(command).redirectErrorStream(true);
+ }
+
+ public int run(long timeout) throws IOException {
+ final Process p = mProcessBuilder.start();
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ String line;
+ mOutput.clear();
+ try {
+ BufferedReader br = new BufferedReader(new InputStreamReader(
+ p.getInputStream()));
+ while ((line = br.readLine()) != null) {
+ mOutput.add(line);
+ }
+ br.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ };
+ };
+ t.start();
+ try {
+ t.join(timeout);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (t.isAlive()) {
+ throw new IOException("external process not terminating.");
+ }
+ try {
+ return p.waitFor();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ throw new IOException(e);
+ }
+ }
+
+ public String getOutputBlob() {
+ StringBuilder sb = new StringBuilder();
+ for (String line : mOutput) {
+ sb.append(line);
+ sb.append(System.getProperty("line.separator"));
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/actions/ToggleNafAction.java b/uiautomatorviewer/src/com/android/uiautomator/actions/ToggleNafAction.java
new file mode 100644
index 0000000..afc422d
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/actions/ToggleNafAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.actions;
+
+import com.android.uiautomator.UiAutomatorModel;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+public class ToggleNafAction extends Action {
+
+ public ToggleNafAction() {
+ super("&Toggle NAF Nodes", IAction.AS_CHECK_BOX);
+ setChecked(UiAutomatorModel.getModel().shouldShowNafNodes());
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return ImageHelper.loadImageDescriptorFromResource("images/warning.png");
+ }
+
+ @Override
+ public void run() {
+ UiAutomatorModel.getModel().toggleShowNaf();
+ setChecked(UiAutomatorModel.getModel().shouldShowNafNodes());
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/AttributePair.java b/uiautomatorviewer/src/com/android/uiautomator/tree/AttributePair.java
new file mode 100644
index 0000000..ef59544
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/AttributePair.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+public class AttributePair {
+ public String key, value;
+
+ public AttributePair(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNode.java b/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNode.java
new file mode 100644
index 0000000..99434d1
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNode.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class BasicTreeNode {
+
+ private static final BasicTreeNode[] CHILDREN_TEMPLATE = new BasicTreeNode[] {};
+
+ protected BasicTreeNode mParent;
+
+ protected final List<BasicTreeNode> mChildren = new ArrayList<BasicTreeNode>();
+
+ public int x, y, width, height;
+
+ // whether the boundary fields are applicable for the node or not
+ // RootWindowNode has no bounds, but UiNodes should
+ protected boolean mHasBounds = false;
+
+ public void addChild(BasicTreeNode child) {
+ if (child == null) {
+ throw new NullPointerException("Cannot add null child");
+ }
+ if (mChildren.contains(child)) {
+ throw new IllegalArgumentException("node already a child");
+ }
+ mChildren.add(child);
+ child.mParent = this;
+ }
+
+ public List<BasicTreeNode> getChildrenList() {
+ return Collections.unmodifiableList(mChildren);
+ }
+
+ public BasicTreeNode[] getChildren() {
+ return mChildren.toArray(CHILDREN_TEMPLATE);
+ }
+
+ public BasicTreeNode getParent() {
+ return mParent;
+ }
+
+ public boolean hasChild() {
+ return mChildren.size() != 0;
+ }
+
+ public int getChildCount() {
+ return mChildren.size();
+ }
+
+ public void clearAllChildren() {
+ for (BasicTreeNode child : mChildren) {
+ child.clearAllChildren();
+ }
+ mChildren.clear();
+ }
+
+ /**
+ *
+ * Find nodes in the tree containing the coordinate
+ *
+ * The found node should have bounds covering the coordinate, and none of its children's
+ * bounds covers it. Depending on the layout, some app may have multiple nodes matching it,
+ * the caller must provide a {@link IFindNodeListener} to receive all found nodes
+ *
+ * @param px
+ * @param py
+ * @return
+ */
+ public boolean findLeafMostNodesAtPoint(int px, int py, IFindNodeListener listener) {
+ boolean foundInChild = false;
+ for (BasicTreeNode node : mChildren) {
+ foundInChild |= node.findLeafMostNodesAtPoint(px, py, listener);
+ }
+ // checked all children, if at least one child covers the point, return directly
+ if (foundInChild) return true;
+ // check self if the node has no children, or no child nodes covers the point
+ if (mHasBounds) {
+ if (x <= px && px <= x + width && y <= py && py <= y + height) {
+ listener.onFoundNode(this);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public Object[] getAttributesArray () {
+ return null;
+ };
+
+ public static interface IFindNodeListener {
+ void onFoundNode(BasicTreeNode node);
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java b/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java
new file mode 100644
index 0000000..d78ceea
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+public class BasicTreeNodeContentProvider implements ITreeContentProvider {
+
+ private static final Object[] EMPTY_ARRAY = {};
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof BasicTreeNode) {
+ return ((BasicTreeNode)parentElement).getChildren();
+ }
+ return EMPTY_ARRAY;
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof BasicTreeNode) {
+ return ((BasicTreeNode)element).getParent();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof BasicTreeNode) {
+ return ((BasicTreeNode) element).hasChild();
+ }
+ return false;
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/RootWindowNode.java b/uiautomatorviewer/src/com/android/uiautomator/tree/RootWindowNode.java
new file mode 100644
index 0000000..27a21e4
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/RootWindowNode.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+
+
+public class RootWindowNode extends BasicTreeNode {
+
+ private final String mWindowName;
+ private Object[] mCachedAttributesArray;
+
+ public RootWindowNode(String windowName) {
+ mWindowName = windowName;
+ }
+
+ @Override
+ public String toString() {
+ return mWindowName;
+ }
+
+ @Override
+ public Object[] getAttributesArray() {
+ if (mCachedAttributesArray == null) {
+ mCachedAttributesArray = new Object[]{new AttributePair("window-name", mWindowName)};
+ }
+ return mCachedAttributesArray;
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/UiHierarchyXmlLoader.java b/uiautomatorviewer/src/com/android/uiautomator/tree/UiHierarchyXmlLoader.java
new file mode 100644
index 0000000..f2339d1
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/UiHierarchyXmlLoader.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+public class UiHierarchyXmlLoader {
+
+ private BasicTreeNode mRootNode;
+ private List<Rectangle> mNafNodes;
+
+ public UiHierarchyXmlLoader() {
+ }
+
+ /**
+ * Uses a SAX parser to process XML dump
+ * @param xmlPath
+ * @return
+ */
+ public BasicTreeNode parseXml(String xmlPath) {
+ mRootNode = null;
+ mNafNodes = new ArrayList<Rectangle>();
+ // standard boilerplate to get a SAX parser
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser parser = null;
+ try {
+ parser = factory.newSAXParser();
+ } catch (ParserConfigurationException e) {
+ e.printStackTrace();
+ return null;
+ } catch (SAXException e) {
+ e.printStackTrace();
+ return null;
+ }
+ // handler class for SAX parser to receiver standard parsing events:
+ // e.g. on reading "<foo>", startElement is called, on reading "</foo>",
+ // endElement is called
+ DefaultHandler handler = new DefaultHandler(){
+ BasicTreeNode mParentNode;
+ BasicTreeNode mWorkingNode;
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ boolean nodeCreated = false;
+ // starting an element implies that the element that has not yet been closed
+ // will be the parent of the element that is being started here
+ mParentNode = mWorkingNode;
+ if ("hierarchy".equals(qName)) {
+ mWorkingNode = new RootWindowNode(attributes.getValue("windowName"));
+ nodeCreated = true;
+ } else if ("node".equals(qName)) {
+ UiNode tmpNode = new UiNode();
+ for (int i = 0; i < attributes.getLength(); i++) {
+ tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i));
+ }
+ mWorkingNode = tmpNode;
+ nodeCreated = true;
+ // check if current node is NAF
+ String naf = tmpNode.getAttribute("NAF");
+ if ("true".equals(naf)) {
+ mNafNodes.add(new Rectangle(tmpNode.x, tmpNode.y,
+ tmpNode.width, tmpNode.height));
+ }
+ }
+ // nodeCreated will be false if the element started is neither
+ // "hierarchy" nor "node"
+ if (nodeCreated) {
+ if (mRootNode == null) {
+ // this will only happen once
+ mRootNode = mWorkingNode;
+ }
+ if (mParentNode != null) {
+ mParentNode.addChild(mWorkingNode);
+ }
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ //mParentNode should never be null here in a well formed XML
+ if (mParentNode != null) {
+ // closing an element implies that we are back to working on
+ // the parent node of the element just closed, i.e. continue to
+ // parse more child nodes
+ mWorkingNode = mParentNode;
+ mParentNode = mParentNode.getParent();
+ }
+ }
+ };
+ try {
+ parser.parse(new File(xmlPath), handler);
+ } catch (SAXException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return mRootNode;
+ }
+
+ /**
+ * Returns the list of "Not Accessibility Friendly" nodes found during parsing.
+ *
+ * Call this function after parsing
+ *
+ * @return
+ */
+ public List<Rectangle> getNafNodes() {
+ return Collections.unmodifiableList(mNafNodes);
+ }
+}
diff --git a/uiautomatorviewer/src/com/android/uiautomator/tree/UiNode.java b/uiautomatorviewer/src/com/android/uiautomator/tree/UiNode.java
new file mode 100644
index 0000000..4adebf4
--- /dev/null
+++ b/uiautomatorviewer/src/com/android/uiautomator/tree/UiNode.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.tree;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class UiNode extends BasicTreeNode {
+ private static final Pattern BOUNDS_PATTERN = Pattern
+ .compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]");
+ // use LinkedHashMap to preserve the order of the attributes
+ private final Map<String, String> mAttributes = new LinkedHashMap<String, String>();
+ private String mDisplayName = "ShouldNotSeeMe";
+ private Object[] mCachedAttributesArray;
+
+ public void addAtrribute(String key, String value) {
+ mAttributes.put(key, value);
+ updateDisplayName();
+ if ("bounds".equals(key)) {
+ updateBounds(value);
+ }
+ }
+
+ public Map<String, String> getAttributes() {
+ return Collections.unmodifiableMap(mAttributes);
+ }
+
+ /**
+ * Builds the display name based on attributes of the node
+ */
+ private void updateDisplayName() {
+ String className = mAttributes.get("class");
+ if (className == null)
+ return;
+ String text = mAttributes.get("text");
+ if (text == null)
+ return;
+ String contentDescription = mAttributes.get("content-desc");
+ if (contentDescription == null)
+ return;
+ String index = mAttributes.get("index");
+ if (index == null)
+ return;
+ String bounds = mAttributes.get("bounds");
+ if (bounds == null) {
+ return;
+ }
+ // shorten the standard class names, otherwise it takes up too much space on UI
+ className = className.replace("android.widget.", "");
+ className = className.replace("android.view.", "");
+ StringBuilder builder = new StringBuilder();
+ builder.append('(');
+ builder.append(index);
+ builder.append(") ");
+ builder.append(className);
+ if (!text.isEmpty()) {
+ builder.append(':');
+ builder.append(text);
+ }
+ if (!contentDescription.isEmpty()) {
+ builder.append(" {");
+ builder.append(contentDescription);
+ builder.append('}');
+ }
+ builder.append(' ');
+ builder.append(bounds);
+ mDisplayName = builder.toString();
+ }
+
+ private void updateBounds(String bounds) {
+ Matcher m = BOUNDS_PATTERN.matcher(bounds);
+ if (m.matches()) {
+ x = Integer.parseInt(m.group(1));
+ y = Integer.parseInt(m.group(2));
+ width = Integer.parseInt(m.group(3)) - x;
+ height = Integer.parseInt(m.group(4)) - y;
+ mHasBounds = true;
+ } else {
+ throw new RuntimeException("Invalid bounds: " + bounds);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+
+ public String getAttribute(String key) {
+ return mAttributes.get(key);
+ }
+
+ @Override
+ public Object[] getAttributesArray() {
+ // this approach means we do not handle the situation where an attribute is added
+ // after this function is first called. This is currently not a concern because the
+ // tree is supposed to be readonly
+ if (mCachedAttributesArray == null) {
+ mCachedAttributesArray = new Object[mAttributes.size()];
+ int i = 0;
+ for (String attr : mAttributes.keySet()) {
+ mCachedAttributesArray[i++] = new AttributePair(attr, mAttributes.get(attr));
+ }
+ }
+ return mCachedAttributesArray;
+ }
+}
diff --git a/uiautomatorviewer/src/images/expandall.png b/uiautomatorviewer/src/images/expandall.png
new file mode 100644
index 0000000..7bdf83d
--- /dev/null
+++ b/uiautomatorviewer/src/images/expandall.png
Binary files differ
diff --git a/uiautomatorviewer/src/images/open-folder.png b/uiautomatorviewer/src/images/open-folder.png
new file mode 100644
index 0000000..8c4a2e1
--- /dev/null
+++ b/uiautomatorviewer/src/images/open-folder.png
Binary files differ
diff --git a/uiautomatorviewer/src/images/screenshot.png b/uiautomatorviewer/src/images/screenshot.png
new file mode 100644
index 0000000..423f781
--- /dev/null
+++ b/uiautomatorviewer/src/images/screenshot.png
Binary files differ
diff --git a/uiautomatorviewer/src/images/warning.png b/uiautomatorviewer/src/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
--- /dev/null
+++ b/uiautomatorviewer/src/images/warning.png
Binary files differ