1 /* 2 * Copyright (C) 2007 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 17 package com.android.ddmlib; 18 19 import java.io.IOException; 20 import java.io.UnsupportedEncodingException; 21 import java.net.InetAddress; 22 import java.net.InetSocketAddress; 23 import java.net.UnknownHostException; 24 import java.nio.ByteBuffer; 25 import java.nio.channels.SocketChannel; 26 import java.security.InvalidParameterException; 27 import java.util.Formatter; 28 import java.util.HashMap; 29 import java.util.Locale; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 33 /** 34 * Provides control over emulated hardware of the Android emulator. 35 * <p/>This is basically a wrapper around the command line console normally used with telnet. 36 *<p/> 37 * Regarding line termination handling:<br> 38 * One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most 39 * implementations don't enforce it (the dos one does). In this particular case, this is mostly 40 * irrelevant since we don't use telnet in Java, but that means we want to make 41 * sure we use the same line termination than what the console expects. The console 42 * code removes <code>\r</code> and waits for <code>\n</code>. 43 * <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console. 44 * <p/> 45 * <b>This API will change in the near future.</b> 46 */ 47 public final class EmulatorConsole { 48 49 private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ 50 51 private final static int WAIT_TIME = 5; // spin-wait sleep, in ms 52 53 private final static int STD_TIMEOUT = 5000; // standard delay, in ms 54 55 private final static String HOST = "127.0.0.1"; //$NON-NLS-1$ 56 57 private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$ 58 private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$ 59 private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$ 60 private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$ 61 private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$ 62 private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$ 63 private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$ 64 private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$ 65 private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$ 66 private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$ 67 private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$ 68 private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$ 69 private final static String COMMAND_GPS = "geo fix %1$f %2$f %3$f\r\n"; //$NON-NLS-1$ 70 71 private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$ 72 73 /** 74 * Array of delay values: no delay, gprs, edge/egprs, umts/3d 75 */ 76 public final static int[] MIN_LATENCIES = new int[] { 77 0, // No delay 78 150, // gprs 79 80, // edge/egprs 80 35 // umts/3g 81 }; 82 83 /** 84 * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa. 85 */ 86 public final int[] DOWNLOAD_SPEEDS = new int[] { 87 0, // full speed 88 14400, // gsm 89 43200, // hscsd 90 80000, // gprs 91 236800, // edge/egprs 92 1920000, // umts/3g 93 14400000 // hsdpa 94 }; 95 96 /** Arrays of valid network speeds */ 97 public final static String[] NETWORK_SPEEDS = new String[] { 98 "full", //$NON-NLS-1$ 99 "gsm", //$NON-NLS-1$ 100 "hscsd", //$NON-NLS-1$ 101 "gprs", //$NON-NLS-1$ 102 "edge", //$NON-NLS-1$ 103 "umts", //$NON-NLS-1$ 104 "hsdpa", //$NON-NLS-1$ 105 }; 106 107 /** Arrays of valid network latencies */ 108 public final static String[] NETWORK_LATENCIES = new String[] { 109 "none", //$NON-NLS-1$ 110 "gprs", //$NON-NLS-1$ 111 "edge", //$NON-NLS-1$ 112 "umts", //$NON-NLS-1$ 113 }; 114 115 /** Gsm Mode enum. */ 116 public static enum GsmMode { 117 UNKNOWN((String)null), 118 UNREGISTERED(new String[] { "unregistered", "off" }), 119 HOME(new String[] { "home", "on" }), 120 ROAMING("roaming"), 121 SEARCHING("searching"), 122 DENIED("denied"); 123 124 private final String[] tags; 125 GsmMode(String tag)126 GsmMode(String tag) { 127 if (tag != null) { 128 this.tags = new String[] { tag }; 129 } else { 130 this.tags = new String[0]; 131 } 132 } 133 GsmMode(String[] tags)134 GsmMode(String[] tags) { 135 this.tags = tags; 136 } 137 getEnum(String tag)138 public static GsmMode getEnum(String tag) { 139 for (GsmMode mode : values()) { 140 for (String t : mode.tags) { 141 if (t.equals(tag)) { 142 return mode; 143 } 144 } 145 } 146 return UNKNOWN; 147 } 148 149 /** 150 * Returns the first tag of the enum. 151 */ getTag()152 public String getTag() { 153 if (tags.length > 0) { 154 return tags[0]; 155 } 156 return null; 157 } 158 } 159 160 public final static String RESULT_OK = null; 161 162 private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN); 163 private final static Pattern sVoiceStatusRegexp = Pattern.compile( 164 "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ 165 private final static Pattern sDataStatusRegexp = Pattern.compile( 166 "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ 167 private final static Pattern sDownloadSpeedRegexp = Pattern.compile( 168 "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ 169 private final static Pattern sMinLatencyRegexp = Pattern.compile( 170 "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ 171 172 private final static HashMap<Integer, EmulatorConsole> sEmulators = 173 new HashMap<Integer, EmulatorConsole>(); 174 175 /** Gsm Status class */ 176 public static class GsmStatus { 177 /** Voice status. */ 178 public GsmMode voice = GsmMode.UNKNOWN; 179 /** Data status. */ 180 public GsmMode data = GsmMode.UNKNOWN; 181 } 182 183 /** Network Status class */ 184 public static class NetworkStatus { 185 /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */ 186 public int speed = -1; 187 /** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */ 188 public int latency = -1; 189 } 190 191 private int mPort; 192 193 private SocketChannel mSocketChannel; 194 195 private byte[] mBuffer = new byte[1024]; 196 197 /** 198 * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can 199 * be an already existing console, or a new one if it hadn't been created yet. 200 * @param d The device that the console links to. 201 * @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed. 202 */ getConsole(IDevice d)203 public static synchronized EmulatorConsole getConsole(IDevice d) { 204 // we need to make sure that the device is an emulator 205 // get the port number. This is the console port. 206 Integer port = getEmulatorPort(d.getSerialNumber()); 207 if (port == null) { 208 return null; 209 } 210 211 EmulatorConsole console = sEmulators.get(port); 212 213 if (console != null) { 214 // if the console exist, we ping the emulator to check the connection. 215 if (console.ping() == false) { 216 RemoveConsole(console.mPort); 217 console = null; 218 } 219 } 220 221 if (console == null) { 222 // no console object exists for this port so we create one, and start 223 // the connection. 224 console = new EmulatorConsole(port); 225 if (console.start()) { 226 sEmulators.put(port, console); 227 } else { 228 console = null; 229 } 230 } 231 232 return console; 233 } 234 235 /** 236 * Return port of emulator given its serial number. 237 * 238 * @param serialNumber the emulator's serial number 239 * @return the integer port or <code>null</code> if it could not be determined 240 */ getEmulatorPort(String serialNumber)241 public static Integer getEmulatorPort(String serialNumber) { 242 Matcher m = sEmulatorRegexp.matcher(serialNumber); 243 if (m.matches()) { 244 // get the port number. This is the console port. 245 int port; 246 try { 247 port = Integer.parseInt(m.group(1)); 248 if (port > 0) { 249 return port; 250 } 251 } catch (NumberFormatException e) { 252 // looks like we failed to get the port number. This is a bit strange since 253 // it's coming from a regexp that only accept digit, but we handle the case 254 // and return null. 255 } 256 } 257 return null; 258 } 259 260 /** 261 * Removes the console object associated with a port from the map. 262 * @param port The port of the console to remove. 263 */ RemoveConsole(int port)264 private static synchronized void RemoveConsole(int port) { 265 sEmulators.remove(port); 266 } 267 EmulatorConsole(int port)268 private EmulatorConsole(int port) { 269 super(); 270 mPort = port; 271 } 272 273 /** 274 * Starts the connection of the console. 275 * @return true if success. 276 */ start()277 private boolean start() { 278 279 InetSocketAddress socketAddr; 280 try { 281 InetAddress hostAddr = InetAddress.getByName(HOST); 282 socketAddr = new InetSocketAddress(hostAddr, mPort); 283 } catch (UnknownHostException e) { 284 return false; 285 } 286 287 try { 288 mSocketChannel = SocketChannel.open(socketAddr); 289 } catch (IOException e1) { 290 return false; 291 } 292 293 // read some stuff from it 294 readLines(); 295 296 return true; 297 } 298 299 /** 300 * Ping the emulator to check if the connection is still alive. 301 * @return true if the connection is alive. 302 */ ping()303 private synchronized boolean ping() { 304 // it looks like we can send stuff, even when the emulator quit, but we can't read 305 // from the socket. So we check the return of readLines() 306 if (sendCommand(COMMAND_PING)) { 307 return readLines() != null; 308 } 309 310 return false; 311 } 312 313 /** 314 * Sends a KILL command to the emulator. 315 */ kill()316 public synchronized void kill() { 317 if (sendCommand(COMMAND_KILL)) { 318 RemoveConsole(mPort); 319 } 320 } 321 getAvdName()322 public synchronized String getAvdName() { 323 if (sendCommand(COMMAND_AVD_NAME)) { 324 String[] result = readLines(); 325 if (result != null && result.length == 2) { // this should be the name on first line, 326 // and ok on 2nd line 327 return result[0]; 328 } else { 329 // try to see if there's a message after KO 330 Matcher m = RE_KO.matcher(result[result.length-1]); 331 if (m.matches()) { 332 return m.group(1); 333 } 334 } 335 } 336 337 return null; 338 } 339 340 /** 341 * Get the network status of the emulator. 342 * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or 343 * <code>null</code> if the query failed. 344 */ getNetworkStatus()345 public synchronized NetworkStatus getNetworkStatus() { 346 if (sendCommand(COMMAND_NETWORK_STATUS)) { 347 /* Result is in the format 348 Current network status: 349 download speed: 14400 bits/s (1.8 KB/s) 350 upload speed: 14400 bits/s (1.8 KB/s) 351 minimum latency: 0 ms 352 maximum latency: 0 ms 353 */ 354 String[] result = readLines(); 355 356 if (isValid(result)) { 357 // we only compare agains the min latency and the download speed 358 // let's not rely on the order of the output, and simply loop through 359 // the line testing the regexp. 360 NetworkStatus status = new NetworkStatus(); 361 for (String line : result) { 362 Matcher m = sDownloadSpeedRegexp.matcher(line); 363 if (m.matches()) { 364 // get the string value 365 String value = m.group(1); 366 367 // get the index from the list 368 status.speed = getSpeedIndex(value); 369 370 // move on to next line. 371 continue; 372 } 373 374 m = sMinLatencyRegexp.matcher(line); 375 if (m.matches()) { 376 // get the string value 377 String value = m.group(1); 378 379 // get the index from the list 380 status.latency = getLatencyIndex(value); 381 382 // move on to next line. 383 continue; 384 } 385 } 386 387 return status; 388 } 389 } 390 391 return null; 392 } 393 394 /** 395 * Returns the current gsm status of the emulator 396 * @return a {@link GsmStatus} object containing the gms status, or <code>null</code> 397 * if the query failed. 398 */ getGsmStatus()399 public synchronized GsmStatus getGsmStatus() { 400 if (sendCommand(COMMAND_GSM_STATUS)) { 401 /* 402 * result is in the format: 403 * gsm status 404 * gsm voice state: home 405 * gsm data state: home 406 */ 407 408 String[] result = readLines(); 409 if (isValid(result)) { 410 411 GsmStatus status = new GsmStatus(); 412 413 // let's not rely on the order of the output, and simply loop through 414 // the line testing the regexp. 415 for (String line : result) { 416 Matcher m = sVoiceStatusRegexp.matcher(line); 417 if (m.matches()) { 418 // get the string value 419 String value = m.group(1); 420 421 // get the index from the list 422 status.voice = GsmMode.getEnum(value.toLowerCase(Locale.US)); 423 424 // move on to next line. 425 continue; 426 } 427 428 m = sDataStatusRegexp.matcher(line); 429 if (m.matches()) { 430 // get the string value 431 String value = m.group(1); 432 433 // get the index from the list 434 status.data = GsmMode.getEnum(value.toLowerCase(Locale.US)); 435 436 // move on to next line. 437 continue; 438 } 439 } 440 441 return status; 442 } 443 } 444 445 return null; 446 } 447 448 /** 449 * Sets the GSM voice mode. 450 * @param mode the {@link GsmMode} value. 451 * @return RESULT_OK if success, an error String otherwise. 452 * @throws InvalidParameterException if mode is an invalid value. 453 */ setGsmVoiceMode(GsmMode mode)454 public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException { 455 if (mode == GsmMode.UNKNOWN) { 456 throw new InvalidParameterException(); 457 } 458 459 String command = String.format(COMMAND_GSM_VOICE, mode.getTag()); 460 return processCommand(command); 461 } 462 463 /** 464 * Sets the GSM data mode. 465 * @param mode the {@link GsmMode} value 466 * @return {@link #RESULT_OK} if success, an error String otherwise. 467 * @throws InvalidParameterException if mode is an invalid value. 468 */ setGsmDataMode(GsmMode mode)469 public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException { 470 if (mode == GsmMode.UNKNOWN) { 471 throw new InvalidParameterException(); 472 } 473 474 String command = String.format(COMMAND_GSM_DATA, mode.getTag()); 475 return processCommand(command); 476 } 477 478 /** 479 * Initiate an incoming call on the emulator. 480 * @param number a string representing the calling number. 481 * @return {@link #RESULT_OK} if success, an error String otherwise. 482 */ call(String number)483 public synchronized String call(String number) { 484 String command = String.format(COMMAND_GSM_CALL, number); 485 return processCommand(command); 486 } 487 488 /** 489 * Cancels a current call. 490 * @param number the number of the call to cancel 491 * @return {@link #RESULT_OK} if success, an error String otherwise. 492 */ cancelCall(String number)493 public synchronized String cancelCall(String number) { 494 String command = String.format(COMMAND_GSM_CANCEL_CALL, number); 495 return processCommand(command); 496 } 497 498 /** 499 * Sends an SMS to the emulator 500 * @param number The sender phone number 501 * @param message The SMS message. \ characters must be escaped. The carriage return is 502 * the 2 character sequence {'\', 'n' } 503 * 504 * @return {@link #RESULT_OK} if success, an error String otherwise. 505 */ sendSms(String number, String message)506 public synchronized String sendSms(String number, String message) { 507 String command = String.format(COMMAND_SMS_SEND, number, message); 508 return processCommand(command); 509 } 510 511 /** 512 * Sets the network speed. 513 * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table. 514 * @return {@link #RESULT_OK} if success, an error String otherwise. 515 */ setNetworkSpeed(int selectionIndex)516 public synchronized String setNetworkSpeed(int selectionIndex) { 517 String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]); 518 return processCommand(command); 519 } 520 521 /** 522 * Sets the network latency. 523 * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table. 524 * @return {@link #RESULT_OK} if success, an error String otherwise. 525 */ setNetworkLatency(int selectionIndex)526 public synchronized String setNetworkLatency(int selectionIndex) { 527 String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]); 528 return processCommand(command); 529 } 530 sendLocation(double longitude, double latitude, double elevation)531 public synchronized String sendLocation(double longitude, double latitude, double elevation) { 532 533 // need to make sure the string format uses dot and not comma 534 Formatter formatter = new Formatter(Locale.US); 535 formatter.format(COMMAND_GPS, longitude, latitude, elevation); 536 537 return processCommand(formatter.toString()); 538 } 539 540 /** 541 * Sends a command to the emulator console. 542 * @param command The command string. <b>MUST BE TERMINATED BY \n</b>. 543 * @return true if success 544 */ sendCommand(String command)545 private boolean sendCommand(String command) { 546 boolean result = false; 547 try { 548 byte[] bCommand; 549 try { 550 bCommand = command.getBytes(DEFAULT_ENCODING); 551 } catch (UnsupportedEncodingException e) { 552 // wrong encoding... 553 return result; 554 } 555 556 // write the command 557 AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut()); 558 559 result = true; 560 } catch (Exception e) { 561 return false; 562 } finally { 563 if (result == false) { 564 // FIXME connection failed somehow, we need to disconnect the console. 565 RemoveConsole(mPort); 566 } 567 } 568 569 return result; 570 } 571 572 /** 573 * Sends a command to the emulator and parses its answer. 574 * @param command the command to send. 575 * @return {@link #RESULT_OK} if success, an error message otherwise. 576 */ processCommand(String command)577 private String processCommand(String command) { 578 if (sendCommand(command)) { 579 String[] result = readLines(); 580 581 if (result != null && result.length > 0) { 582 Matcher m = RE_KO.matcher(result[result.length-1]); 583 if (m.matches()) { 584 return m.group(1); 585 } 586 return RESULT_OK; 587 } 588 589 return "Unable to communicate with the emulator"; 590 } 591 592 return "Unable to send command to the emulator"; 593 } 594 595 /** 596 * Reads line from the console socket. This call is blocking until we read the lines: 597 * <ul> 598 * <li>OK\r\n</li> 599 * <li>KO<msg>\r\n</li> 600 * </ul> 601 * @return the array of strings read from the emulator. 602 */ readLines()603 private String[] readLines() { 604 try { 605 ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length); 606 int numWaits = 0; 607 boolean stop = false; 608 609 while (buf.position() != buf.limit() && stop == false) { 610 int count; 611 612 count = mSocketChannel.read(buf); 613 if (count < 0) { 614 return null; 615 } else if (count == 0) { 616 if (numWaits * WAIT_TIME > STD_TIMEOUT) { 617 return null; 618 } 619 // non-blocking spin 620 try { 621 Thread.sleep(WAIT_TIME); 622 } catch (InterruptedException ie) { 623 } 624 numWaits++; 625 } else { 626 numWaits = 0; 627 } 628 629 // check the last few char aren't OK. For a valid message to test 630 // we need at least 4 bytes (OK/KO + \r\n) 631 if (buf.position() >= 4) { 632 int pos = buf.position(); 633 if (endsWithOK(pos) || lastLineIsKO(pos)) { 634 stop = true; 635 } 636 } 637 } 638 639 String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING); 640 return msg.split("\r\n"); //$NON-NLS-1$ 641 } catch (IOException e) { 642 return null; 643 } 644 } 645 646 /** 647 * Returns true if the 4 characters *before* the current position are "OK\r\n" 648 * @param currentPosition The current position 649 */ endsWithOK(int currentPosition)650 private boolean endsWithOK(int currentPosition) { 651 if (mBuffer[currentPosition-1] == '\n' && 652 mBuffer[currentPosition-2] == '\r' && 653 mBuffer[currentPosition-3] == 'K' && 654 mBuffer[currentPosition-4] == 'O') { 655 return true; 656 } 657 658 return false; 659 } 660 661 /** 662 * Returns true if the last line starts with KO and is also terminated by \r\n 663 * @param currentPosition the current position 664 */ lastLineIsKO(int currentPosition)665 private boolean lastLineIsKO(int currentPosition) { 666 // first check that the last 2 characters are CRLF 667 if (mBuffer[currentPosition-1] != '\n' || 668 mBuffer[currentPosition-2] != '\r') { 669 return false; 670 } 671 672 // now loop backward looking for the previous CRLF, or the beginning of the buffer 673 int i = 0; 674 for (i = currentPosition-3 ; i >= 0; i--) { 675 if (mBuffer[i] == '\n') { 676 // found \n! 677 if (i > 0 && mBuffer[i-1] == '\r') { 678 // found \r! 679 break; 680 } 681 } 682 } 683 684 // here it is either -1 if we reached the start of the buffer without finding 685 // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2 686 if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') { 687 // found error! 688 return true; 689 } 690 691 return false; 692 } 693 694 /** 695 * Returns true if the last line of the result does not start with KO 696 */ isValid(String[] result)697 private boolean isValid(String[] result) { 698 if (result != null && result.length > 0) { 699 return !(RE_KO.matcher(result[result.length-1]).matches()); 700 } 701 return false; 702 } 703 getLatencyIndex(String value)704 private int getLatencyIndex(String value) { 705 try { 706 // get the int value 707 int latency = Integer.parseInt(value); 708 709 // check for the speed from the index 710 for (int i = 0 ; i < MIN_LATENCIES.length; i++) { 711 if (MIN_LATENCIES[i] == latency) { 712 return i; 713 } 714 } 715 } catch (NumberFormatException e) { 716 // Do nothing, we'll just return -1. 717 } 718 719 return -1; 720 } 721 getSpeedIndex(String value)722 private int getSpeedIndex(String value) { 723 try { 724 // get the int value 725 int speed = Integer.parseInt(value); 726 727 // check for the speed from the index 728 for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) { 729 if (DOWNLOAD_SPEEDS[i] == speed) { 730 return i; 731 } 732 } 733 } catch (NumberFormatException e) { 734 // Do nothing, we'll just return -1. 735 } 736 737 return -1; 738 } 739 } 740