1 /* 2 * Copyright 2009, 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 package com.android.commands.monkey; 17 18 import android.content.Context; 19 import android.os.IPowerManager; 20 import android.os.RemoteException; 21 import android.os.ServiceManager; 22 import android.os.SystemClock; 23 import android.util.Log; 24 import android.view.KeyCharacterMap; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 28 import java.io.BufferedReader; 29 import java.io.IOException; 30 import java.io.InputStreamReader; 31 import java.io.PrintWriter; 32 import java.lang.Integer; 33 import java.lang.NumberFormatException; 34 import java.net.InetAddress; 35 import java.net.ServerSocket; 36 import java.net.Socket; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.Map; 40 import java.util.LinkedList; 41 import java.util.List; 42 import java.util.Queue; 43 import java.util.StringTokenizer; 44 45 /** 46 * An Event source for getting Monkey Network Script commands from 47 * over the network. 48 */ 49 public class MonkeySourceNetwork implements MonkeyEventSource { 50 private static final String TAG = "MonkeyStub"; 51 52 /** 53 * ReturnValue from the MonkeyCommand that indicates whether the 54 * command was sucessful or not. 55 */ 56 public static class MonkeyCommandReturn { 57 private final boolean success; 58 private final String message; 59 60 public MonkeyCommandReturn(boolean success) { 61 this.success = success; 62 this.message = null; 63 } 64 65 public MonkeyCommandReturn(boolean success, 66 String message) { 67 this.success = success; 68 this.message = message; 69 } 70 71 boolean hasMessage() { 72 return message != null; 73 } 74 75 String getMessage() { 76 return message; 77 } 78 79 boolean wasSuccessful() { 80 return success; 81 } 82 } 83 84 public final static MonkeyCommandReturn OK = new MonkeyCommandReturn(true); 85 public final static MonkeyCommandReturn ERROR = new MonkeyCommandReturn(false); 86 public final static MonkeyCommandReturn EARG = new MonkeyCommandReturn(false, 87 "Invalid Argument"); 88 89 /** 90 * Interface that MonkeyCommands must implement. 91 */ 92 public interface MonkeyCommand { 93 /** 94 * Translate the command line into a sequence of MonkeyEvents. 95 * 96 * @param command the command line. 97 * @param queue the command queue. 98 * @returs MonkeyCommandReturn indicating what happened. 99 */ 100 MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue); 101 } 102 103 /** 104 * Command to simulate closing and opening the keyboard. 105 */ 106 private static class FlipCommand implements MonkeyCommand { 107 // flip open 108 // flip closed 109 public MonkeyCommandReturn translateCommand(List<String> command, 110 CommandQueue queue) { 111 if (command.size() > 1) { 112 String direction = command.get(1); 113 if ("open".equals(direction)) { 114 queue.enqueueEvent(new MonkeyFlipEvent(true)); 115 return OK; 116 } else if ("close".equals(direction)) { 117 queue.enqueueEvent(new MonkeyFlipEvent(false)); 118 return OK; 119 } 120 } 121 return EARG; 122 } 123 } 124 125 /** 126 * Command to send touch events to the input system. 127 */ 128 private static class TouchCommand implements MonkeyCommand { 129 // touch [down|up|move] [x] [y] 130 // touch down 120 120 131 // touch move 140 140 132 // touch up 140 140 133 public MonkeyCommandReturn translateCommand(List<String> command, 134 CommandQueue queue) { 135 if (command.size() == 4) { 136 String actionName = command.get(1); 137 int x = 0; 138 int y = 0; 139 try { 140 x = Integer.parseInt(command.get(2)); 141 y = Integer.parseInt(command.get(3)); 142 } catch (NumberFormatException e) { 143 // Ok, it wasn't a number 144 Log.e(TAG, "Got something that wasn't a number", e); 145 return EARG; 146 } 147 148 // figure out the action 149 int action = -1; 150 if ("down".equals(actionName)) { 151 action = MotionEvent.ACTION_DOWN; 152 } else if ("up".equals(actionName)) { 153 action = MotionEvent.ACTION_UP; 154 } else if ("move".equals(actionName)) { 155 action = MotionEvent.ACTION_MOVE; 156 } 157 if (action == -1) { 158 Log.e(TAG, "Got a bad action: " + actionName); 159 return EARG; 160 } 161 162 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER, 163 -1, action, x, y, 0)); 164 return OK; 165 } 166 return EARG; 167 } 168 } 169 170 /** 171 * Command to send Trackball events to the input system. 172 */ 173 private static class TrackballCommand implements MonkeyCommand { 174 // trackball [dx] [dy] 175 // trackball 1 0 -- move right 176 // trackball -1 0 -- move left 177 public MonkeyCommandReturn translateCommand(List<String> command, 178 CommandQueue queue) { 179 if (command.size() == 3) { 180 int dx = 0; 181 int dy = 0; 182 try { 183 dx = Integer.parseInt(command.get(1)); 184 dy = Integer.parseInt(command.get(2)); 185 } catch (NumberFormatException e) { 186 // Ok, it wasn't a number 187 Log.e(TAG, "Got something that wasn't a number", e); 188 return EARG; 189 } 190 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, -1, 191 MotionEvent.ACTION_MOVE, dx, dy, 0)); 192 return OK; 193 194 } 195 return EARG; 196 } 197 } 198 199 /** 200 * Command to send Key events to the input system. 201 */ 202 private static class KeyCommand implements MonkeyCommand { 203 // key [down|up] [keycode] 204 // key down 82 205 // key up 82 206 public MonkeyCommandReturn translateCommand(List<String> command, 207 CommandQueue queue) { 208 if (command.size() == 3) { 209 int keyCode = getKeyCode(command.get(2)); 210 if (keyCode < 0) { 211 // Ok, you gave us something bad. 212 Log.e(TAG, "Can't find keyname: " + command.get(2)); 213 return EARG; 214 } 215 Log.d(TAG, "keycode: " + keyCode); 216 int action = -1; 217 if ("down".equals(command.get(1))) { 218 action = KeyEvent.ACTION_DOWN; 219 } else if ("up".equals(command.get(1))) { 220 action = KeyEvent.ACTION_UP; 221 } 222 if (action == -1) { 223 Log.e(TAG, "got unknown action."); 224 return EARG; 225 } 226 queue.enqueueEvent(new MonkeyKeyEvent(action, keyCode)); 227 return OK; 228 } 229 return EARG; 230 } 231 } 232 233 /** 234 * Get an integer keycode value from a given keyname. 235 * 236 * @param keyName the key name to get the code for 237 * @returns the integer keycode value, or -1 on error. 238 */ 239 private static int getKeyCode(String keyName) { 240 int keyCode = -1; 241 try { 242 keyCode = Integer.parseInt(keyName); 243 } catch (NumberFormatException e) { 244 // Ok, it wasn't a number, see if we have a 245 // keycode name for it 246 keyCode = MonkeySourceRandom.getKeyCode(keyName); 247 if (keyCode == -1) { 248 // OK, one last ditch effort to find a match. 249 // Build the KEYCODE_STRING from the string 250 // we've been given and see if that key 251 // exists. This would allow you to do "key 252 // down menu", for example. 253 keyCode = MonkeySourceRandom.getKeyCode("KEYCODE_" + keyName.toUpperCase()); 254 } 255 } 256 return keyCode; 257 } 258 259 /** 260 * Command to put the Monkey to sleep. 261 */ 262 private static class SleepCommand implements MonkeyCommand { 263 // sleep 2000 264 public MonkeyCommandReturn translateCommand(List<String> command, 265 CommandQueue queue) { 266 if (command.size() == 2) { 267 int sleep = -1; 268 String sleepStr = command.get(1); 269 try { 270 sleep = Integer.parseInt(sleepStr); 271 } catch (NumberFormatException e) { 272 Log.e(TAG, "Not a number: " + sleepStr, e); 273 return EARG; 274 } 275 queue.enqueueEvent(new MonkeyThrottleEvent(sleep)); 276 return OK; 277 } 278 return EARG; 279 } 280 } 281 282 /** 283 * Command to type a string 284 */ 285 private static class TypeCommand implements MonkeyCommand { 286 // wake 287 public MonkeyCommandReturn translateCommand(List<String> command, 288 CommandQueue queue) { 289 if (command.size() == 2) { 290 String str = command.get(1); 291 292 char[] chars = str.toString().toCharArray(); 293 294 // Convert the string to an array of KeyEvent's for 295 // the built in keymap. 296 KeyCharacterMap keyCharacterMap = KeyCharacterMap. 297 load(KeyCharacterMap.BUILT_IN_KEYBOARD); 298 KeyEvent[] events = keyCharacterMap.getEvents(chars); 299 300 // enqueue all the events we just got. 301 for (KeyEvent event : events) { 302 queue.enqueueEvent(new MonkeyKeyEvent(event)); 303 } 304 return OK; 305 } 306 return EARG; 307 } 308 } 309 310 /** 311 * Command to wake the device up 312 */ 313 private static class WakeCommand implements MonkeyCommand { 314 // wake 315 public MonkeyCommandReturn translateCommand(List<String> command, 316 CommandQueue queue) { 317 if (!wake()) { 318 return ERROR; 319 } 320 return OK; 321 } 322 } 323 324 /** 325 * Command to "tap" at a location (Sends a down and up touch 326 * event). 327 */ 328 private static class TapCommand implements MonkeyCommand { 329 // tap x y 330 public MonkeyCommandReturn translateCommand(List<String> command, 331 CommandQueue queue) { 332 if (command.size() == 3) { 333 int x = 0; 334 int y = 0; 335 try { 336 x = Integer.parseInt(command.get(1)); 337 y = Integer.parseInt(command.get(2)); 338 } catch (NumberFormatException e) { 339 // Ok, it wasn't a number 340 Log.e(TAG, "Got something that wasn't a number", e); 341 return EARG; 342 } 343 344 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER, 345 -1, MotionEvent.ACTION_DOWN, 346 x, y, 0)); 347 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER, 348 -1, MotionEvent.ACTION_UP, 349 x, y, 0)); 350 return OK; 351 } 352 return EARG; 353 } 354 } 355 356 /** 357 * Command to "press" a buttons (Sends an up and down key event.) 358 */ 359 private static class PressCommand implements MonkeyCommand { 360 // press keycode 361 public MonkeyCommandReturn translateCommand(List<String> command, 362 CommandQueue queue) { 363 if (command.size() == 2) { 364 int keyCode = getKeyCode(command.get(1)); 365 if (keyCode < 0) { 366 // Ok, you gave us something bad. 367 Log.e(TAG, "Can't find keyname: " + command.get(1)); 368 return EARG; 369 } 370 371 queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, keyCode)); 372 queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_UP, keyCode)); 373 return OK; 374 375 } 376 return EARG; 377 } 378 } 379 380 /** 381 * Force the device to wake up. 382 * 383 * @return true if woken up OK. 384 */ 385 private static final boolean wake() { 386 IPowerManager pm = 387 IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE)); 388 try { 389 pm.userActivityWithForce(SystemClock.uptimeMillis(), true, true); 390 } catch (RemoteException e) { 391 Log.e(TAG, "Got remote exception", e); 392 return false; 393 } 394 return true; 395 } 396 397 // This maps from command names to command implementations. 398 private static final Map<String, MonkeyCommand> COMMAND_MAP = new HashMap<String, MonkeyCommand>(); 399 400 static { 401 // Add in all the commands we support 402 COMMAND_MAP.put("flip", new FlipCommand()); 403 COMMAND_MAP.put("touch", new TouchCommand()); 404 COMMAND_MAP.put("trackball", new TrackballCommand()); 405 COMMAND_MAP.put("key", new KeyCommand()); 406 COMMAND_MAP.put("sleep", new SleepCommand()); 407 COMMAND_MAP.put("wake", new WakeCommand()); 408 COMMAND_MAP.put("tap", new TapCommand()); 409 COMMAND_MAP.put("press", new PressCommand()); 410 COMMAND_MAP.put("type", new TypeCommand()); 411 COMMAND_MAP.put("listvar", new MonkeySourceNetworkVars.ListVarCommand()); 412 COMMAND_MAP.put("getvar", new MonkeySourceNetworkVars.GetVarCommand()); 413 } 414 415 // QUIT command 416 private static final String QUIT = "quit"; 417 // DONE command 418 private static final String DONE = "done"; 419 420 // command response strings 421 private static final String OK_STR = "OK"; 422 private static final String ERROR_STR = "ERROR"; 423 424 public static interface CommandQueue { 425 /** 426 * Enqueue an event to be returned later. This allows a 427 * command to return multiple events. Commands using the 428 * command queue still have to return a valid event from their 429 * translateCommand method. The returned command will be 430 * executed before anything put into the queue. 431 * 432 * @param e the event to be enqueued. 433 */ 434 public void enqueueEvent(MonkeyEvent e); 435 }; 436 437 // Queue of Events to be processed. This allows commands to push 438 // multiple events into the queue to be processed. 439 private static class CommandQueueImpl implements CommandQueue{ 440 private final Queue<MonkeyEvent> queuedEvents = new LinkedList<MonkeyEvent>(); 441 442 public void enqueueEvent(MonkeyEvent e) { 443 queuedEvents.offer(e); 444 } 445 446 /** 447 * Get the next queued event to excecute. 448 * 449 * @returns the next event, or null if there aren't any more. 450 */ 451 public MonkeyEvent getNextQueuedEvent() { 452 return queuedEvents.poll(); 453 } 454 }; 455 456 private final CommandQueueImpl commandQueue = new CommandQueueImpl(); 457 458 private BufferedReader input; 459 private PrintWriter output; 460 private boolean started = false; 461 462 private ServerSocket serverSocket; 463 private Socket clientSocket; 464 465 public MonkeySourceNetwork(int port) throws IOException { 466 // Only bind this to local host. This means that you can only 467 // talk to the monkey locally, or though adb port forwarding. 468 serverSocket = new ServerSocket(port, 469 0, // default backlog 470 InetAddress.getLocalHost()); 471 } 472 473 /** 474 * Start a network server listening on the specified port. The 475 * network protocol is a line oriented protocol, where each line 476 * is a different command that can be run. 477 * 478 * @param port the port to listen on 479 */ 480 private void startServer() throws IOException { 481 clientSocket = serverSocket.accept(); 482 // At this point, we have a client connected. Wake the device 483 // up in preparation for doing some commands. 484 wake(); 485 486 input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); 487 // auto-flush 488 output = new PrintWriter(clientSocket.getOutputStream(), true); 489 } 490 491 /** 492 * Stop the server from running so it can reconnect a new client. 493 */ 494 private void stopServer() throws IOException { 495 clientSocket.close(); 496 input.close(); 497 output.close(); 498 started = false; 499 } 500 501 /** 502 * Helper function for commandLineSplit that replaces quoted 503 * charaters with their real values. 504 * 505 * @param input the string to do replacement on. 506 * @returns the results with the characters replaced. 507 */ 508 private static String replaceQuotedChars(String input) { 509 return input.replace("\\\"", "\""); 510 } 511 512 /** 513 * This function splits the given line into String parts. It obey's quoted 514 * strings and returns them as a single part. 515 * 516 * "This is a test" -> returns only one element 517 * This is a test -> returns four elements 518 * 519 * @param line the line to parse 520 * @return the List of elements 521 */ 522 private static List<String> commandLineSplit(String line) { 523 ArrayList<String> result = new ArrayList<String>(); 524 StringTokenizer tok = new StringTokenizer(line); 525 526 boolean insideQuote = false; 527 StringBuffer quotedWord = new StringBuffer(); 528 while (tok.hasMoreTokens()) { 529 String cur = tok.nextToken(); 530 if (!insideQuote && cur.startsWith("\"")) { 531 // begin quote 532 quotedWord.append(replaceQuotedChars(cur)); 533 insideQuote = true; 534 } else if (insideQuote) { 535 // end quote 536 if (cur.endsWith("\"")) { 537 insideQuote = false; 538 quotedWord.append(" ").append(replaceQuotedChars(cur)); 539 String word = quotedWord.toString(); 540 541 // trim off the quotes 542 result.add(word.substring(1, word.length() - 1)); 543 } else { 544 quotedWord.append(" ").append(replaceQuotedChars(cur)); 545 } 546 } else { 547 result.add(replaceQuotedChars(cur)); 548 } 549 } 550 return result; 551 } 552 553 /** 554 * Translate the given command line into a MonkeyEvent. 555 * 556 * @param commandLine the full command line given. 557 */ 558 private void translateCommand(String commandLine) { 559 Log.d(TAG, "translateCommand: " + commandLine); 560 List<String> parts = commandLineSplit(commandLine); 561 if (parts.size() > 0) { 562 MonkeyCommand command = COMMAND_MAP.get(parts.get(0)); 563 if (command != null) { 564 MonkeyCommandReturn ret = command.translateCommand(parts, 565 commandQueue); 566 if (ret.wasSuccessful()) { 567 if (ret.hasMessage()) { 568 returnOk(ret.getMessage()); 569 } else { 570 returnOk(); 571 } 572 } else { 573 if (ret.hasMessage()) { 574 returnError(ret.getMessage()); 575 } else { 576 returnError(); 577 } 578 } 579 } 580 } 581 } 582 583 public MonkeyEvent getNextEvent() { 584 if (!started) { 585 try { 586 startServer(); 587 } catch (IOException e) { 588 Log.e(TAG, "Got IOException from server", e); 589 return null; 590 } 591 started = true; 592 } 593 594 // Now, get the next command. This call may block, but that's OK 595 try { 596 while (true) { 597 // Check to see if we have any events queued up. If 598 // we do, use those until we have no more. Then get 599 // more input from the user. 600 MonkeyEvent queuedEvent = commandQueue.getNextQueuedEvent(); 601 if (queuedEvent != null) { 602 // dispatch the event 603 return queuedEvent; 604 } 605 606 String command = input.readLine(); 607 if (command == null) { 608 Log.d(TAG, "Connection dropped."); 609 // Treat this exactly the same as if the user had 610 // ended the session cleanly with a done commant. 611 command = DONE; 612 } 613 614 if (DONE.equals(command)) { 615 // stop the server so it can accept new connections 616 try { 617 stopServer(); 618 } catch (IOException e) { 619 Log.e(TAG, "Got IOException shutting down!", e); 620 return null; 621 } 622 // return a noop event so we keep executing the main 623 // loop 624 return new MonkeyNoopEvent(); 625 } 626 627 // Do quit checking here 628 if (QUIT.equals(command)) { 629 // then we're done 630 Log.d(TAG, "Quit requested"); 631 // let the host know the command ran OK 632 returnOk(); 633 return null; 634 } 635 636 // Do comment checking here. Comments aren't a 637 // command, so we don't echo anything back to the 638 // user. 639 if (command.startsWith("#")) { 640 // keep going 641 continue; 642 } 643 644 // Translate the command line. This will handle returning error/ok to the user 645 translateCommand(command); 646 } 647 } catch (IOException e) { 648 Log.e(TAG, "Exception: ", e); 649 return null; 650 } 651 } 652 653 /** 654 * Returns ERROR to the user. 655 */ 656 private void returnError() { 657 output.println(ERROR_STR); 658 } 659 660 /** 661 * Returns ERROR to the user. 662 * 663 * @param msg the error message to include 664 */ 665 private void returnError(String msg) { 666 output.print(ERROR_STR); 667 output.print(":"); 668 output.println(msg); 669 } 670 671 /** 672 * Returns OK to the user. 673 */ 674 private void returnOk() { 675 output.println(OK_STR); 676 } 677 678 /** 679 * Returns OK to the user. 680 * 681 * @param returnValue the value to return from this command. 682 */ 683 private void returnOk(String returnValue) { 684 output.print(OK_STR); 685 output.print(":"); 686 output.println(returnValue); 687 } 688 689 public void setVerbose(int verbose) { 690 // We're not particualy verbose 691 } 692 693 public boolean validate() { 694 // we have no pre-conditions to validate 695 return true; 696 } 697 } 698