DO NOT MERGE: moving uiautomatorviewer into sdk

It's currently under frameworks/testing

This is cherry-pick'd back to ensure that all branches have a
copy of uiautomatorviewer

Change-Id: I8444f5c375a605c8c6480a33cee19a6d3d5ad7ca
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