1 2 /* 3 * Copyright (C) 2011 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package com.android.chimpchat; 18 19 import com.android.chimpchat.core.IChimpView; 20 import com.android.chimpchat.core.PhysicalButton; 21 import com.android.chimpchat.core.ChimpException; 22 import com.android.chimpchat.core.ChimpRect; 23 import com.android.chimpchat.core.ChimpView; 24 25 import com.google.common.collect.Lists; 26 27 import java.io.BufferedReader; 28 import java.io.BufferedWriter; 29 import java.io.IOException; 30 import java.io.InputStreamReader; 31 import java.io.OutputStreamWriter; 32 import java.net.Socket; 33 import java.net.SocketException; 34 import java.util.Arrays; 35 import java.util.Collection; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.StringTokenizer; 39 import java.util.logging.Level; 40 import java.util.logging.Logger; 41 42 /** 43 * Provides a nicer interface to interacting with the low-level network access protocol for talking 44 * to the monkey. 45 * 46 * This class is thread-safe and can handle being called from multiple threads. 47 */ 48 public class ChimpManager { 49 private static Logger LOG = Logger.getLogger(ChimpManager.class.getName()); 50 51 private Socket monkeySocket; 52 private BufferedWriter monkeyWriter; 53 private BufferedReader monkeyReader; 54 55 /** 56 * Create a new ChimpMananger to talk to the specified device. 57 * 58 * @param monkeySocket the already connected socket on which to send protocol messages. 59 * @throws IOException if there is an issue setting up the sockets 60 */ ChimpManager(Socket monkeySocket)61 public ChimpManager(Socket monkeySocket) throws IOException { 62 this.monkeySocket = monkeySocket; 63 monkeyWriter = 64 new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); 65 monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); 66 } 67 68 /* Ensure that everything gets shutdown properly */ finalize()69 protected void finalize() throws Throwable { 70 try { 71 quit(); 72 } finally { 73 close(); 74 super.finalize(); 75 } 76 } 77 78 /** 79 * Send a touch down event at the specified location. 80 * 81 * @param x the x coordinate of where to click 82 * @param y the y coordinate of where to click 83 * @return success or not 84 * @throws IOException on error communicating with the device 85 */ touchDown(int x, int y)86 public boolean touchDown(int x, int y) throws IOException { 87 return sendMonkeyEvent("touch down " + x + " " + y); 88 } 89 90 /** 91 * Send a touch down event at the specified location. 92 * 93 * @param x the x coordinate of where to click 94 * @param y the y coordinate of where to click 95 * @return success or not 96 * @throws IOException on error communicating with the device 97 */ touchUp(int x, int y)98 public boolean touchUp(int x, int y) throws IOException { 99 return sendMonkeyEvent("touch up " + x + " " + y); 100 } 101 102 /** 103 * Send a touch move event at the specified location. 104 * 105 * @param x the x coordinate of where to click 106 * @param y the y coordinate of where to click 107 * @return success or not 108 * @throws IOException on error communicating with the device 109 */ touchMove(int x, int y)110 public boolean touchMove(int x, int y) throws IOException { 111 return sendMonkeyEvent("touch move " + x + " " + y); 112 } 113 114 /** 115 * Send a touch (down and then up) event at the specified location. 116 * 117 * @param x the x coordinate of where to click 118 * @param y the y coordinate of where to click 119 * @return success or not 120 * @throws IOException on error communicating with the device 121 */ touch(int x, int y)122 public boolean touch(int x, int y) throws IOException { 123 return sendMonkeyEvent("tap " + x + " " + y); 124 } 125 126 /** 127 * Press a physical button on the device. 128 * 129 * @param name the name of the button (As specified in the protocol) 130 * @return success or not 131 * @throws IOException on error communicating with the device 132 */ press(String name)133 public boolean press(String name) throws IOException { 134 return sendMonkeyEvent("press " + name); 135 } 136 137 /** 138 * Send a Key Down event for the specified button. 139 * 140 * @param name the name of the button (As specified in the protocol) 141 * @return success or not 142 * @throws IOException on error communicating with the device 143 */ keyDown(String name)144 public boolean keyDown(String name) throws IOException { 145 return sendMonkeyEvent("key down " + name); 146 } 147 148 /** 149 * Send a Key Up event for the specified button. 150 * 151 * @param name the name of the button (As specified in the protocol) 152 * @return success or not 153 * @throws IOException on error communicating with the device 154 */ keyUp(String name)155 public boolean keyUp(String name) throws IOException { 156 return sendMonkeyEvent("key up " + name); 157 } 158 159 /** 160 * Press a physical button on the device. 161 * 162 * @param button the button to press 163 * @return success or not 164 * @throws IOException on error communicating with the device 165 */ press(PhysicalButton button)166 public boolean press(PhysicalButton button) throws IOException { 167 return press(button.getKeyName()); 168 } 169 170 /** 171 * This function allows the communication bridge between the host and the device 172 * to be invisible to the script for internal needs. 173 * It splits a command into monkey events and waits for responses for each over an adb tcp socket. 174 * Returns on an error, else continues and sets up last response. 175 * 176 * @param command the monkey command to send to the device 177 * @return the (unparsed) response returned from the monkey. 178 * @throws IOException on error communicating with the device 179 */ sendMonkeyEventAndGetResponse(String command)180 private String sendMonkeyEventAndGetResponse(String command) throws IOException { 181 command = command.trim(); 182 LOG.info("Monkey Command: " + command + "."); 183 184 // send a single command and get the response 185 monkeyWriter.write(command + "\n"); 186 monkeyWriter.flush(); 187 return monkeyReader.readLine(); 188 } 189 190 /** 191 * Parse a monkey response string to see if the command succeeded or not. 192 * 193 * @param monkeyResponse the response 194 * @return true if response code indicated success. 195 */ parseResponseForSuccess(String monkeyResponse)196 private boolean parseResponseForSuccess(String monkeyResponse) { 197 if (monkeyResponse == null) { 198 return false; 199 } 200 // return on ok 201 if(monkeyResponse.startsWith("OK")) { 202 return true; 203 } 204 205 return false; 206 } 207 208 /** 209 * Parse a monkey response string to get the extra data returned. 210 * 211 * @param monkeyResponse the response 212 * @return any extra data that was returned, or empty string if there was nothing. 213 */ parseResponseForExtra(String monkeyResponse)214 private String parseResponseForExtra(String monkeyResponse) { 215 int offset = monkeyResponse.indexOf(':'); 216 if (offset < 0) { 217 return ""; 218 } 219 return monkeyResponse.substring(offset + 1); 220 } 221 222 /** 223 * This function allows the communication bridge between the host and the device 224 * to be invisible to the script for internal needs. 225 * It splits a command into monkey events and waits for responses for each over an 226 * adb tcp socket. 227 * 228 * @param command the monkey command to send to the device 229 * @return true on success. 230 * @throws IOException on error communicating with the device 231 */ sendMonkeyEvent(String command)232 private boolean sendMonkeyEvent(String command) throws IOException { 233 synchronized (this) { 234 String monkeyResponse = sendMonkeyEventAndGetResponse(command); 235 return parseResponseForSuccess(monkeyResponse); 236 } 237 } 238 239 /** 240 * Close all open resources related to this device. 241 */ close()242 public void close() { 243 try { 244 monkeySocket.close(); 245 } catch (IOException e) { 246 LOG.log(Level.SEVERE, "Unable to close monkeySocket", e); 247 } 248 try { 249 monkeyReader.close(); 250 } catch (IOException e) { 251 LOG.log(Level.SEVERE, "Unable to close monkeyReader", e); 252 } 253 try { 254 monkeyWriter.close(); 255 } catch (IOException e) { 256 LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e); 257 } 258 } 259 260 /** 261 * Function to get a static variable from the device. 262 * 263 * @param name name of static variable to get 264 * @return the value of the variable, or null if there was an error 265 * @throws IOException on error communicating with the device 266 */ getVariable(String name)267 public String getVariable(String name) throws IOException { 268 synchronized (this) { 269 String response = sendMonkeyEventAndGetResponse("getvar " + name); 270 if (!parseResponseForSuccess(response)) { 271 return null; 272 } 273 return parseResponseForExtra(response); 274 } 275 } 276 277 /** 278 * Function to get the list of variables from the device. 279 * @return the list of variables as a collection of strings 280 * @throws IOException on error communicating with the device 281 */ listVariable()282 public Collection<String> listVariable() throws IOException { 283 synchronized (this) { 284 String response = sendMonkeyEventAndGetResponse("listvar"); 285 if (!parseResponseForSuccess(response)) { 286 Collections.emptyList(); 287 } 288 String extras = parseResponseForExtra(response); 289 return Lists.newArrayList(extras.split(" ")); 290 } 291 } 292 293 /** 294 * Tells the monkey that we are done for this session. 295 * @throws IOException on error communicating with the device 296 */ done()297 public void done() throws IOException { 298 // this command just drops the connection, so handle it here 299 synchronized (this) { 300 sendMonkeyEventAndGetResponse("done"); 301 } 302 } 303 304 /** 305 * Tells the monkey that we are done forever. 306 * @throws IOException on error communicating with the device 307 */ quit()308 public void quit() throws IOException { 309 // this command drops the connection, so handle it here 310 synchronized (this) { 311 try { 312 sendMonkeyEventAndGetResponse("quit"); 313 } catch (SocketException e) { 314 // flush was called after the call had been written, so it tried flushing to a 315 // broken pipe. 316 } 317 } 318 } 319 320 /** 321 * Send a tap event at the specified location. 322 * 323 * @param x the x coordinate of where to click 324 * @param y the y coordinate of where to click 325 * @return success or not 326 * @throws IOException on error communicating with the device 327 */ tap(int x, int y)328 public boolean tap(int x, int y) throws IOException { 329 return sendMonkeyEvent("tap " + x + " " + y); 330 } 331 332 /** 333 * Type the following string to the monkey. 334 * 335 * @param text the string to type 336 * @return success 337 * @throws IOException on error communicating with the device 338 */ type(String text)339 public boolean type(String text) throws IOException { 340 // The network protocol can't handle embedded line breaks, so we have to handle it 341 // here instead 342 StringTokenizer tok = new StringTokenizer(text, "\n", true); 343 while (tok.hasMoreTokens()) { 344 String line = tok.nextToken(); 345 if ("\n".equals(line)) { 346 boolean success = press(PhysicalButton.ENTER); 347 if (!success) { 348 return false; 349 } 350 } else { 351 boolean success = sendMonkeyEvent("type " + line); 352 if (!success) { 353 return false; 354 } 355 } 356 } 357 return true; 358 } 359 360 /** 361 * Type the character to the monkey. 362 * 363 * @param keyChar the character to type. 364 * @return success 365 * @throws IOException on error communicating with the device 366 */ type(char keyChar)367 public boolean type(char keyChar) throws IOException { 368 return type(Character.toString(keyChar)); 369 } 370 371 /** 372 * Wake the device up from sleep. 373 * @throws IOException on error communicating with the device 374 */ wake()375 public void wake() throws IOException { 376 sendMonkeyEvent("wake"); 377 } 378 379 380 /** 381 * Retrieves the list of view ids from the current application. 382 * @return the list of view ids as a collection of strings 383 * @throws IOException on error communicating with the device 384 */ listViewIds()385 public Collection<String> listViewIds() throws IOException { 386 synchronized (this) { 387 String response = sendMonkeyEventAndGetResponse("listviews"); 388 if (!parseResponseForSuccess(response)) { 389 Collections.emptyList(); 390 } 391 String extras = parseResponseForExtra(response); 392 return Lists.newArrayList(extras.split(" ")); 393 } 394 } 395 396 /** 397 * Queries the on-screen view with the given id and returns the response. 398 * It's up to the calling method to parse the returned String. 399 * @param idType The type of ID to query the view by 400 * @param id The view id of the view 401 * @param query the query 402 * @return the response from the query 403 * @throws IOException on error communicating with the device 404 */ queryView(String idType, List<String> ids, String query)405 public String queryView(String idType, List<String> ids, String query) throws IOException { 406 StringBuilder monkeyCommand = new StringBuilder("queryview " + idType + " "); 407 for(String id : ids) { 408 monkeyCommand.append(id).append(" "); 409 } 410 monkeyCommand.append(query); 411 synchronized (this) { 412 String response = sendMonkeyEventAndGetResponse(monkeyCommand.toString()); 413 if (!parseResponseForSuccess(response)) { 414 throw new ChimpException(parseResponseForExtra(response)); 415 } 416 return parseResponseForExtra(response); 417 } 418 } 419 420 /** 421 * Returns the current root view of the device. 422 * @return the root view of the device 423 */ getRootView()424 public IChimpView getRootView() throws IOException { 425 synchronized(this) { 426 String response = sendMonkeyEventAndGetResponse("getrootview"); 427 String extra = parseResponseForExtra(response); 428 List<String> ids = Arrays.asList(extra.split(" ")); 429 if (!parseResponseForSuccess(response) || ids.size() != 2) { 430 throw new ChimpException(extra); 431 } 432 ChimpView root = new ChimpView(ChimpView.ACCESSIBILITY_IDS, ids); 433 root.setManager(this); 434 return root; 435 } 436 } 437 438 /** 439 * Queries the device for a list of views with the given 440 * @return A string containing the accessibility ids of the views with the given text 441 */ getViewsWithText(String text)442 public String getViewsWithText(String text) throws IOException { 443 synchronized(this) { 444 // Monkey has trouble parsing a single word in quotes 445 if (text.split(" ").length > 1) { 446 text = "\"" + text + "\""; 447 } 448 String response = sendMonkeyEventAndGetResponse("getviewswithtext " + text); 449 if (!parseResponseForSuccess(response)) { 450 throw new ChimpException(parseResponseForExtra(response)); 451 } 452 return parseResponseForExtra(response); 453 } 454 } 455 } 456