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