1 /* 2 * Copyright 2011, 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.commands.monkey; 18 19 import static com.android.commands.monkey.MonkeySourceNetwork.EARG; 20 21 import android.app.ActivityManager; 22 import android.app.UiAutomation; 23 import android.app.UiAutomationConnection; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.IPackageManager; 26 import android.graphics.Rect; 27 import android.os.HandlerThread; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.view.accessibility.AccessibilityInteractionClient; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.view.accessibility.AccessibilityWindowInfo; 33 34 import com.android.commands.monkey.MonkeySourceNetwork.CommandQueue; 35 import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommand; 36 import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommandReturn; 37 38 import dalvik.system.DexClassLoader; 39 40 import java.lang.reflect.Field; 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Utility class that enables Monkey to perform view introspection when issued Monkey Network 48 * Script commands over the network. 49 */ 50 public class MonkeySourceNetworkViews { 51 52 protected static android.app.UiAutomation sUiTestAutomationBridge; 53 54 private static IPackageManager sPm = 55 IPackageManager.Stub.asInterface(ServiceManager.getService("package")); 56 private static Map<String, Class<?>> sClassMap = new HashMap<String, Class<?>>(); 57 58 private static final String HANDLER_THREAD_NAME = "UiAutomationHandlerThread"; 59 60 private static final String REMOTE_ERROR = 61 "Unable to retrieve application info from PackageManager"; 62 private static final String CLASS_NOT_FOUND = "Error retrieving class information"; 63 private static final String NO_ACCESSIBILITY_EVENT = "No accessibility event has occured yet"; 64 private static final String NO_NODE = "Node with given ID does not exist"; 65 private static final String NO_CONNECTION = "Failed to connect to AccessibilityService, " 66 + "try restarting Monkey"; 67 68 private static final Map<String, ViewIntrospectionCommand> COMMAND_MAP = 69 new HashMap<String, ViewIntrospectionCommand>(); 70 71 /* Interface for view queries */ 72 private static interface ViewIntrospectionCommand { 73 /** 74 * Get the response to the query 75 * @return the response to the query 76 */ query(AccessibilityNodeInfo node, List<String> args)77 public MonkeyCommandReturn query(AccessibilityNodeInfo node, List<String> args); 78 } 79 80 static { 81 COMMAND_MAP.put("getlocation", new GetLocation()); 82 COMMAND_MAP.put("gettext", new GetText()); 83 COMMAND_MAP.put("getclass", new GetClass()); 84 COMMAND_MAP.put("getchecked", new GetChecked()); 85 COMMAND_MAP.put("getenabled", new GetEnabled()); 86 COMMAND_MAP.put("getselected", new GetSelected()); 87 COMMAND_MAP.put("setselected", new SetSelected()); 88 COMMAND_MAP.put("getfocused", new GetFocused()); 89 COMMAND_MAP.put("setfocused", new SetFocused()); 90 COMMAND_MAP.put("getparent", new GetParent()); 91 COMMAND_MAP.put("getchildren", new GetChildren()); 92 COMMAND_MAP.put("getaccessibilityids", new GetAccessibilityIds()); 93 } 94 95 private static final HandlerThread sHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); 96 97 /** 98 * Registers the event listener for AccessibilityEvents. 99 * Also sets up a communication connection so we can query the 100 * accessibility service. 101 */ setup()102 public static void setup() { 103 sHandlerThread.setDaemon(true); 104 sHandlerThread.start(); 105 sUiTestAutomationBridge = new UiAutomation(sHandlerThread.getLooper(), 106 new UiAutomationConnection()); 107 sUiTestAutomationBridge.connect(); 108 } 109 teardown()110 public static void teardown() { 111 sHandlerThread.quit(); 112 } 113 114 /** 115 * Get the ID class for the given package. 116 * This will cause issues if people reload a package with different 117 * resource identifiers, but don't restart the Monkey server. 118 * 119 * @param packageName The package that we want to retrieve the ID class for 120 * @return The ID class for the given package 121 */ getIdClass(String packageName, String sourceDir)122 private static Class<?> getIdClass(String packageName, String sourceDir) 123 throws ClassNotFoundException { 124 // This kind of reflection is expensive, so let's only do it 125 // if we need to 126 Class<?> klass = sClassMap.get(packageName); 127 if (klass == null) { 128 DexClassLoader classLoader = new DexClassLoader( 129 sourceDir, "/data/local/tmp", 130 null, ClassLoader.getSystemClassLoader()); 131 klass = classLoader.loadClass(packageName + ".R$id"); 132 sClassMap.put(packageName, klass); 133 } 134 return klass; 135 } 136 getNodeByAccessibilityIds( String windowString, String viewString)137 private static AccessibilityNodeInfo getNodeByAccessibilityIds( 138 String windowString, String viewString) { 139 int windowId = Integer.parseInt(windowString); 140 int viewId = Integer.parseInt(viewString); 141 int connectionId = sUiTestAutomationBridge.getConnectionId(); 142 AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); 143 return client.findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId, viewId, 144 false, 0, null); 145 } 146 getNodeByViewId(String viewId)147 private static AccessibilityNodeInfo getNodeByViewId(String viewId) throws MonkeyViewException { 148 int connectionId = sUiTestAutomationBridge.getConnectionId(); 149 AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); 150 List<AccessibilityNodeInfo> infos = client.findAccessibilityNodeInfosByViewId( 151 connectionId, AccessibilityWindowInfo.ACTIVE_WINDOW_ID, 152 AccessibilityNodeInfo.ROOT_NODE_ID, viewId); 153 return (!infos.isEmpty()) ? infos.get(0) : null; 154 } 155 156 /** 157 * Command to list all possible view ids for the given application. 158 * This lists all view ids regardless if they are on screen or not. 159 */ 160 public static class ListViewsCommand implements MonkeyCommand { 161 //listviews translateCommand(List<String> command, CommandQueue queue)162 public MonkeyCommandReturn translateCommand(List<String> command, 163 CommandQueue queue) { 164 AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow(); 165 /* Occasionally the API will generate an event with no source, which is essentially the 166 * same as it generating no event at all */ 167 if (node == null) { 168 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT); 169 } 170 String packageName = node.getPackageName().toString(); 171 try{ 172 Class<?> klass; 173 ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, 174 ActivityManager.getCurrentUser()); 175 klass = getIdClass(packageName, appInfo.sourceDir); 176 StringBuilder fieldBuilder = new StringBuilder(); 177 Field[] fields = klass.getFields(); 178 for (Field field : fields) { 179 fieldBuilder.append(field.getName() + " "); 180 } 181 return new MonkeyCommandReturn(true, fieldBuilder.toString()); 182 } catch (RemoteException e){ 183 return new MonkeyCommandReturn(false, REMOTE_ERROR); 184 } catch (ClassNotFoundException e){ 185 return new MonkeyCommandReturn(false, CLASS_NOT_FOUND); 186 } 187 } 188 } 189 190 /** 191 * A command that allows for querying of views. It takes an id type, the requisite ids, 192 * and the command for querying the view. 193 */ 194 public static class QueryViewCommand implements MonkeyCommand { 195 //queryview [id type] [id(s)] [command] 196 //queryview viewid button1 gettext 197 //queryview accessibilityids 12 5 getparent translateCommand(List<String> command, CommandQueue queue)198 public MonkeyCommandReturn translateCommand(List<String> command, 199 CommandQueue queue) { 200 if (command.size() > 2) { 201 String idType = command.get(1); 202 AccessibilityNodeInfo node; 203 String viewQuery; 204 List<String> args; 205 if ("viewid".equals(idType)) { 206 try { 207 node = getNodeByViewId(command.get(2)); 208 viewQuery = command.get(3); 209 args = command.subList(4, command.size()); 210 } catch (MonkeyViewException e) { 211 return new MonkeyCommandReturn(false, e.getMessage()); 212 } 213 } else if (idType.equals("accessibilityids")) { 214 try { 215 node = getNodeByAccessibilityIds(command.get(2), command.get(3)); 216 viewQuery = command.get(4); 217 args = command.subList(5, command.size()); 218 } catch (NumberFormatException e) { 219 return EARG; 220 } 221 } else { 222 return EARG; 223 } 224 if (node == null) { 225 return new MonkeyCommandReturn(false, NO_NODE); 226 } 227 ViewIntrospectionCommand getter = COMMAND_MAP.get(viewQuery); 228 if (getter != null) { 229 return getter.query(node, args); 230 } else { 231 return EARG; 232 } 233 } 234 return EARG; 235 } 236 } 237 238 /** 239 * A command that returns the accessibility ids of the root view. 240 */ 241 public static class GetRootViewCommand implements MonkeyCommand { 242 // getrootview translateCommand(List<String> command, CommandQueue queue)243 public MonkeyCommandReturn translateCommand(List<String> command, 244 CommandQueue queue) { 245 AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow(); 246 return (new GetAccessibilityIds()).query(node, new ArrayList<String>()); 247 } 248 } 249 250 /** 251 * A command that returns the accessibility ids of the views that contain the given text. 252 * It takes a string of text and returns the accessibility ids of the nodes that contain the 253 * text as a list of integers separated by spaces. 254 */ 255 public static class GetViewsWithTextCommand implements MonkeyCommand { 256 // getviewswithtext [text] 257 // getviewswithtext "some text here" translateCommand(List<String> command, CommandQueue queue)258 public MonkeyCommandReturn translateCommand(List<String> command, 259 CommandQueue queue) { 260 if (command.size() == 2) { 261 String text = command.get(1); 262 int connectionId = sUiTestAutomationBridge.getConnectionId(); 263 List<AccessibilityNodeInfo> nodes = AccessibilityInteractionClient.getInstance() 264 .findAccessibilityNodeInfosByText(connectionId, 265 AccessibilityWindowInfo.ACTIVE_WINDOW_ID, 266 AccessibilityNodeInfo.ROOT_NODE_ID, text); 267 ViewIntrospectionCommand idGetter = new GetAccessibilityIds(); 268 List<String> emptyArgs = new ArrayList<String>(); 269 StringBuilder ids = new StringBuilder(); 270 for (AccessibilityNodeInfo node : nodes) { 271 MonkeyCommandReturn result = idGetter.query(node, emptyArgs); 272 if (!result.wasSuccessful()){ 273 return result; 274 } 275 ids.append(result.getMessage()).append(" "); 276 } 277 return new MonkeyCommandReturn(true, ids.toString()); 278 } 279 return EARG; 280 } 281 } 282 283 /** 284 * Command to retrieve the location of the given node. 285 * Returns the x, y, width and height of the view, separated by spaces. 286 */ 287 public static class GetLocation implements ViewIntrospectionCommand { 288 //queryview [id type] [id] getlocation 289 //queryview viewid button1 getlocation query(AccessibilityNodeInfo node, List<String> args)290 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 291 List<String> args) { 292 if (args.size() == 0) { 293 Rect nodePosition = new Rect(); 294 node.getBoundsInScreen(nodePosition); 295 StringBuilder positions = new StringBuilder(); 296 positions.append(nodePosition.left).append(" ").append(nodePosition.top); 297 positions.append(" ").append(nodePosition.right-nodePosition.left).append(" "); 298 positions.append(nodePosition.bottom-nodePosition.top); 299 return new MonkeyCommandReturn(true, positions.toString()); 300 } 301 return EARG; 302 } 303 } 304 305 306 /** 307 * Command to retrieve the text of the given node 308 */ 309 public static class GetText implements ViewIntrospectionCommand { 310 //queryview [id type] [id] gettext 311 //queryview viewid button1 gettext query(AccessibilityNodeInfo node, List<String> args)312 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 313 List<String> args) { 314 if (args.size() == 0) { 315 if (node.isPassword()){ 316 return new MonkeyCommandReturn(false, "Node contains a password"); 317 } 318 /* Occasionally we get a null from the accessibility API, rather than an empty 319 * string */ 320 if (node.getText() == null) { 321 return new MonkeyCommandReturn(true, ""); 322 } 323 return new MonkeyCommandReturn(true, node.getText().toString()); 324 } 325 return EARG; 326 } 327 } 328 329 330 /** 331 * Command to retrieve the class name of the given node 332 */ 333 public static class GetClass implements ViewIntrospectionCommand { 334 //queryview [id type] [id] getclass 335 //queryview viewid button1 getclass query(AccessibilityNodeInfo node, List<String> args)336 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 337 List<String> args) { 338 if (args.size() == 0) { 339 return new MonkeyCommandReturn(true, node.getClassName().toString()); 340 } 341 return EARG; 342 } 343 } 344 /** 345 * Command to retrieve the checked status of the given node 346 */ 347 public static class GetChecked implements ViewIntrospectionCommand { 348 //queryview [id type] [id] getchecked 349 //queryview viewid button1 getchecked query(AccessibilityNodeInfo node, List<String> args)350 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 351 List<String> args) { 352 if (args.size() == 0) { 353 return new MonkeyCommandReturn(true, Boolean.toString(node.isChecked())); 354 } 355 return EARG; 356 } 357 } 358 359 /** 360 * Command to retrieve whether the given node is enabled 361 */ 362 public static class GetEnabled implements ViewIntrospectionCommand { 363 //queryview [id type] [id] getenabled 364 //queryview viewid button1 getenabled query(AccessibilityNodeInfo node, List<String> args)365 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 366 List<String> args) { 367 if (args.size() == 0) { 368 return new MonkeyCommandReturn(true, Boolean.toString(node.isEnabled())); 369 } 370 return EARG; 371 } 372 } 373 374 /** 375 * Command to retrieve whether the given node is selected 376 */ 377 public static class GetSelected implements ViewIntrospectionCommand { 378 //queryview [id type] [id] getselected 379 //queryview viewid button1 getselected query(AccessibilityNodeInfo node, List<String> args)380 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 381 List<String> args) { 382 if (args.size() == 0) { 383 return new MonkeyCommandReturn(true, Boolean.toString(node.isSelected())); 384 } 385 return EARG; 386 } 387 } 388 389 /** 390 * Command to set the selected status of the given node. Takes a boolean value as its only 391 * argument. 392 */ 393 public static class SetSelected implements ViewIntrospectionCommand { 394 //queryview [id type] [id] setselected [boolean] 395 //queryview viewid button1 setselected true query(AccessibilityNodeInfo node, List<String> args)396 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 397 List<String> args) { 398 if (args.size() == 1) { 399 boolean actionPerformed; 400 if (Boolean.valueOf(args.get(0))) { 401 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_SELECT); 402 } else if (!Boolean.valueOf(args.get(0))) { 403 actionPerformed = 404 node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); 405 } else { 406 return EARG; 407 } 408 return new MonkeyCommandReturn(actionPerformed); 409 } 410 return EARG; 411 } 412 } 413 414 /** 415 * Command to get whether the given node is focused. 416 */ 417 public static class GetFocused implements ViewIntrospectionCommand { 418 //queryview [id type] [id] getfocused 419 //queryview viewid button1 getfocused query(AccessibilityNodeInfo node, List<String> args)420 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 421 List<String> args) { 422 if (args.size() == 0) { 423 return new MonkeyCommandReturn(true, Boolean.toString(node.isFocused())); 424 } 425 return EARG; 426 } 427 } 428 429 /** 430 * Command to set the focus status of the given node. Takes a boolean value 431 * as its only argument. 432 */ 433 public static class SetFocused implements ViewIntrospectionCommand { 434 //queryview [id type] [id] setfocused [boolean] 435 //queryview viewid button1 setfocused false query(AccessibilityNodeInfo node, List<String> args)436 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 437 List<String> args) { 438 if (args.size() == 1) { 439 boolean actionPerformed; 440 if (Boolean.valueOf(args.get(0))) { 441 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS); 442 } else if (!Boolean.valueOf(args.get(0))) { 443 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); 444 } else { 445 return EARG; 446 } 447 return new MonkeyCommandReturn(actionPerformed); 448 } 449 return EARG; 450 } 451 } 452 453 /** 454 * Command to get the accessibility ids of the given node. Returns the accessibility ids as a 455 * space separated pair of integers with window id coming first, followed by the accessibility 456 * view id. 457 */ 458 public static class GetAccessibilityIds implements ViewIntrospectionCommand { 459 //queryview [id type] [id] getaccessibilityids 460 //queryview viewid button1 getaccessibilityids query(AccessibilityNodeInfo node, List<String> args)461 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 462 List<String> args) { 463 if (args.size() == 0) { 464 int viewId; 465 try { 466 Class<?> klass = node.getClass(); 467 Field field = klass.getDeclaredField("mAccessibilityViewId"); 468 field.setAccessible(true); 469 viewId = ((Integer) field.get(node)).intValue(); 470 } catch (NoSuchFieldException e) { 471 return new MonkeyCommandReturn(false, NO_NODE); 472 } catch (IllegalAccessException e) { 473 return new MonkeyCommandReturn(false, "Access exception"); 474 } 475 String ids = node.getWindowId() + " " + viewId; 476 return new MonkeyCommandReturn(true, ids); 477 } 478 return EARG; 479 } 480 } 481 482 /** 483 * Command to get the accessibility ids of the parent of the given node. Returns the 484 * accessibility ids as a space separated pair of integers with window id coming first followed 485 * by the accessibility view id. 486 */ 487 public static class GetParent implements ViewIntrospectionCommand { 488 //queryview [id type] [id] getparent 489 //queryview viewid button1 getparent query(AccessibilityNodeInfo node, List<String> args)490 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 491 List<String> args) { 492 if (args.size() == 0) { 493 AccessibilityNodeInfo parent = node.getParent(); 494 if (parent == null) { 495 return new MonkeyCommandReturn(false, "Given node has no parent"); 496 } 497 return (new GetAccessibilityIds()).query(parent, new ArrayList<String>()); 498 } 499 return EARG; 500 } 501 } 502 503 /** 504 * Command to get the accessibility ids of the children of the given node. Returns the 505 * children's ids as a space separated list of integer pairs. Each of the pairs consists of the 506 * window id, followed by the accessibility id. 507 */ 508 public static class GetChildren implements ViewIntrospectionCommand { 509 //queryview [id type] [id] getchildren 510 //queryview viewid button1 getchildren query(AccessibilityNodeInfo node, List<String> args)511 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 512 List<String> args) { 513 if (args.size() == 0) { 514 ViewIntrospectionCommand idGetter = new GetAccessibilityIds(); 515 List<String> emptyArgs = new ArrayList<String>(); 516 StringBuilder ids = new StringBuilder(); 517 int totalChildren = node.getChildCount(); 518 for (int i = 0; i < totalChildren; i++) { 519 MonkeyCommandReturn result = idGetter.query(node.getChild(i), emptyArgs); 520 if (!result.wasSuccessful()) { 521 return result; 522 } else { 523 ids.append(result.getMessage()).append(" "); 524 } 525 } 526 return new MonkeyCommandReturn(true, ids.toString()); 527 } 528 return EARG; 529 } 530 } 531 } 532