diff options
author | Guang Zhu <guangzhu@google.com> | 2012-07-27 15:55:35 -0700 |
---|---|---|
committer | Guang Zhu <guangzhu@google.com> | 2012-07-27 15:55:35 -0700 |
commit | 428f10724739d2cc7f4bbbd7add6bdf8229d1294 (patch) | |
tree | fb90801f360dc2505120e4656653c37e2a8c567a /uiautomatorviewer | |
parent | 3d1fd6f05a58463a8d56f7ba374240a093a061d0 (diff) | |
download | sdk-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')
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 Binary files differnew file mode 100644 index 0000000..7bdf83d --- /dev/null +++ b/uiautomatorviewer/src/images/expandall.png diff --git a/uiautomatorviewer/src/images/open-folder.png b/uiautomatorviewer/src/images/open-folder.png Binary files differnew file mode 100644 index 0000000..8c4a2e1 --- /dev/null +++ b/uiautomatorviewer/src/images/open-folder.png diff --git a/uiautomatorviewer/src/images/screenshot.png b/uiautomatorviewer/src/images/screenshot.png Binary files differnew file mode 100644 index 0000000..423f781 --- /dev/null +++ b/uiautomatorviewer/src/images/screenshot.png diff --git a/uiautomatorviewer/src/images/warning.png b/uiautomatorviewer/src/images/warning.png Binary files differnew file mode 100644 index 0000000..ca3b6ed --- /dev/null +++ b/uiautomatorviewer/src/images/warning.png |