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 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 134 GsmMode(String[] tags) { 135 this.tags = tags; 136 } 137 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 */ 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 */ 203 public static synchronized EmulatorConsole getConsole(IDevice d) { 204 // we need to make sure that the device is an emulator 205 Matcher m = sEmulatorRegexp.matcher(d.getSerialNumber()); 206 if (m.matches()) { 207 // get the port number. This is the console port. 208 int port; 209 try { 210 port = Integer.parseInt(m.group(1)); 211 if (port <= 0) { 212 return null; 213 } 214 } catch (NumberFormatException e) { 215 // looks like we failed to get the port number. This is a bit strange since 216 // it's coming from a regexp that only accept digit, but we handle the case 217 // and return null. 218 return null; 219 } 220 221 EmulatorConsole console = sEmulators.get(port); 222 223 if (console != null) { 224 // if the console exist, we ping the emulator to check the connection. 225 if (console.ping() == false) { 226 RemoveConsole(console.mPort); 227 console = null; 228 } 229 } 230 231 if (console == null) { 232 // no console object exists for this port so we create one, and start 233 // the connection. 234 console = new EmulatorConsole(port); 235 if (console.start()) { 236 sEmulators.put(port, console); 237 } else { 238 console = null; 239 } 240 } 241 242 return console; 243 } 244 245 return null; 246 } 247 248 /** 249 * Removes the console object associated with a port from the map. 250 * @param port The port of the console to remove. 251 */ 252 private static synchronized void RemoveConsole(int port) { 253 sEmulators.remove(port); 254 } 255 256 private EmulatorConsole(int port) { 257 super(); 258 mPort = port; 259 } 260 261 /** 262 * Starts the connection of the console. 263 * @return true if success. 264 */ 265 private boolean start() { 266 267 InetSocketAddress socketAddr; 268 try { 269 InetAddress hostAddr = InetAddress.getByName(HOST); 270 socketAddr = new InetSocketAddress(hostAddr, mPort); 271 } catch (UnknownHostException e) { 272 return false; 273 } 274 275 try { 276 mSocketChannel = SocketChannel.open(socketAddr); 277 } catch (IOException e1) { 278 return false; 279 } 280 281 // read some stuff from it 282 readLines(); 283 284 return true; 285 } 286 287 /** 288 * Ping the emulator to check if the connection is still alive. 289 * @return true if the connection is alive. 290 */ 291 private synchronized boolean ping() { 292 // it looks like we can send stuff, even when the emulator quit, but we can't read 293 // from the socket. So we check the return of readLines() 294 if (sendCommand(COMMAND_PING)) { 295 return readLines() != null; 296 } 297 298 return false; 299 } 300 301 /** 302 * Sends a KILL command to the emulator. 303 */ 304 public synchronized void kill() { 305 if (sendCommand(COMMAND_KILL)) { 306 RemoveConsole(mPort); 307 } 308 } 309 310 public synchronized String getAvdName() { 311 if (sendCommand(COMMAND_AVD_NAME)) { 312 String[] result = readLines(); 313 if (result != null && result.length == 2) { // this should be the name on first line, 314 // and ok on 2nd line 315 return result[0]; 316 } else { 317 // try to see if there's a message after KO 318 Matcher m = RE_KO.matcher(result[result.length-1]); 319 if (m.matches()) { 320 return m.group(1); 321 } 322 } 323 } 324 325 return null; 326 } 327 328 /** 329 * Get the network status of the emulator. 330 * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or 331 * <code>null</code> if the query failed. 332 */ 333 public synchronized NetworkStatus getNetworkStatus() { 334 if (sendCommand(COMMAND_NETWORK_STATUS)) { 335 /* Result is in the format 336 Current network status: 337 download speed: 14400 bits/s (1.8 KB/s) 338 upload speed: 14400 bits/s (1.8 KB/s) 339 minimum latency: 0 ms 340 maximum latency: 0 ms 341 */ 342 String[] result = readLines(); 343 344 if (isValid(result)) { 345 // we only compare agains the min latency and the download speed 346 // let's not rely on the order of the output, and simply loop through 347 // the line testing the regexp. 348 NetworkStatus status = new NetworkStatus(); 349 for (String line : result) { 350 Matcher m = sDownloadSpeedRegexp.matcher(line); 351 if (m.matches()) { 352 // get the string value 353 String value = m.group(1); 354 355 // get the index from the list 356 status.speed = getSpeedIndex(value); 357 358 // move on to next line. 359 continue; 360 } 361 362 m = sMinLatencyRegexp.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.latency = getLatencyIndex(value); 369 370 // move on to next line. 371 continue; 372 } 373 } 374 375 return status; 376 } 377 } 378 379 return null; 380 } 381 382 /** 383 * Returns the current gsm status of the emulator 384 * @return a {@link GsmStatus} object containing the gms status, or <code>null</code> 385 * if the query failed. 386 */ 387 public synchronized GsmStatus getGsmStatus() { 388 if (sendCommand(COMMAND_GSM_STATUS)) { 389 /* 390 * result is in the format: 391 * gsm status 392 * gsm voice state: home 393 * gsm data state: home 394 */ 395 396 String[] result = readLines(); 397 if (isValid(result)) { 398 399 GsmStatus status = new GsmStatus(); 400 401 // let's not rely on the order of the output, and simply loop through 402 // the line testing the regexp. 403 for (String line : result) { 404 Matcher m = sVoiceStatusRegexp.matcher(line); 405 if (m.matches()) { 406 // get the string value 407 String value = m.group(1); 408 409 // get the index from the list 410 status.voice = GsmMode.getEnum(value.toLowerCase()); 411 412 // move on to next line. 413 continue; 414 } 415 416 m = sDataStatusRegexp.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.data = GsmMode.getEnum(value.toLowerCase()); 423 424 // move on to next line. 425 continue; 426 } 427 } 428 429 return status; 430 } 431 } 432 433 return null; 434 } 435 436 /** 437 * Sets the GSM voice mode. 438 * @param mode the {@link GsmMode} value. 439 * @return RESULT_OK if success, an error String otherwise. 440 * @throws InvalidParameterException if mode is an invalid value. 441 */ 442 public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException { 443 if (mode == GsmMode.UNKNOWN) { 444 throw new InvalidParameterException(); 445 } 446 447 String command = String.format(COMMAND_GSM_VOICE, mode.getTag()); 448 return processCommand(command); 449 } 450 451 /** 452 * Sets the GSM data mode. 453 * @param mode the {@link GsmMode} value 454 * @return {@link #RESULT_OK} if success, an error String otherwise. 455 * @throws InvalidParameterException if mode is an invalid value. 456 */ 457 public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException { 458 if (mode == GsmMode.UNKNOWN) { 459 throw new InvalidParameterException(); 460 } 461 462 String command = String.format(COMMAND_GSM_DATA, mode.getTag()); 463 return processCommand(command); 464 } 465 466 /** 467 * Initiate an incoming call on the emulator. 468 * @param number a string representing the calling number. 469 * @return {@link #RESULT_OK} if success, an error String otherwise. 470 */ 471 public synchronized String call(String number) { 472 String command = String.format(COMMAND_GSM_CALL, number); 473 return processCommand(command); 474 } 475 476 /** 477 * Cancels a current call. 478 * @param number the number of the call to cancel 479 * @return {@link #RESULT_OK} if success, an error String otherwise. 480 */ 481 public synchronized String cancelCall(String number) { 482 String command = String.format(COMMAND_GSM_CANCEL_CALL, number); 483 return processCommand(command); 484 } 485 486 /** 487 * Sends an SMS to the emulator 488 * @param number The sender phone number 489 * @param message The SMS message. \ characters must be escaped. The carriage return is 490 * the 2 character sequence {'\', 'n' } 491 * 492 * @return {@link #RESULT_OK} if success, an error String otherwise. 493 */ 494 public synchronized String sendSms(String number, String message) { 495 String command = String.format(COMMAND_SMS_SEND, number, message); 496 return processCommand(command); 497 } 498 499 /** 500 * Sets the network speed. 501 * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table. 502 * @return {@link #RESULT_OK} if success, an error String otherwise. 503 */ 504 public synchronized String setNetworkSpeed(int selectionIndex) { 505 String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]); 506 return processCommand(command); 507 } 508 509 /** 510 * Sets the network latency. 511 * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table. 512 * @return {@link #RESULT_OK} if success, an error String otherwise. 513 */ 514 public synchronized String setNetworkLatency(int selectionIndex) { 515 String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]); 516 return processCommand(command); 517 } 518 519 public synchronized String sendLocation(double longitude, double latitude, double elevation) { 520 521 // need to make sure the string format uses dot and not comma 522 Formatter formatter = new Formatter(Locale.US); 523 formatter.format(COMMAND_GPS, longitude, latitude, elevation); 524 525 return processCommand(formatter.toString()); 526 } 527 528 /** 529 * Sends a command to the emulator console. 530 * @param command The command string. <b>MUST BE TERMINATED BY \n</b>. 531 * @return true if success 532 */ 533 private boolean sendCommand(String command) { 534 boolean result = false; 535 try { 536 byte[] bCommand; 537 try { 538 bCommand = command.getBytes(DEFAULT_ENCODING); 539 } catch (UnsupportedEncodingException e) { 540 // wrong encoding... 541 return result; 542 } 543 544 // write the command 545 AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut()); 546 547 result = true; 548 } catch (Exception e) { 549 return false; 550 } finally { 551 if (result == false) { 552 // FIXME connection failed somehow, we need to disconnect the console. 553 RemoveConsole(mPort); 554 } 555 } 556 557 return result; 558 } 559 560 /** 561 * Sends a command to the emulator and parses its answer. 562 * @param command the command to send. 563 * @return {@link #RESULT_OK} if success, an error message otherwise. 564 */ 565 private String processCommand(String command) { 566 if (sendCommand(command)) { 567 String[] result = readLines(); 568 569 if (result != null && result.length > 0) { 570 Matcher m = RE_KO.matcher(result[result.length-1]); 571 if (m.matches()) { 572 return m.group(1); 573 } 574 return RESULT_OK; 575 } 576 577 return "Unable to communicate with the emulator"; 578 } 579 580 return "Unable to send command to the emulator"; 581 } 582 583 /** 584 * Reads line from the console socket. This call is blocking until we read the lines: 585 * <ul> 586 * <li>OK\r\n</li> 587 * <li>KO<msg>\r\n</li> 588 * </ul> 589 * @return the array of strings read from the emulator. 590 */ 591 private String[] readLines() { 592 try { 593 ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length); 594 int numWaits = 0; 595 boolean stop = false; 596 597 while (buf.position() != buf.limit() && stop == false) { 598 int count; 599 600 count = mSocketChannel.read(buf); 601 if (count < 0) { 602 return null; 603 } else if (count == 0) { 604 if (numWaits * WAIT_TIME > STD_TIMEOUT) { 605 return null; 606 } 607 // non-blocking spin 608 try { 609 Thread.sleep(WAIT_TIME); 610 } catch (InterruptedException ie) { 611 } 612 numWaits++; 613 } else { 614 numWaits = 0; 615 } 616 617 // check the last few char aren't OK. For a valid message to test 618 // we need at least 4 bytes (OK/KO + \r\n) 619 if (buf.position() >= 4) { 620 int pos = buf.position(); 621 if (endsWithOK(pos) || lastLineIsKO(pos)) { 622 stop = true; 623 } 624 } 625 } 626 627 String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING); 628 return msg.split("\r\n"); //$NON-NLS-1$ 629 } catch (IOException e) { 630 return null; 631 } 632 } 633 634 /** 635 * Returns true if the 4 characters *before* the current position are "OK\r\n" 636 * @param currentPosition The current position 637 */ 638 private boolean endsWithOK(int currentPosition) { 639 if (mBuffer[currentPosition-1] == '\n' && 640 mBuffer[currentPosition-2] == '\r' && 641 mBuffer[currentPosition-3] == 'K' && 642 mBuffer[currentPosition-4] == 'O') { 643 return true; 644 } 645 646 return false; 647 } 648 649 /** 650 * Returns true if the last line starts with KO and is also terminated by \r\n 651 * @param currentPosition the current position 652 */ 653 private boolean lastLineIsKO(int currentPosition) { 654 // first check that the last 2 characters are CRLF 655 if (mBuffer[currentPosition-1] != '\n' || 656 mBuffer[currentPosition-2] != '\r') { 657 return false; 658 } 659 660 // now loop backward looking for the previous CRLF, or the beginning of the buffer 661 int i = 0; 662 for (i = currentPosition-3 ; i >= 0; i--) { 663 if (mBuffer[i] == '\n') { 664 // found \n! 665 if (i > 0 && mBuffer[i-1] == '\r') { 666 // found \r! 667 break; 668 } 669 } 670 } 671 672 // here it is either -1 if we reached the start of the buffer without finding 673 // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2 674 if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') { 675 // found error! 676 return true; 677 } 678 679 return false; 680 } 681 682 /** 683 * Returns true if the last line of the result does not start with KO 684 */ 685 private boolean isValid(String[] result) { 686 if (result != null && result.length > 0) { 687 return !(RE_KO.matcher(result[result.length-1]).matches()); 688 } 689 return false; 690 } 691 692 private int getLatencyIndex(String value) { 693 try { 694 // get the int value 695 int latency = Integer.parseInt(value); 696 697 // check for the speed from the index 698 for (int i = 0 ; i < MIN_LATENCIES.length; i++) { 699 if (MIN_LATENCIES[i] == latency) { 700 return i; 701 } 702 } 703 } catch (NumberFormatException e) { 704 // Do nothing, we'll just return -1. 705 } 706 707 return -1; 708 } 709 710 private int getSpeedIndex(String value) { 711 try { 712 // get the int value 713 int speed = Integer.parseInt(value); 714 715 // check for the speed from the index 716 for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) { 717 if (DOWNLOAD_SPEEDS[i] == speed) { 718 return i; 719 } 720 } 721 } catch (NumberFormatException e) { 722 // Do nothing, we'll just return -1. 723 } 724 725 return -1; 726 } 727 } 728