2 * Copyright 2009, The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.android.commands.monkey;
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;
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;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Queue;
43 import java.util.StringTokenizer;
46 * An Event source for getting Monkey Network Script commands from
49 public class MonkeySourceNetwork implements MonkeyEventSource {
50 private static final String TAG = "MonkeyStub";
52 private interface MonkeyCommand {
53 MonkeyEvent translateCommand(List<String> command, CommandQueue queue);
57 * Command to simulate closing and opening the keyboard.
59 private static class FlipCommand implements MonkeyCommand {
62 public MonkeyEvent translateCommand(List<String> command,
64 if (command.size() > 1) {
65 String direction = command.get(1);
66 if ("open".equals(direction)) {
67 return new MonkeyFlipEvent(true);
68 } else if ("close".equals(direction)) {
69 return new MonkeyFlipEvent(false);
77 * Command to send touch events to the input system.
79 private static class TouchCommand implements MonkeyCommand {
80 // touch [down|up|move] [x] [y]
84 public MonkeyEvent translateCommand(List<String> command,
86 if (command.size() == 4) {
87 String actionName = command.get(1);
91 x = Integer.parseInt(command.get(2));
92 y = Integer.parseInt(command.get(3));
93 } catch (NumberFormatException e) {
94 // Ok, it wasn't a number
95 Log.e(TAG, "Got something that wasn't a number", e);
99 // figure out the action
101 if ("down".equals(actionName)) {
102 action = MotionEvent.ACTION_DOWN;
103 } else if ("up".equals(actionName)) {
104 action = MotionEvent.ACTION_UP;
105 } else if ("move".equals(actionName)) {
106 action = MotionEvent.ACTION_MOVE;
109 Log.e(TAG, "Got a bad action: " + actionName);
113 return new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
114 -1, action, x, y, 0);
122 * Command to send Trackball events to the input system.
124 private static class TrackballCommand implements MonkeyCommand {
125 // trackball [dx] [dy]
126 // trackball 1 0 -- move right
127 // trackball -1 0 -- move left
128 public MonkeyEvent translateCommand(List<String> command,
129 CommandQueue queue) {
130 if (command.size() == 3) {
134 dx = Integer.parseInt(command.get(1));
135 dy = Integer.parseInt(command.get(2));
136 } catch (NumberFormatException e) {
137 // Ok, it wasn't a number
138 Log.e(TAG, "Got something that wasn't a number", e);
141 return new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, -1,
142 MotionEvent.ACTION_MOVE, dx, dy, 0);
150 * Command to send Key events to the input system.
152 private static class KeyCommand implements MonkeyCommand {
153 // key [down|up] [keycode]
156 public MonkeyEvent translateCommand(List<String> command,
157 CommandQueue queue) {
158 if (command.size() == 3) {
159 int keyCode = getKeyCode(command.get(2));
161 // Ok, you gave us something bad.
162 Log.e(TAG, "Can't find keyname: " + command.get(2));
165 Log.d(TAG, "keycode: " + keyCode);
167 if ("down".equals(command.get(1))) {
168 action = KeyEvent.ACTION_DOWN;
169 } else if ("up".equals(command.get(1))) {
170 action = KeyEvent.ACTION_UP;
173 Log.e(TAG, "got unknown action.");
176 return new MonkeyKeyEvent(action, keyCode);
183 * Get an integer keycode value from a given keyname.
185 * @param keyName the key name to get the code for
186 * @returns the integer keycode value, or -1 on error.
188 private static int getKeyCode(String keyName) {
191 keyCode = Integer.parseInt(keyName);
192 } catch (NumberFormatException e) {
193 // Ok, it wasn't a number, see if we have a
194 // keycode name for it
195 keyCode = MonkeySourceRandom.getKeyCode(keyName);
197 // OK, one last ditch effort to find a match.
198 // Build the KEYCODE_STRING from the string
199 // we've been given and see if that key
200 // exists. This would allow you to do "key
201 // down menu", for example.
202 keyCode = MonkeySourceRandom.getKeyCode("KEYCODE_" + keyName.toUpperCase());
209 * Command to put the Monkey to sleep.
211 private static class SleepCommand implements MonkeyCommand {
213 public MonkeyEvent translateCommand(List<String> command,
214 CommandQueue queue) {
215 if (command.size() == 2) {
217 String sleepStr = command.get(1);
219 sleep = Integer.parseInt(sleepStr);
220 } catch (NumberFormatException e) {
221 Log.e(TAG, "Not a number: " + sleepStr, e);
223 return new MonkeyThrottleEvent(sleep);
230 * Command to type a string
232 private static class TypeCommand implements MonkeyCommand {
234 public MonkeyEvent translateCommand(List<String> command,
235 CommandQueue queue) {
236 if (command.size() == 2) {
237 String str = command.get(1);
239 char[] chars = str.toString().toCharArray();
241 // Convert the string to an array of KeyEvent's for
242 // the built in keymap.
243 KeyCharacterMap keyCharacterMap = KeyCharacterMap.
244 load(KeyCharacterMap.BUILT_IN_KEYBOARD);
245 KeyEvent[] events = keyCharacterMap.getEvents(chars);
247 // enqueue all the events we just got.
248 for (KeyEvent event : events) {
249 queue.enqueueEvent(new MonkeyKeyEvent(event));
251 return new MonkeyNoopEvent();
258 * Command to wake the device up
260 private static class WakeCommand implements MonkeyCommand {
262 public MonkeyEvent translateCommand(List<String> command,
263 CommandQueue queue) {
267 return new MonkeyNoopEvent();
272 * Command to "tap" at a location (Sends a down and up touch
275 private static class TapCommand implements MonkeyCommand {
277 public MonkeyEvent translateCommand(List<String> command,
278 CommandQueue queue) {
279 if (command.size() == 3) {
283 x = Integer.parseInt(command.get(1));
284 y = Integer.parseInt(command.get(2));
285 } catch (NumberFormatException e) {
286 // Ok, it wasn't a number
287 Log.e(TAG, "Got something that wasn't a number", e);
291 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
292 -1, MotionEvent.ACTION_DOWN,
294 queue.enqueueEvent(new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
295 -1, MotionEvent.ACTION_UP,
297 return new MonkeyNoopEvent();
304 * Command to "press" a buttons (Sends an up and down key event.)
306 private static class PressCommand implements MonkeyCommand {
308 public MonkeyEvent translateCommand(List<String> command,
309 CommandQueue queue) {
310 if (command.size() == 2) {
311 int keyCode = getKeyCode(command.get(1));
313 // Ok, you gave us something bad.
314 Log.e(TAG, "Can't find keyname: " + command.get(1));
318 queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, keyCode));
319 queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_UP, keyCode));
320 return new MonkeyNoopEvent();
328 * Force the device to wake up.
330 * @return true if woken up OK.
332 private static final boolean wake() {
334 IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
336 pm.userActivityWithForce(SystemClock.uptimeMillis(), true, true);
337 } catch (RemoteException e) {
338 Log.e(TAG, "Got remote exception", e);
344 // This maps from command names to command implementations.
345 private static final Map<String, MonkeyCommand> COMMAND_MAP = new HashMap<String, MonkeyCommand>();
348 // Add in all the commands we support
349 COMMAND_MAP.put("flip", new FlipCommand());
350 COMMAND_MAP.put("touch", new TouchCommand());
351 COMMAND_MAP.put("trackball", new TrackballCommand());
352 COMMAND_MAP.put("key", new KeyCommand());
353 COMMAND_MAP.put("sleep", new SleepCommand());
354 COMMAND_MAP.put("wake", new WakeCommand());
355 COMMAND_MAP.put("tap", new TapCommand());
356 COMMAND_MAP.put("press", new PressCommand());
357 COMMAND_MAP.put("type", new TypeCommand());
361 private static final String QUIT = "quit";
363 // command response strings
364 private static final String OK = "OK";
365 private static final String ERROR = "ERROR";
367 private static interface CommandQueue {
369 * Enqueue an event to be returned later. This allows a
370 * command to return multiple events. Commands using the
371 * command queue still have to return a valid event from their
372 * translateCommand method. The returned command will be
373 * executed before anything put into the queue.
375 * @param e the event to be enqueued.
377 public void enqueueEvent(MonkeyEvent e);
380 // Queue of Events to be processed. This allows commands to push
381 // multiple events into the queue to be processed.
382 private static class CommandQueueImpl implements CommandQueue{
383 private final Queue<MonkeyEvent> queuedEvents = new LinkedList<MonkeyEvent>();
385 public void enqueueEvent(MonkeyEvent e) {
386 queuedEvents.offer(e);
390 * Get the next queued event to excecute.
392 * @returns the next event, or null if there aren't any more.
394 public MonkeyEvent getNextQueuedEvent() {
395 return queuedEvents.poll();
399 private final CommandQueueImpl commandQueue = new CommandQueueImpl();
401 private final int port;
402 private BufferedReader input;
403 private PrintWriter output;
404 private boolean started = false;
406 public MonkeySourceNetwork(int port) {
411 * Start a network server listening on the specified port. The
412 * network protocol is a line oriented protocol, where each line
413 * is a different command that can be run.
415 * @param port the port to listen on
417 private void startServer() throws IOException {
418 // Only bind this to local host. This means that you can only
419 // talk to the monkey locally, or though adb port forwarding.
420 ServerSocket server = new ServerSocket(port,
421 0, // default backlog
422 InetAddress.getLocalHost());
423 Socket s = server.accept();
424 // At this point, we have a client connected. Wake the device
425 // up in preparation for doing some commands.
428 input = new BufferedReader(new InputStreamReader(s.getInputStream()));
430 output = new PrintWriter(s.getOutputStream(), true);
434 * Helper function for commandLineSplit that replaces quoted
435 * charaters with their real values.
437 * @param input the string to do replacement on.
438 * @returns the results with the characters replaced.
440 private static String replaceQuotedChars(String input) {
441 return input.replace("\\\"", "\"");
445 * This function splits the given line into String parts. It obey's quoted
446 * strings and returns them as a single part.
448 * "This is a test" -> returns only one element
449 * This is a test -> returns four elements
451 * @param line the line to parse
452 * @return the List of elements
454 private static List<String> commandLineSplit(String line) {
455 ArrayList<String> result = new ArrayList<String>();
456 StringTokenizer tok = new StringTokenizer(line);
458 boolean insideQuote = false;
459 StringBuffer quotedWord = new StringBuffer();
460 while (tok.hasMoreTokens()) {
461 String cur = tok.nextToken();
462 if (!insideQuote && cur.startsWith("\"")) {
464 quotedWord.append(replaceQuotedChars(cur));
466 } else if (insideQuote) {
468 if (cur.endsWith("\"")) {
470 quotedWord.append(" ").append(replaceQuotedChars(cur));
471 String word = quotedWord.toString();
473 // trim off the quotes
474 result.add(word.substring(1, word.length() - 1));
476 quotedWord.append(" ").append(replaceQuotedChars(cur));
479 result.add(replaceQuotedChars(cur));
486 * Translate the given command line into a MonkeyEvent.
488 * @param commandLine the full command line given.
489 * @returns the MonkeyEvent corresponding to the command, or null
490 * if there was an issue.
492 private MonkeyEvent translateCommand(String commandLine) {
493 Log.d(TAG, "translateCommand: " + commandLine);
494 List<String> parts = commandLineSplit(commandLine);
495 if (parts.size() > 0) {
496 MonkeyCommand command = COMMAND_MAP.get(parts.get(0));
497 if (command != null) {
498 return command.translateCommand(parts,
506 public MonkeyEvent getNextEvent() {
510 } catch (IOException e) {
511 Log.e(TAG, "Got IOException from server", e);
517 // Now, get the next command. This call may block, but that's OK
520 // Check to see if we have any events queued up. If
521 // we do, use those until we have no more. Then get
522 // more input from the user.
523 MonkeyEvent queuedEvent = commandQueue.getNextQueuedEvent();
524 if (queuedEvent != null) {
525 // dispatch the event
529 String command = input.readLine();
530 if (command == null) {
531 Log.d(TAG, "Connection dropped.");
534 // Do quit checking here
535 if (QUIT.equals(command)) {
537 Log.d(TAG, "Quit requested");
538 // let the host know the command ran OK
543 // Do comment checking here. Comments aren't a
544 // command, so we don't echo anything back to the
546 if (command.startsWith("#")) {
551 // Translate the command line
552 MonkeyEvent event = translateCommand(command);
554 // let the host know the command ran OK
558 // keep going. maybe the next command will make more sense
559 Log.e(TAG, "Got unknown command! \"" + command + "\"");
560 output.println(ERROR);
562 } catch (IOException e) {
563 Log.e(TAG, "Exception: ", e);
568 public void setVerbose(int verbose) {
569 // We're not particualy verbose
572 public boolean validate() {
573 // we have no pre-conditions to validate