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