1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.uiautomator; 18 19 import com.android.uiautomator.tree.BasicTreeNode; 20 import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener; 21 import com.android.uiautomator.tree.UiHierarchyXmlLoader; 22 import com.android.uiautomator.tree.UiNode; 23 24 import org.eclipse.swt.SWTException; 25 import org.eclipse.swt.graphics.Image; 26 import org.eclipse.swt.graphics.ImageData; 27 import org.eclipse.swt.graphics.ImageLoader; 28 import org.eclipse.swt.graphics.Rectangle; 29 30 import java.io.File; 31 import java.util.ArrayList; 32 import java.util.List; 33 34 public class UiAutomatorModel { 35 36 private static UiAutomatorModel inst = null; 37 38 private File mScreenshotFile, mXmlDumpFile; 39 private UiAutomatorViewer mView; 40 private Image mScreenshot; 41 private BasicTreeNode mRootNode; 42 private BasicTreeNode mSelectedNode; 43 private Rectangle mCurrentDrawingRect; 44 private List<Rectangle> mNafNodes; 45 private List<File> mTmpDirs; 46 47 // determines whether we lookup the leaf UI node on mouse move of screenshot image 48 private boolean mExploreMode = true; 49 50 private boolean mShowNafNodes = false; 51 52 private UiAutomatorModel(UiAutomatorViewer view) { 53 mView = view; 54 mTmpDirs = new ArrayList<File>(); 55 } 56 57 public static UiAutomatorModel createInstance(UiAutomatorViewer view) { 58 if (inst != null) { 59 throw new IllegalStateException("instance already created!"); 60 } 61 inst = new UiAutomatorModel(view); 62 return inst; 63 } 64 65 public static UiAutomatorModel getModel() { 66 if (inst == null) { 67 throw new IllegalStateException("instance not created yet!"); 68 } 69 return inst; 70 } 71 72 public File getScreenshotFile() { 73 return mScreenshotFile; 74 } 75 76 public File getXmlDumpFile() { 77 return mXmlDumpFile; 78 } 79 80 public boolean loadScreenshotAndXmlDump(File screenshotFile, File xmlDumpFile) { 81 if (screenshotFile != null && xmlDumpFile != null 82 && screenshotFile.isFile() && xmlDumpFile.isFile()) { 83 ImageData[] data = null; 84 Image img = null; 85 try { 86 // use SWT's ImageLoader to read png from path 87 data = new ImageLoader().load(screenshotFile.getAbsolutePath()); 88 } catch (SWTException e) { 89 e.printStackTrace(); 90 return false; 91 } 92 // "data" is an array, probably used to handle images that has multiple frames 93 // i.e. gifs or icons, we just care if it has at least one here 94 if (data.length < 1) return false; 95 UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader(); 96 BasicTreeNode rootNode = loader.parseXml(xmlDumpFile 97 .getAbsolutePath()); 98 if (rootNode == null) { 99 System.err.println("null rootnode after parsing."); 100 return false; 101 } 102 mNafNodes = loader.getNafNodes(); 103 try { 104 // Image is tied to ImageData and a Display, so we only need to create once 105 // per new image 106 img = new Image(mView.getShell().getDisplay(), data[0]); 107 } catch (SWTException e) { 108 e.printStackTrace(); 109 return false; 110 } 111 // only update screenhot and xml if both are loaded successfully 112 if (mScreenshot != null) { 113 mScreenshot.dispose(); 114 } 115 mScreenshot = img; 116 if (mRootNode != null) { 117 mRootNode.clearAllChildren(); 118 } 119 // TODO: we should verify here if the coordinates in the XML matches the png 120 // or not: think loading a phone screenshot with a tablet XML dump 121 mRootNode = rootNode; 122 mScreenshotFile = screenshotFile; 123 mXmlDumpFile = xmlDumpFile; 124 mExploreMode = true; 125 mView.loadScreenshotAndXml(); 126 return true; 127 } 128 return false; 129 } 130 131 public BasicTreeNode getXmlRootNode() { 132 return mRootNode; 133 } 134 135 public Image getScreenshot() { 136 return mScreenshot; 137 } 138 139 public BasicTreeNode getSelectedNode() { 140 return mSelectedNode; 141 } 142 143 /** 144 * change node selection in the Model recalculate the rect to highlight, 145 * also notifies the View to refresh accordingly 146 * 147 * @param node 148 */ 149 public void setSelectedNode(BasicTreeNode node) { 150 mSelectedNode = node; 151 if (mSelectedNode != null && mSelectedNode instanceof UiNode) { 152 UiNode uiNode = (UiNode) mSelectedNode; 153 mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height); 154 } else { 155 mCurrentDrawingRect = null; 156 } 157 mView.updateScreenshot(); 158 if (mSelectedNode != null) { 159 mView.loadAttributeTable(); 160 } 161 } 162 163 public Rectangle getCurrentDrawingRect() { 164 return mCurrentDrawingRect; 165 } 166 167 /** 168 * Do a search in tree to find a leaf node or deepest parent node containing the coordinate 169 * 170 * @param x 171 * @param y 172 */ 173 public void updateSelectionForCoordinates(int x, int y) { 174 if (mRootNode == null) 175 return; 176 MinAreaFindNodeListener listener = new MinAreaFindNodeListener(); 177 boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener); 178 if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) { 179 mView.updateTreeSelection(listener.mNode); 180 } 181 } 182 183 public boolean isExploreMode() { 184 return mExploreMode; 185 } 186 187 public void toggleExploreMode() { 188 mExploreMode = !mExploreMode; 189 mView.updateScreenshot(); 190 } 191 192 public void setExploreMode(boolean exploreMode) { 193 mExploreMode = exploreMode; 194 } 195 196 private static class MinAreaFindNodeListener implements IFindNodeListener { 197 BasicTreeNode mNode = null; 198 @Override 199 public void onFoundNode(BasicTreeNode node) { 200 if (mNode == null) { 201 mNode = node; 202 } else { 203 if ((node.height * node.width) < (mNode.height * mNode.width)) { 204 mNode = node; 205 } 206 } 207 } 208 } 209 210 public List<Rectangle> getNafNodes() { 211 return mNafNodes; 212 } 213 214 public void toggleShowNaf() { 215 mShowNafNodes = !mShowNafNodes; 216 mView.updateScreenshot(); 217 } 218 219 public boolean shouldShowNafNodes() { 220 return mShowNafNodes; 221 } 222 223 /** 224 * Registers a temporary directory for deletion when app exists 225 * 226 * @param tmpDir 227 */ 228 public void registerTempDirectory(File tmpDir) { 229 mTmpDirs.add(tmpDir); 230 } 231 232 /** 233 * Performs cleanup tasks when the app is exiting 234 */ 235 public void cleanUp() { 236 for (File dir : mTmpDirs) { 237 Utils.deleteRecursive(dir); 238 } 239 } 240 } 241