com.android.ddmlib.EmulatorConsole Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ddmlib Show documentation
Show all versions of ddmlib Show documentation
Library providing APIs to talk to Android devices
/*
* 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.GuardedBy;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.security.InvalidParameterException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provides control over emulated hardware of the Android emulator.
* This is basically a wrapper around the command line console normally used with telnet.
*
* Regarding line termination handling:
* One of the issues is that the telnet protocol requires usage of \r\n
. 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 \r
and waits for \n
.
* However this means you may receive \r\n
when reading from the console.
*
* This API will change in the near future.
*/
public final class EmulatorConsole {
private static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
private static final int WAIT_TIME = 5; // spin-wait sleep, in ms
private static final int STD_TIMEOUT = 5000; // standard delay, in ms
private static final String HOST = "127.0.0.1"; //$NON-NLS-1$
private static final String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
private static final String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
private static final String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
private static final String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
private static final String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
private static final String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
private static final String COMMAND_GPS = "geo fix %1$f %2$f %3$f\r\n"; //$NON-NLS-1$
private static final Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
/**
* Array of delay values: no delay, gprs, edge/egprs, umts/3d
*/
public static final 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 static 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 static final 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 static final 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 static final String RESULT_OK = null;
private static final Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
private static final Pattern sVoiceStatusRegexp = Pattern.compile(
"gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static final Pattern sDataStatusRegexp = Pattern.compile(
"gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static final Pattern sDownloadSpeedRegexp = Pattern.compile(
"\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static final Pattern sMinLatencyRegexp = Pattern.compile(
"\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
@GuardedBy(value = "sEmulators")
private static final HashMap sEmulators =
new HashMap();
private static final String LOG_TAG = "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 = -1;
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.
* Note: emulator consoles don't automatically close when an emulator exists. It is the
* responsibility of higher level code to explicitly call {@link #close()} when the emulator
* corresponding to a open console is killed.
* @param d The device that the console links to.
* @return an EmulatorConsole
object or null
if the connection failed.
*/
@Nullable
public static EmulatorConsole getConsole(IDevice d) {
// we need to make sure that the device is an emulator
// get the port number. This is the console port.
Integer port = getEmulatorPort(d.getSerialNumber());
if (port == null) {
Log.w(LOG_TAG, "Failed to find emulator port from serial: " + d.getSerialNumber());
return null;
}
EmulatorConsole console = retrieveConsole(port);
if (!console.checkConnection()) {
removeConsole(console.mPort);
console = null;
}
return console;
}
/**
* Return port of emulator given its serial number.
*
* @param serialNumber the emulator's serial number
* @return the integer port or null
if it could not be determined
*/
public static Integer getEmulatorPort(String serialNumber) {
Matcher m = sEmulatorRegexp.matcher(serialNumber);
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 port;
}
} 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;
}
/**
* Retrieve a console object for this port, creating if necessary.
*/
@NonNull
private static EmulatorConsole retrieveConsole(int port) {
synchronized (sEmulators) {
EmulatorConsole console = sEmulators.get(port);
if (console == null) {
Log.v(LOG_TAG, "Creating emulator console for " + Integer.toString(port));
console = new EmulatorConsole(port);
sEmulators.put(port, console);
}
return console;
}
}
/**
* Removes the console object associated with a port from the map.
* @param port The port of the console to remove.
*/
private static void removeConsole(int port) {
synchronized (sEmulators) {
Log.v(LOG_TAG, "Removing emulator console for " + Integer.toString(port));
sEmulators.remove(port);
}
}
private EmulatorConsole(int port) {
mPort = port;
}
/**
* Determine if connection to emulator console is functioning. Starts the connection if
* necessary
* @return true if success.
*/
private synchronized boolean checkConnection() {
if (mSocketChannel == null) {
// connection not established, try to connect
InetSocketAddress socketAddr;
try {
InetAddress hostAddr = InetAddress.getByName(HOST);
socketAddr = new InetSocketAddress(hostAddr, mPort);
mSocketChannel = SocketChannel.open(socketAddr);
mSocketChannel.configureBlocking(false);
// read initial output from console
readLines();
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to start Emulator console for " + Integer.toString(mPort));
return false;
}
}
return ping();
}
/**
* 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)) {
close();
}
}
/**
* Closes this instance of the emulator console.
*/
public synchronized void close() {
if (mPort == -1) {
return;
}
removeConsole(mPort);
try {
if (mSocketChannel != null) {
mSocketChannel.close();
}
mSocketChannel = null;
mPort = -1;
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to close EmulatorConsole channel");
}
}
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);
}
Log.w(LOG_TAG, "avd name result did not match expected");
for (int i=0; i < result.length; i++) {
Log.d(LOG_TAG, result[i]);
}
}
}
return null;
}
/**
* Get the network status of the emulator.
* @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
* null
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 against 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 null
* 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(Locale.US));
// 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(Locale.US));
// 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) {
// need to make sure the string format uses dot and not comma
Formatter formatter = new Formatter(Locale.US);
try {
formatter.format(COMMAND_GPS, longitude, latitude, elevation);
return processCommand(formatter.toString());
} finally {
formatter.close();
}
}
/**
* Sends a command to the emulator console.
* @param command The command string. MUST BE TERMINATED BY \n.
* @return true if success
*/
private boolean sendCommand(String command) {
boolean result = false;
try {
byte[] bCommand;
try {
bCommand = command.getBytes(DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.w(LOG_TAG, "wrong encoding when sending " + command + " to " +
Integer.toString(mPort));
// wrong encoding...
return result;
}
// write the command
AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut());
result = true;
} catch (Exception e) {
Log.d(LOG_TAG, "Exception sending command " + command + " to " +
Integer.toString(mPort));
return false;
} finally {
if (!result) {
// 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:
*
* - OK\r\n
* - KO
\r\n
*
* @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) {
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) {
Log.d(LOG_TAG, "Exception reading lines for " + Integer.toString(mPort));
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) {
return mBuffer[currentPosition - 1] == '\n' &&
mBuffer[currentPosition - 2] == '\r' &&
mBuffer[currentPosition - 3] == 'K' &&
mBuffer[currentPosition - 4] == 'O';
}
/**
* 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;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy