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