blob: 36254ce850d20779821c978563e8dd81138ad003 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.chimpchat;
import com.android.chimpchat.core.IChimpView;
import com.android.chimpchat.core.PhysicalButton;
import com.android.chimpchat.core.ChimpException;
import com.android.chimpchat.core.ChimpRect;
import com.android.chimpchat.core.ChimpView;
import com.google.common.collect.Lists;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Provides a nicer interface to interacting with the low-level network access protocol for talking
* to the monkey.
*
* This class is thread-safe and can handle being called from multiple threads.
*/
public class ChimpManager {
private static Logger LOG = Logger.getLogger(ChimpManager.class.getName());
private Socket monkeySocket;
private BufferedWriter monkeyWriter;
private BufferedReader monkeyReader;
/**
* Create a new ChimpMananger to talk to the specified device.
*
* @param monkeySocket the already connected socket on which to send protocol messages.
* @throws IOException if there is an issue setting up the sockets
*/
public ChimpManager(Socket monkeySocket) throws IOException {
this.monkeySocket = monkeySocket;
monkeyWriter =
new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream()));
monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream()));
}
/* Ensure that everything gets shutdown properly */
@Override
protected void finalize() throws Throwable {
try {
quit();
} finally {
close();
super.finalize();
}
}
/**
* Send a touch down event at the specified location.
*
* @param x the x coordinate of where to click
* @param y the y coordinate of where to click
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean touchDown(int x, int y) throws IOException {
return sendMonkeyEvent("touch down " + x + " " + y);
}
/**
* Send a touch down event at the specified location.
*
* @param x the x coordinate of where to click
* @param y the y coordinate of where to click
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean touchUp(int x, int y) throws IOException {
return sendMonkeyEvent("touch up " + x + " " + y);
}
/**
* Send a touch move event at the specified location.
*
* @param x the x coordinate of where to click
* @param y the y coordinate of where to click
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean touchMove(int x, int y) throws IOException {
return sendMonkeyEvent("touch move " + x + " " + y);
}
/**
* Send a touch (down and then up) event at the specified location.
*
* @param x the x coordinate of where to click
* @param y the y coordinate of where to click
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean touch(int x, int y) throws IOException {
return sendMonkeyEvent("tap " + x + " " + y);
}
/**
* Press a physical button on the device.
*
* @param name the name of the button (As specified in the protocol)
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean press(String name) throws IOException {
return sendMonkeyEvent("press " + name);
}
/**
* Send a Key Down event for the specified button.
*
* @param name the name of the button (As specified in the protocol)
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean keyDown(String name) throws IOException {
return sendMonkeyEvent("key down " + name);
}
/**
* Send a Key Up event for the specified button.
*
* @param name the name of the button (As specified in the protocol)
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean keyUp(String name) throws IOException {
return sendMonkeyEvent("key up " + name);
}
/**
* Press a physical button on the device.
*
* @param button the button to press
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean press(PhysicalButton button) throws IOException {
return press(button.getKeyName());
}
/**
* This function allows the communication bridge between the host and the device
* to be invisible to the script for internal needs.
* It splits a command into monkey events and waits for responses for each over an adb tcp socket.
* Returns on an error, else continues and sets up last response.
*
* @param command the monkey command to send to the device
* @return the (unparsed) response returned from the monkey.
* @throws IOException on error communicating with the device
*/
private String sendMonkeyEventAndGetResponse(String command) throws IOException {
command = command.trim();
LOG.info("Monkey Command: " + command + ".");
// send a single command and get the response
monkeyWriter.write(command + "\n");
monkeyWriter.flush();
return monkeyReader.readLine();
}
/**
* Parse a monkey response string to see if the command succeeded or not.
*
* @param monkeyResponse the response
* @return true if response code indicated success.
*/
private boolean parseResponseForSuccess(String monkeyResponse) {
if (monkeyResponse == null) {
return false;
}
// return on ok
if(monkeyResponse.startsWith("OK")) {
return true;
}
return false;
}
/**
* Parse a monkey response string to get the extra data returned.
*
* @param monkeyResponse the response
* @return any extra data that was returned, or empty string if there was nothing.
*/
private String parseResponseForExtra(String monkeyResponse) {
int offset = monkeyResponse.indexOf(':');
if (offset < 0) {
return "";
}
return monkeyResponse.substring(offset + 1);
}
/**
* This function allows the communication bridge between the host and the device
* to be invisible to the script for internal needs.
* It splits a command into monkey events and waits for responses for each over an
* adb tcp socket.
*
* @param command the monkey command to send to the device
* @return true on success.
* @throws IOException on error communicating with the device
*/
private boolean sendMonkeyEvent(String command) throws IOException {
synchronized (this) {
String monkeyResponse = sendMonkeyEventAndGetResponse(command);
return parseResponseForSuccess(monkeyResponse);
}
}
/**
* Close all open resources related to this device.
*/
public void close() {
try {
monkeySocket.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to close monkeySocket", e);
}
try {
monkeyReader.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to close monkeyReader", e);
}
try {
monkeyWriter.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e);
}
}
/**
* Function to get a static variable from the device.
*
* @param name name of static variable to get
* @return the value of the variable, or null if there was an error
* @throws IOException on error communicating with the device
*/
public String getVariable(String name) throws IOException {
synchronized (this) {
String response = sendMonkeyEventAndGetResponse("getvar " + name);
if (!parseResponseForSuccess(response)) {
return null;
}
return parseResponseForExtra(response);
}
}
/**
* Function to get the list of variables from the device.
* @return the list of variables as a collection of strings
* @throws IOException on error communicating with the device
*/
public Collection<String> listVariable() throws IOException {
synchronized (this) {
String response = sendMonkeyEventAndGetResponse("listvar");
if (!parseResponseForSuccess(response)) {
Collections.emptyList();
}
String extras = parseResponseForExtra(response);
return Lists.newArrayList(extras.split(" "));
}
}
/**
* Tells the monkey that we are done for this session.
* @throws IOException on error communicating with the device
*/
public void done() throws IOException {
// this command just drops the connection, so handle it here
synchronized (this) {
sendMonkeyEventAndGetResponse("done");
}
}
/**
* Tells the monkey that we are done forever.
* @throws IOException on error communicating with the device
*/
public void quit() throws IOException {
// this command drops the connection, so handle it here
synchronized (this) {
try {
sendMonkeyEventAndGetResponse("quit");
} catch (SocketException e) {
// flush was called after the call had been written, so it tried flushing to a
// broken pipe.
}
}
}
/**
* Send a tap event at the specified location.
*
* @param x the x coordinate of where to click
* @param y the y coordinate of where to click
* @return success or not
* @throws IOException on error communicating with the device
*/
public boolean tap(int x, int y) throws IOException {
return sendMonkeyEvent("tap " + x + " " + y);
}
/**
* Type the following string to the monkey.
*
* @param text the string to type
* @return success
* @throws IOException on error communicating with the device
*/
public boolean type(String text) throws IOException {
// The network protocol can't handle embedded line breaks, so we have to handle it
// here instead
StringTokenizer tok = new StringTokenizer(text, "\n", true);
while (tok.hasMoreTokens()) {
String line = tok.nextToken();
if ("\n".equals(line)) {
boolean success = press(PhysicalButton.ENTER);
if (!success) {
return false;
}
} else {
boolean success = sendMonkeyEvent("type " + line);
if (!success) {
return false;
}
}
}
return true;
}
/**
* Type the character to the monkey.
*
* @param keyChar the character to type.
* @return success
* @throws IOException on error communicating with the device
*/
public boolean type(char keyChar) throws IOException {
return type(Character.toString(keyChar));
}
/**
* Wake the device up from sleep.
* @throws IOException on error communicating with the device
*/
public void wake() throws IOException {
sendMonkeyEvent("wake");
}
/**
* Retrieves the list of view ids from the current application.
* @return the list of view ids as a collection of strings
* @throws IOException on error communicating with the device
*/
public Collection<String> listViewIds() throws IOException {
synchronized (this) {
String response = sendMonkeyEventAndGetResponse("listviews");
if (!parseResponseForSuccess(response)) {
Collections.emptyList();
}
String extras = parseResponseForExtra(response);
return Lists.newArrayList(extras.split(" "));
}
}
/**
* Queries the on-screen view with the given id and returns the response.
* It's up to the calling method to parse the returned String.
* @param idType The type of ID to query the view by
* @param id The view id of the view
* @param query the query
* @return the response from the query
* @throws IOException on error communicating with the device
*/
public String queryView(String idType, List<String> ids, String query) throws IOException {
StringBuilder monkeyCommand = new StringBuilder("queryview " + idType + " ");
for(String id : ids) {
monkeyCommand.append(id).append(" ");
}
monkeyCommand.append(query);
synchronized (this) {
String response = sendMonkeyEventAndGetResponse(monkeyCommand.toString());
if (!parseResponseForSuccess(response)) {
throw new ChimpException(parseResponseForExtra(response));
}
return parseResponseForExtra(response);
}
}
/**
* Returns the current root view of the device.
* @return the root view of the device
*/
public IChimpView getRootView() throws IOException {
synchronized(this) {
String response = sendMonkeyEventAndGetResponse("getrootview");
String extra = parseResponseForExtra(response);
List<String> ids = Arrays.asList(extra.split(" "));
if (!parseResponseForSuccess(response) || ids.size() != 2) {
throw new ChimpException(extra);
}
ChimpView root = new ChimpView(ChimpView.ACCESSIBILITY_IDS, ids);
root.setManager(this);
return root;
}
}
/**
* Queries the device for a list of views with the given
* @return A string containing the accessibility ids of the views with the given text
*/
public String getViewsWithText(String text) throws IOException {
synchronized(this) {
// Monkey has trouble parsing a single word in quotes
if (text.split(" ").length > 1) {
text = "\"" + text + "\"";
}
String response = sendMonkeyEventAndGetResponse("getviewswithtext " + text);
if (!parseResponseForSuccess(response)) {
throw new ChimpException(parseResponseForExtra(response));
}
return parseResponseForExtra(response);
}
}
}