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