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 */ 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 */ 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 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 */ 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 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, 0); 143 } 144 145 private static AccessibilityNodeInfo getNodeByViewId(String viewId) throws MonkeyViewException { 146 int connectionId = sUiTestAutomationBridge.getConnectionId(); 147 AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); 148 List<AccessibilityNodeInfo> infos = client.findAccessibilityNodeInfosByViewId( 149 connectionId, AccessibilityNodeInfo.ACTIVE_WINDOW_ID, 150 AccessibilityNodeInfo.ROOT_NODE_ID, viewId); 151 return (!infos.isEmpty()) ? infos.get(0) : null; 152 } 153 154 /** 155 * Command to list all possible view ids for the given application. 156 * This lists all view ids regardless if they are on screen or not. 157 */ 158 public static class ListViewsCommand implements MonkeyCommand { 159 //listviews 160 public MonkeyCommandReturn translateCommand(List<String> command, 161 CommandQueue queue) { 162 AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow(); 163 /* Occasionally the API will generate an event with no source, which is essentially the 164 * same as it generating no event at all */ 165 if (node == null) { 166 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT); 167 } 168 String packageName = node.getPackageName().toString(); 169 try{ 170 Class<?> klass; 171 ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, 172 UserHandle.myUserId()); 173 klass = getIdClass(packageName, appInfo.sourceDir); 174 StringBuilder fieldBuilder = new StringBuilder(); 175 Field[] fields = klass.getFields(); 176 for (Field field : fields) { 177 fieldBuilder.append(field.getName() + " "); 178 } 179 return new MonkeyCommandReturn(true, fieldBuilder.toString()); 180 } catch (RemoteException e){ 181 return new MonkeyCommandReturn(false, REMOTE_ERROR); 182 } catch (ClassNotFoundException e){ 183 return new MonkeyCommandReturn(false, CLASS_NOT_FOUND); 184 } 185 } 186 } 187 188 /** 189 * A command that allows for querying of views. It takes an id type, the requisite ids, 190 * and the command for querying the view. 191 */ 192 public static class QueryViewCommand implements MonkeyCommand { 193 //queryview [id type] [id(s)] [command] 194 //queryview viewid button1 gettext 195 //queryview accessibilityids 12 5 getparent 196 public MonkeyCommandReturn translateCommand(List<String> command, 197 CommandQueue queue) { 198 if (command.size() > 2) { 199 String idType = command.get(1); 200 AccessibilityNodeInfo node; 201 String viewQuery; 202 List<String> args; 203 if ("viewid".equals(idType)) { 204 try { 205 node = getNodeByViewId(command.get(2)); 206 viewQuery = command.get(3); 207 args = command.subList(4, command.size()); 208 } catch (MonkeyViewException e) { 209 return new MonkeyCommandReturn(false, e.getMessage()); 210 } 211 } else if (idType.equals("accessibilityids")) { 212 try { 213 node = getNodeByAccessibilityIds(command.get(2), command.get(3)); 214 viewQuery = command.get(4); 215 args = command.subList(5, command.size()); 216 } catch (NumberFormatException e) { 217 return EARG; 218 } 219 } else { 220 return EARG; 221 } 222 if (node == null) { 223 return new MonkeyCommandReturn(false, NO_NODE); 224 } 225 ViewIntrospectionCommand getter = COMMAND_MAP.get(viewQuery); 226 if (getter != null) { 227 return getter.query(node, args); 228 } else { 229 return EARG; 230 } 231 } 232 return EARG; 233 } 234 } 235 236 /** 237 * A command that returns the accessibility ids of the root view. 238 */ 239 public static class GetRootViewCommand implements MonkeyCommand { 240 // getrootview 241 public MonkeyCommandReturn translateCommand(List<String> command, 242 CommandQueue queue) { 243 AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow(); 244 return (new GetAccessibilityIds()).query(node, new ArrayList<String>()); 245 } 246 } 247 248 /** 249 * A command that returns the accessibility ids of the views that contain the given text. 250 * It takes a string of text and returns the accessibility ids of the nodes that contain the 251 * text as a list of integers separated by spaces. 252 */ 253 public static class GetViewsWithTextCommand implements MonkeyCommand { 254 // getviewswithtext [text] 255 // getviewswithtext "some text here" 256 public MonkeyCommandReturn translateCommand(List<String> command, 257 CommandQueue queue) { 258 if (command.size() == 2) { 259 String text = command.get(1); 260 int connectionId = sUiTestAutomationBridge.getConnectionId(); 261 List<AccessibilityNodeInfo> nodes = AccessibilityInteractionClient.getInstance() 262 .findAccessibilityNodeInfosByText(connectionId, 263 AccessibilityNodeInfo.ACTIVE_WINDOW_ID, 264 AccessibilityNodeInfo.ROOT_NODE_ID, text); 265 ViewIntrospectionCommand idGetter = new GetAccessibilityIds(); 266 List<String> emptyArgs = new ArrayList<String>(); 267 StringBuilder ids = new StringBuilder(); 268 for (AccessibilityNodeInfo node : nodes) { 269 MonkeyCommandReturn result = idGetter.query(node, emptyArgs); 270 if (!result.wasSuccessful()){ 271 return result; 272 } 273 ids.append(result.getMessage()).append(" "); 274 } 275 return new MonkeyCommandReturn(true, ids.toString()); 276 } 277 return EARG; 278 } 279 } 280 281 /** 282 * Command to retrieve the location of the given node. 283 * Returns the x, y, width and height of the view, separated by spaces. 284 */ 285 public static class GetLocation implements ViewIntrospectionCommand { 286 //queryview [id type] [id] getlocation 287 //queryview viewid button1 getlocation 288 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 289 List<String> args) { 290 if (args.size() == 0) { 291 Rect nodePosition = new Rect(); 292 node.getBoundsInScreen(nodePosition); 293 StringBuilder positions = new StringBuilder(); 294 positions.append(nodePosition.left).append(" ").append(nodePosition.top); 295 positions.append(" ").append(nodePosition.right-nodePosition.left).append(" "); 296 positions.append(nodePosition.bottom-nodePosition.top); 297 return new MonkeyCommandReturn(true, positions.toString()); 298 } 299 return EARG; 300 } 301 } 302 303 304 /** 305 * Command to retrieve the text of the given node 306 */ 307 public static class GetText implements ViewIntrospectionCommand { 308 //queryview [id type] [id] gettext 309 //queryview viewid button1 gettext 310 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 311 List<String> args) { 312 if (args.size() == 0) { 313 if (node.isPassword()){ 314 return new MonkeyCommandReturn(false, "Node contains a password"); 315 } 316 /* Occasionally we get a null from the accessibility API, rather than an empty 317 * string */ 318 if (node.getText() == null) { 319 return new MonkeyCommandReturn(true, ""); 320 } 321 return new MonkeyCommandReturn(true, node.getText().toString()); 322 } 323 return EARG; 324 } 325 } 326 327 328 /** 329 * Command to retrieve the class name of the given node 330 */ 331 public static class GetClass implements ViewIntrospectionCommand { 332 //queryview [id type] [id] getclass 333 //queryview viewid button1 getclass 334 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 335 List<String> args) { 336 if (args.size() == 0) { 337 return new MonkeyCommandReturn(true, node.getClassName().toString()); 338 } 339 return EARG; 340 } 341 } 342 /** 343 * Command to retrieve the checked status of the given node 344 */ 345 public static class GetChecked implements ViewIntrospectionCommand { 346 //queryview [id type] [id] getchecked 347 //queryview viewid button1 getchecked 348 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 349 List<String> args) { 350 if (args.size() == 0) { 351 return new MonkeyCommandReturn(true, Boolean.toString(node.isChecked())); 352 } 353 return EARG; 354 } 355 } 356 357 /** 358 * Command to retrieve whether the given node is enabled 359 */ 360 public static class GetEnabled implements ViewIntrospectionCommand { 361 //queryview [id type] [id] getenabled 362 //queryview viewid button1 getenabled 363 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 364 List<String> args) { 365 if (args.size() == 0) { 366 return new MonkeyCommandReturn(true, Boolean.toString(node.isEnabled())); 367 } 368 return EARG; 369 } 370 } 371 372 /** 373 * Command to retrieve whether the given node is selected 374 */ 375 public static class GetSelected implements ViewIntrospectionCommand { 376 //queryview [id type] [id] getselected 377 //queryview viewid button1 getselected 378 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 379 List<String> args) { 380 if (args.size() == 0) { 381 return new MonkeyCommandReturn(true, Boolean.toString(node.isSelected())); 382 } 383 return EARG; 384 } 385 } 386 387 /** 388 * Command to set the selected status of the given node. Takes a boolean value as its only 389 * argument. 390 */ 391 public static class SetSelected implements ViewIntrospectionCommand { 392 //queryview [id type] [id] setselected [boolean] 393 //queryview viewid button1 setselected true 394 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 395 List<String> args) { 396 if (args.size() == 1) { 397 boolean actionPerformed; 398 if (Boolean.valueOf(args.get(0))) { 399 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_SELECT); 400 } else if (!Boolean.valueOf(args.get(0))) { 401 actionPerformed = 402 node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); 403 } else { 404 return EARG; 405 } 406 return new MonkeyCommandReturn(actionPerformed); 407 } 408 return EARG; 409 } 410 } 411 412 /** 413 * Command to get whether the given node is focused. 414 */ 415 public static class GetFocused implements ViewIntrospectionCommand { 416 //queryview [id type] [id] getfocused 417 //queryview viewid button1 getfocused 418 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 419 List<String> args) { 420 if (args.size() == 0) { 421 return new MonkeyCommandReturn(true, Boolean.toString(node.isFocused())); 422 } 423 return EARG; 424 } 425 } 426 427 /** 428 * Command to set the focus status of the given node. Takes a boolean value 429 * as its only argument. 430 */ 431 public static class SetFocused implements ViewIntrospectionCommand { 432 //queryview [id type] [id] setfocused [boolean] 433 //queryview viewid button1 setfocused false 434 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 435 List<String> args) { 436 if (args.size() == 1) { 437 boolean actionPerformed; 438 if (Boolean.valueOf(args.get(0))) { 439 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS); 440 } else if (!Boolean.valueOf(args.get(0))) { 441 actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); 442 } else { 443 return EARG; 444 } 445 return new MonkeyCommandReturn(actionPerformed); 446 } 447 return EARG; 448 } 449 } 450 451 /** 452 * Command to get the accessibility ids of the given node. Returns the accessibility ids as a 453 * space separated pair of integers with window id coming first, followed by the accessibility 454 * view id. 455 */ 456 public static class GetAccessibilityIds implements ViewIntrospectionCommand { 457 //queryview [id type] [id] getaccessibilityids 458 //queryview viewid button1 getaccessibilityids 459 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 460 List<String> args) { 461 if (args.size() == 0) { 462 int viewId; 463 try { 464 Class<?> klass = node.getClass(); 465 Field field = klass.getDeclaredField("mAccessibilityViewId"); 466 field.setAccessible(true); 467 viewId = ((Integer) field.get(node)).intValue(); 468 } catch (NoSuchFieldException e) { 469 return new MonkeyCommandReturn(false, NO_NODE); 470 } catch (IllegalAccessException e) { 471 return new MonkeyCommandReturn(false, "Access exception"); 472 } 473 String ids = node.getWindowId() + " " + viewId; 474 return new MonkeyCommandReturn(true, ids); 475 } 476 return EARG; 477 } 478 } 479 480 /** 481 * Command to get the accessibility ids of the parent of the given node. Returns the 482 * accessibility ids as a space separated pair of integers with window id coming first followed 483 * by the accessibility view id. 484 */ 485 public static class GetParent implements ViewIntrospectionCommand { 486 //queryview [id type] [id] getparent 487 //queryview viewid button1 getparent 488 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 489 List<String> args) { 490 if (args.size() == 0) { 491 AccessibilityNodeInfo parent = node.getParent(); 492 if (parent == null) { 493 return new MonkeyCommandReturn(false, "Given node has no parent"); 494 } 495 return (new GetAccessibilityIds()).query(parent, new ArrayList<String>()); 496 } 497 return EARG; 498 } 499 } 500 501 /** 502 * Command to get the accessibility ids of the children of the given node. Returns the 503 * children's ids as a space separated list of integer pairs. Each of the pairs consists of the 504 * window id, followed by the accessibility id. 505 */ 506 public static class GetChildren implements ViewIntrospectionCommand { 507 //queryview [id type] [id] getchildren 508 //queryview viewid button1 getchildren 509 public MonkeyCommandReturn query(AccessibilityNodeInfo node, 510 List<String> args) { 511 if (args.size() == 0) { 512 ViewIntrospectionCommand idGetter = new GetAccessibilityIds(); 513 List<String> emptyArgs = new ArrayList<String>(); 514 StringBuilder ids = new StringBuilder(); 515 int totalChildren = node.getChildCount(); 516 for (int i = 0; i < totalChildren; i++) { 517 MonkeyCommandReturn result = idGetter.query(node.getChild(i), emptyArgs); 518 if (!result.wasSuccessful()) { 519 return result; 520 } else { 521 ids.append(result.getMessage()).append(" "); 522 } 523 } 524 return new MonkeyCommandReturn(true, ids.toString()); 525 } 526 return EARG; 527 } 528 } 529 } 530