| |
| /* |
| * 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); |
| } |
| } |
| } |