blob: 6ac019de4777d3fb731c072f068298475f1cb062 [file] [log] [blame]
/*
* Copyright (C) 2007 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.ddmlib;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.security.InvalidParameterException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provides control over emulated hardware of the Android emulator.
* <p/>This is basically a wrapper around the command line console normally used with telnet.
*<p/>
* Regarding line termination handling:<br>
* One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most
* implementations don't enforce it (the dos one does). In this particular case, this is mostly
* irrelevant since we don't use telnet in Java, but that means we want to make
* sure we use the same line termination than what the console expects. The console
* code removes <code>\r</code> and waits for <code>\n</code>.
* <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console.
* <p/>
* <b>This API will change in the near future.</b>
*/
public final class EmulatorConsole {
private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
private final static int WAIT_TIME = 5; // spin-wait sleep, in ms
private final static int STD_TIMEOUT = 5000; // standard delay, in ms
private final static String HOST = "127.0.0.1"; //$NON-NLS-1$
private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GPS =
"geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$
"%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$
"1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$
private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
/**
* Array of delay values: no delay, gprs, edge/egprs, umts/3d
*/
public final static int[] MIN_LATENCIES = new int[] {
0, // No delay
150, // gprs
80, // edge/egprs
35 // umts/3g
};
/**
* Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa.
*/
public final int[] DOWNLOAD_SPEEDS = new int[] {
0, // full speed
14400, // gsm
43200, // hscsd
80000, // gprs
236800, // edge/egprs
1920000, // umts/3g
14400000 // hsdpa
};
/** Arrays of valid network speeds */
public final static String[] NETWORK_SPEEDS = new String[] {
"full", //$NON-NLS-1$
"gsm", //$NON-NLS-1$
"hscsd", //$NON-NLS-1$
"gprs", //$NON-NLS-1$
"edge", //$NON-NLS-1$
"umts", //$NON-NLS-1$
"hsdpa", //$NON-NLS-1$
};
/** Arrays of valid network latencies */
public final static String[] NETWORK_LATENCIES = new String[] {
"none", //$NON-NLS-1$
"gprs", //$NON-NLS-1$
"edge", //$NON-NLS-1$
"umts", //$NON-NLS-1$
};
/** Gsm Mode enum. */
public static enum GsmMode {
UNKNOWN((String)null),
UNREGISTERED(new String[] { "unregistered", "off" }),
HOME(new String[] { "home", "on" }),
ROAMING("roaming"),
SEARCHING("searching"),
DENIED("denied");
private final String[] tags;
GsmMode(String tag) {
if (tag != null) {
this.tags = new String[] { tag };
} else {
this.tags = new String[0];
}
}
GsmMode(String[] tags) {
this.tags = tags;
}
public static GsmMode getEnum(String tag) {
for (GsmMode mode : values()) {
for (String t : mode.tags) {
if (t.equals(tag)) {
return mode;
}
}
}
return UNKNOWN;
}
/**
* Returns the first tag of the enum.
*/
public String getTag() {
if (tags.length > 0) {
return tags[0];
}
return null;
}
}
public final static String RESULT_OK = null;
private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
private final static Pattern sVoiceStatusRegexp = Pattern.compile(
"gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sDataStatusRegexp = Pattern.compile(
"gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sDownloadSpeedRegexp = Pattern.compile(
"\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sMinLatencyRegexp = Pattern.compile(
"\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static HashMap<Integer, EmulatorConsole> sEmulators =
new HashMap<Integer, EmulatorConsole>();
/** Gsm Status class */
public static class GsmStatus {
/** Voice status. */
public GsmMode voice = GsmMode.UNKNOWN;
/** Data status. */
public GsmMode data = GsmMode.UNKNOWN;
}
/** Network Status class */
public static class NetworkStatus {
/** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
public int speed = -1;
/** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */
public int latency = -1;
}
private int mPort;
private SocketChannel mSocketChannel;
private byte[] mBuffer = new byte[1024];
/**
* Returns an {@link EmulatorConsole} object for the given {@link Device}. This can
* be an already existing console, or a new one if it hadn't been created yet.
* @param d The device that the console links to.
* @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed.
*/
public static synchronized EmulatorConsole getConsole(IDevice d) {
// we need to make sure that the device is an emulator
Matcher m = sEmulatorRegexp.matcher(d.getSerialNumber());
if (m.matches()) {
// get the port number. This is the console port.
int port;
try {
port = Integer.parseInt(m.group(1));
if (port <= 0) {
return null;
}
} catch (NumberFormatException e) {
// looks like we failed to get the port number. This is a bit strange since
// it's coming from a regexp that only accept digit, but we handle the case
// and return null.
return null;
}
EmulatorConsole console = sEmulators.get(port);
if (console != null) {
// if the console exist, we ping the emulator to check the connection.
if (console.ping() == false) {
RemoveConsole(console.mPort);
console = null;
}
}
if (console == null) {
// no console object exists for this port so we create one, and start
// the connection.
console = new EmulatorConsole(port);
if (console.start()) {
sEmulators.put(port, console);
} else {
console = null;
}
}
return console;
}
return null;
}
/**
* Removes the console object associated with a port from the map.
* @param port The port of the console to remove.
*/
private static synchronized void RemoveConsole(int port) {
sEmulators.remove(port);
}
private EmulatorConsole(int port) {
super();
mPort = port;
}
/**
* Starts the connection of the console.
* @return true if success.
*/
private boolean start() {
InetSocketAddress socketAddr;
try {
InetAddress hostAddr = InetAddress.getByName(HOST);
socketAddr = new InetSocketAddress(hostAddr, mPort);
} catch (UnknownHostException e) {
return false;
}
try {
mSocketChannel = SocketChannel.open(socketAddr);
} catch (IOException e1) {
return false;
}
// read some stuff from it
readLines();
return true;
}
/**
* Ping the emulator to check if the connection is still alive.
* @return true if the connection is alive.
*/
private synchronized boolean ping() {
// it looks like we can send stuff, even when the emulator quit, but we can't read
// from the socket. So we check the return of readLines()
if (sendCommand(COMMAND_PING)) {
return readLines() != null;
}
return false;
}
/**
* Sends a KILL command to the emulator.
*/
public synchronized void kill() {
if (sendCommand(COMMAND_KILL)) {
RemoveConsole(mPort);
}
}
public synchronized String getAvdName() {
if (sendCommand(COMMAND_AVD_NAME)) {
String[] result = readLines();
if (result != null && result.length == 2) { // this should be the name on first line,
// and ok on 2nd line
return result[0];
} else {
// try to see if there's a message after KO
Matcher m = RE_KO.matcher(result[result.length-1]);
if (m.matches()) {
return m.group(1);
}
}
}
return null;
}
/**
* Get the network status of the emulator.
* @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
* <code>null</code> if the query failed.
*/
public synchronized NetworkStatus getNetworkStatus() {
if (sendCommand(COMMAND_NETWORK_STATUS)) {
/* Result is in the format
Current network status:
download speed: 14400 bits/s (1.8 KB/s)
upload speed: 14400 bits/s (1.8 KB/s)
minimum latency: 0 ms
maximum latency: 0 ms
*/
String[] result = readLines();
if (isValid(result)) {
// we only compare agains the min latency and the download speed
// let's not rely on the order of the output, and simply loop through
// the line testing the regexp.
NetworkStatus status = new NetworkStatus();
for (String line : result) {
Matcher m = sDownloadSpeedRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.speed = getSpeedIndex(value);
// move on to next line.
continue;
}
m = sMinLatencyRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.latency = getLatencyIndex(value);
// move on to next line.
continue;
}
}
return status;
}
}
return null;
}
/**
* Returns the current gsm status of the emulator
* @return a {@link GsmStatus} object containing the gms status, or <code>null</code>
* if the query failed.
*/
public synchronized GsmStatus getGsmStatus() {
if (sendCommand(COMMAND_GSM_STATUS)) {
/*
* result is in the format:
* gsm status
* gsm voice state: home
* gsm data state: home
*/
String[] result = readLines();
if (isValid(result)) {
GsmStatus status = new GsmStatus();
// let's not rely on the order of the output, and simply loop through
// the line testing the regexp.
for (String line : result) {
Matcher m = sVoiceStatusRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.voice = GsmMode.getEnum(value.toLowerCase());
// move on to next line.
continue;
}
m = sDataStatusRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.data = GsmMode.getEnum(value.toLowerCase());
// move on to next line.
continue;
}
}
return status;
}
}
return null;
}
/**
* Sets the GSM voice mode.
* @param mode the {@link GsmMode} value.
* @return RESULT_OK if success, an error String otherwise.
* @throws InvalidParameterException if mode is an invalid value.
*/
public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException {
if (mode == GsmMode.UNKNOWN) {
throw new InvalidParameterException();
}
String command = String.format(COMMAND_GSM_VOICE, mode.getTag());
return processCommand(command);
}
/**
* Sets the GSM data mode.
* @param mode the {@link GsmMode} value
* @return {@link #RESULT_OK} if success, an error String otherwise.
* @throws InvalidParameterException if mode is an invalid value.
*/
public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException {
if (mode == GsmMode.UNKNOWN) {
throw new InvalidParameterException();
}
String command = String.format(COMMAND_GSM_DATA, mode.getTag());
return processCommand(command);
}
/**
* Initiate an incoming call on the emulator.
* @param number a string representing the calling number.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String call(String number) {
String command = String.format(COMMAND_GSM_CALL, number);
return processCommand(command);
}
/**
* Cancels a current call.
* @param number the number of the call to cancel
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String cancelCall(String number) {
String command = String.format(COMMAND_GSM_CANCEL_CALL, number);
return processCommand(command);
}
/**
* Sends an SMS to the emulator
* @param number The sender phone number
* @param message The SMS message. \ characters must be escaped. The carriage return is
* the 2 character sequence {'\', 'n' }
*
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String sendSms(String number, String message) {
String command = String.format(COMMAND_SMS_SEND, number, message);
return processCommand(command);
}
/**
* Sets the network speed.
* @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String setNetworkSpeed(int selectionIndex) {
String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]);
return processCommand(command);
}
/**
* Sets the network latency.
* @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String setNetworkLatency(int selectionIndex) {
String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]);
return processCommand(command);
}
public synchronized String sendLocation(double longitude, double latitude, double elevation) {
Calendar c = Calendar.getInstance();
double absLong = Math.abs(longitude);
int longDegree = (int)Math.floor(absLong);
char longDirection = 'E';
if (longitude < 0) {
longDirection = 'W';
}
double longMinute = (absLong - Math.floor(absLong)) * 60;
double absLat = Math.abs(latitude);
int latDegree = (int)Math.floor(absLat);
char latDirection = 'N';
if (latitude < 0) {
latDirection = 'S';
}
double latMinute = (absLat - Math.floor(absLat)) * 60;
String command = String.format(COMMAND_GPS,
c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE),
c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND),
latDegree, latMinute, latDirection,
longDegree, longMinute, longDirection);
return processCommand(command);
}
/**
* Sends a command to the emulator console.
* @param command The command string. <b>MUST BE TERMINATED BY \n</b>.
* @return true if success
*/
private boolean sendCommand(String command) {
boolean result = false;
try {
byte[] bCommand;
try {
bCommand = command.getBytes(DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
// wrong encoding...
return result;
}
// write the command
AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut());
result = true;
} catch (IOException e) {
return false;
} finally {
if (result == false) {
// FIXME connection failed somehow, we need to disconnect the console.
RemoveConsole(mPort);
}
}
return result;
}
/**
* Sends a command to the emulator and parses its answer.
* @param command the command to send.
* @return {@link #RESULT_OK} if success, an error message otherwise.
*/
private String processCommand(String command) {
if (sendCommand(command)) {
String[] result = readLines();
if (result != null && result.length > 0) {
Matcher m = RE_KO.matcher(result[result.length-1]);
if (m.matches()) {
return m.group(1);
}
return RESULT_OK;
}
return "Unable to communicate with the emulator";
}
return "Unable to send command to the emulator";
}
/**
* Reads line from the console socket. This call is blocking until we read the lines:
* <ul>
* <li>OK\r\n</li>
* <li>KO<msg>\r\n</li>
* </ul>
* @return the array of strings read from the emulator.
*/
private String[] readLines() {
try {
ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length);
int numWaits = 0;
boolean stop = false;
while (buf.position() != buf.limit() && stop == false) {
int count;
count = mSocketChannel.read(buf);
if (count < 0) {
return null;
} else if (count == 0) {
if (numWaits * WAIT_TIME > STD_TIMEOUT) {
return null;
}
// non-blocking spin
try {
Thread.sleep(WAIT_TIME);
} catch (InterruptedException ie) {
}
numWaits++;
} else {
numWaits = 0;
}
// check the last few char aren't OK. For a valid message to test
// we need at least 4 bytes (OK/KO + \r\n)
if (buf.position() >= 4) {
int pos = buf.position();
if (endsWithOK(pos) || lastLineIsKO(pos)) {
stop = true;
}
}
}
String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING);
return msg.split("\r\n"); //$NON-NLS-1$
} catch (IOException e) {
return null;
}
}
/**
* Returns true if the 4 characters *before* the current position are "OK\r\n"
* @param currentPosition The current position
*/
private boolean endsWithOK(int currentPosition) {
if (mBuffer[currentPosition-1] == '\n' &&
mBuffer[currentPosition-2] == '\r' &&
mBuffer[currentPosition-3] == 'K' &&
mBuffer[currentPosition-4] == 'O') {
return true;
}
return false;
}
/**
* Returns true if the last line starts with KO and is also terminated by \r\n
* @param currentPosition the current position
*/
private boolean lastLineIsKO(int currentPosition) {
// first check that the last 2 characters are CRLF
if (mBuffer[currentPosition-1] != '\n' ||
mBuffer[currentPosition-2] != '\r') {
return false;
}
// now loop backward looking for the previous CRLF, or the beginning of the buffer
int i = 0;
for (i = currentPosition-3 ; i >= 0; i--) {
if (mBuffer[i] == '\n') {
// found \n!
if (i > 0 && mBuffer[i-1] == '\r') {
// found \r!
break;
}
}
}
// here it is either -1 if we reached the start of the buffer without finding
// a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2
if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') {
// found error!
return true;
}
return false;
}
/**
* Returns true if the last line of the result does not start with KO
*/
private boolean isValid(String[] result) {
if (result != null && result.length > 0) {
return !(RE_KO.matcher(result[result.length-1]).matches());
}
return false;
}
private int getLatencyIndex(String value) {
try {
// get the int value
int latency = Integer.parseInt(value);
// check for the speed from the index
for (int i = 0 ; i < MIN_LATENCIES.length; i++) {
if (MIN_LATENCIES[i] == latency) {
return i;
}
}
} catch (NumberFormatException e) {
// Do nothing, we'll just return -1.
}
return -1;
}
private int getSpeedIndex(String value) {
try {
// get the int value
int speed = Integer.parseInt(value);
// check for the speed from the index
for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) {
if (DOWNLOAD_SPEEDS[i] == speed) {
return i;
}
}
} catch (NumberFormatException e) {
// Do nothing, we'll just return -1.
}
return -1;
}
}