/* * Copyright (C) 2011 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.tools.sdkcontroller.lib; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.Iterator; import java.util.Set; import java.util.Vector; import android.util.Log; /** * Encapsulates a connection with the emulator. The connection is established * over a TCP port forwarding enabled with 'adb forward' command. *

* Communication with the emulator is performed via two socket channels * connected to the forwarded TCP port. One channel is a query channel that is * intended solely for receiving queries from the emulator. Another channel is * an event channel that is intended for sending notification messages (events) * to the emulator. *

* EmulatorConnection is considered to be "connected" when both channels are connected. * EmulatorConnection is considered to be "disconnected" when connection with any of the * channels is lost. *

* Instance of this class is operational only for a single connection with the * emulator. Once connection is established and then lost, a new instance of * this class must be created to establish new connection. *

* Note that connection with the device over TCP port forwarding is extremely * fragile at the moment. For whatever reason the connection is even more * fragile if device uses asynchronous sockets (based on java.nio API). So, to * address this issue EmulatorConnection class implements two types of connections. One is * using synchronous sockets, and another is using asynchronous sockets. The * type of connection is selected when EmulatorConnection instance is created (see * comments to EmulatorConnection's constructor). *

* According to the exchange protocol with the emulator, queries, responses to * the queries, and notification messages are all zero-terminated strings. */ public class EmulatorConnection { /** Defines connection types supported by the EmulatorConnection class. */ public enum EmulatorConnectionType { /** Use asynchronous connection (based on java.nio API). */ ASYNC_CONNECTION, /** Use synchronous connection (based on synchronous Socket objects). */ SYNC_CONNECTION, } /** TCP port reserved for the sensors emulation. */ public static final int SENSORS_PORT = 1968; /** TCP port reserved for the multitouch emulation. */ public static final int MULTITOUCH_PORT = 1969; /** Tag for logging messages. */ private static final String TAG = "EmulatorConnection"; /** EmulatorConnection events listener. */ private EmulatorListener mListener; /** I/O selector (looper). */ private Selector mSelector; /** Server socket channel. */ private ServerSocketChannel mServerSocket; /** Query channel. */ private EmulatorChannel mQueryChannel; /** Event channel. */ private EmulatorChannel mEventChannel; /** Selector for the connection type. */ private EmulatorConnectionType mConnectionType; /** Connection status */ private boolean mIsConnected = false; /** Disconnection status */ private boolean mIsDisconnected = false; /** Exit I/O loop flag. */ private boolean mExitIoLoop = false; /** Disconnect flag. */ private boolean mDisconnect = false; /*************************************************************************** * EmulatorChannel - Base class for sync / async channels. **************************************************************************/ /** * Encapsulates a base class for synchronous and asynchronous communication * channels. */ private abstract class EmulatorChannel { /** Identifier for a query channel type. */ private static final String QUERY_CHANNEL = "query"; /** Identifier for an event channel type. */ private static final String EVENT_CHANNEL = "event"; /** BLOB query string. */ private static final String BLOBL_QUERY = "$BLOB"; /*********************************************************************** * Abstract API **********************************************************************/ /** * Sends a message via this channel. * * @param msg Zero-terminated message string to send. */ public abstract void sendMessage(String msg) throws IOException; /** * Closes this channel. */ abstract public void closeChannel() throws IOException; /*********************************************************************** * Public API **********************************************************************/ /** * Constructs EmulatorChannel instance. */ public EmulatorChannel() { } /** * Handles a query received in this channel. * * @param socket A socket through which the query has been received. * @param query_str Query received from this channel. All queries are * formatted as such: : where - * Is a query name that identifies the query, and - * represent parameters for the query. * Query name and query parameters are separated with a ':' * character. */ public void onQueryReceived(Socket socket, String query_str) throws IOException { String query, query_param, response; // Lets see if query has parameters. int sep = query_str.indexOf(':'); if (sep == -1) { // Query has no parameters. query = query_str; query_param = ""; } else { // Separate query name from its parameters. query = query_str.substring(0, sep); // Make sure that substring after the ':' does contain // something, otherwise the query is paramless. query_param = (sep < (query_str.length() - 1)) ? query_str.substring(sep + 1) : ""; } // Handle the query, obtain response string, and reply it back to // the emulator. Note that there is one special query: $BLOB, that // requires reading of a byte array of data first. The size of the // array is defined by the query parameter. if (query.compareTo(BLOBL_QUERY) == 0) { // This is the BLOB query. It must have a parameter which // contains byte size of the blob. final int array_size = Integer.parseInt(query_param); if (array_size > 0) { // Read data from the query's socket. byte[] array = new byte[array_size]; final int transferred = readSocketArray(socket, array); if (transferred == array_size) { // Handle blob query. response = onBlobQuery(array); } else { response = "ko:Transfer failure\0"; } } else { response = "ko:Invalid parameter\0"; } } else { response = onQuery(query, query_param); if (response.length() == 0 || response.charAt(0) == '\0') { Logw("No response to the query " + query_str); } } if (response.length() != 0) { if (response.charAt(response.length() - 1) != '\0') { Logw("Response '" + response + "' to query '" + query + "' does not contain zero-terminator."); } sendMessage(response); } } } // EmulatorChannel /*************************************************************************** * EmulatorSyncChannel - Implements a synchronous channel. **************************************************************************/ /** * Encapsulates a synchronous communication channel with the emulator. */ private class EmulatorSyncChannel extends EmulatorChannel { /** Communication socket. */ private Socket mSocket; /** * Constructs EmulatorSyncChannel instance. * * @param socket Connected ('accept'ed) communication socket. */ public EmulatorSyncChannel(Socket socket) { mSocket = socket; // Start the reader thread. new Thread(new Runnable() { @Override public void run() { theReader(); } }, "EmuSyncChannel").start(); } /*********************************************************************** * Abstract API implementation **********************************************************************/ /** * Sends a message via this channel. * * @param msg Zero-terminated message string to send. */ @Override public void sendMessage(String msg) throws IOException { if (msg.charAt(msg.length() - 1) != '\0') { Logw("Missing zero-terminator in message '" + msg + "'"); } mSocket.getOutputStream().write(msg.getBytes()); } /** * Closes this channel. */ @Override public void closeChannel() throws IOException { mSocket.close(); } /*********************************************************************** * EmulatorSyncChannel implementation **********************************************************************/ /** * The reader thread: loops reading and dispatching queries. */ private void theReader() { try { for (;;) { String query = readSocketString(mSocket); onQueryReceived(mSocket, query); } } catch (IOException e) { onLostConnection(); } } } // EmulatorSyncChannel /*************************************************************************** * EmulatorAsyncChannel - Implements an asynchronous channel. **************************************************************************/ /** * Encapsulates an asynchronous communication channel with the emulator. */ private class EmulatorAsyncChannel extends EmulatorChannel { /** Communication socket channel. */ private SocketChannel mChannel; /** I/O selection key for this channel. */ private SelectionKey mSelectionKey; /** Accumulator for the query string received in this channel. */ private String mQuery = ""; /** * Preallocated character reader that is used when data is read from * this channel. See 'onRead' method for more details. */ private ByteBuffer mIn = ByteBuffer.allocate(1); /** * Currently sent notification message(s). See 'sendMessage', and * 'onWrite' methods for more details. */ private ByteBuffer mOut; /** * Array of pending notification messages. See 'sendMessage', and * 'onWrite' methods for more details. */ private Vector mNotifications = new Vector(); /** * Constructs EmulatorAsyncChannel instance. * * @param channel Accepted socket channel to use for communication. * @throws IOException */ private EmulatorAsyncChannel(SocketChannel channel) throws IOException { // Mark character reader at the beginning, so we can reset it after // next read character has been pulled out from the buffer. mIn.mark(); // Configure communication channel as non-blocking, and register // it with the I/O selector for reading. mChannel = channel; mChannel.configureBlocking(false); mSelectionKey = mChannel.register(mSelector, SelectionKey.OP_READ, this); // Start receiving read I/O. mSelectionKey.selector().wakeup(); } /*********************************************************************** * Abstract API implementation **********************************************************************/ /** * Sends a message via this channel. * * @param msg Zero-terminated message string to send. */ @Override public void sendMessage(String msg) throws IOException { if (msg.charAt(msg.length() - 1) != '\0') { Logw("Missing zero-terminator in message '" + msg + "'"); } synchronized (this) { if (mOut != null) { // Channel is busy with writing another message. // Queue this one. It will be picked up later when current // write operation is completed. mNotifications.add(msg); return; } // No other messages are in progress. Send this one outside of // the lock. mOut = ByteBuffer.wrap(msg.getBytes()); } mChannel.write(mOut); // Lets see if we were able to send the entire message. if (mOut.hasRemaining()) { // Write didn't complete. Schedule write I/O callback to // pick up from where this write has left. enableWrite(); return; } // Entire message has been sent. Lets see if other messages were // queued while we were busy sending this one. for (;;) { synchronized (this) { // Dequeue message that was yielding to this write. if (!dequeueMessage()) { // Writing is over... disableWrite(); mOut = null; return; } } // Send queued message. mChannel.write(mOut); // Lets see if we were able to send the entire message. if (mOut.hasRemaining()) { // Write didn't complete. Schedule write I/O callback to // pick up from where this write has left. enableWrite(); return; } } } /** * Closes this channel. */ @Override public void closeChannel() throws IOException { mSelectionKey.cancel(); synchronized (this) { mNotifications.clear(); } mChannel.close(); } /*********************************************************************** * EmulatorAsyncChannel implementation **********************************************************************/ /** * Reads data from the channel. This method is invoked from the I/O loop * when data is available for reading on this channel. When reading from * a channel we read character-by-character, building the query string * until zero-terminator is read. When zero-terminator is read, we * handle the query, and start building the new query string. * * @throws IOException */ private void onRead() throws IOException, ClosedChannelException { int count = mChannel.read(mIn); Logv("onRead: " + count); while (count == 1) { final char c = (char) mIn.array()[0]; mIn.reset(); if (c == '\0') { // Zero-terminator is read. Process the query, and reset // the query string. onQueryReceived(mChannel.socket(), mQuery); mQuery = ""; } else { // Continue building the query string. mQuery += c; } count = mChannel.read(mIn); } if (count == -1) { // Channel got disconnected. throw new ClosedChannelException(); } else { // "Don't block" in effect. Will get back to reading as soon as // read I/O is available. assert (count == 0); } } /** * Writes data to the channel. This method is ivnoked from the I/O loop * when data is available for writing on this channel. * * @throws IOException */ private void onWrite() throws IOException { if (mOut != null && mOut.hasRemaining()) { // Continue writing to the channel. mChannel.write(mOut); if (mOut.hasRemaining()) { // Write is still incomplete. Come back to it when write I/O // becomes available. return; } } // We're done with the current message. Lets see if we've // accumulated some more while this write was in progress. synchronized (this) { // Dequeue next message into mOut. if (!dequeueMessage()) { // Nothing left to write. disableWrite(); mOut = null; return; } // We don't really want to run a big loop here, flushing the // message queue. The reason is that we're inside the I/O loop, // so we don't want to block others for long. So, we will // continue with queue flushing next time we're picked up by // write I/O event. } } /** * Dequeues messages that were yielding to the write in progress. * Messages will be dequeued directly to the mOut, so it's ready to be * sent when this method returns. NOTE: This method must be called from * within synchronized(this). * * @return true if messages were dequeued, or false if message queue was * empty. */ private boolean dequeueMessage() { // It's tempting to dequeue all messages here, but in practice it's // less performant than dequeuing just one. if (!mNotifications.isEmpty()) { mOut = ByteBuffer.wrap(mNotifications.remove(0).getBytes()); return true; } else { return false; } } /** * Enables write I/O callbacks. */ private void enableWrite() { mSelectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); // Looks like we must wake up the selector. Otherwise it's not going // to immediately pick up on the change that we just made. mSelectionKey.selector().wakeup(); } /** * Disables write I/O callbacks. */ private void disableWrite() { mSelectionKey.interestOps(SelectionKey.OP_READ); } } // EmulatorChannel /*************************************************************************** * EmulatorConnection public API **************************************************************************/ /** * Constructs EmulatorConnection instance. * Caller must call {@link #connect(int, EmulatorConnectionType)} afterwards. * * @param listener EmulatorConnection event listener. Must not be null. */ public EmulatorConnection(EmulatorListener listener) { mListener = listener; } /** * Connects the EmulatorConnection instance. *

* Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main * thread. The caller is responsible to make sure this is NOT called from a main UI thread. * * @param port TCP port where emulator connects. * @param ctype Defines connection type to use (sync / async). See comments * to EmulatorConnection class for more info. * @return This object for chaining calls. */ public EmulatorConnection connect(int port, EmulatorConnectionType ctype) { constructEmulator(port, ctype); return this; } /** * Disconnects the emulator. */ public void disconnect() { mDisconnect = true; mSelector.wakeup(); } /** * Constructs EmulatorConnection instance. *

* Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main * thread. The caller is responsible to make sure this is NOT called from a main UI thread. *

* On error or success, this calls * {@link EmulatorListener#onEmulatorBindResult(boolean, Exception)} to indicate whether * the socket was properly bound. * The IO loop will start after the method reported a successful bind. * * @param port TCP port where emulator connects. * @param ctype Defines connection type to use (sync / async). See comments * to EmulatorConnection class for more info. */ private void constructEmulator(final int port, EmulatorConnectionType ctype) { try { mConnectionType = ctype; // Create I/O looper. mSelector = SelectorProvider.provider().openSelector(); // Create non-blocking server socket that would listen for connections, // and bind it to the given port on the local host. mServerSocket = ServerSocketChannel.open(); mServerSocket.configureBlocking(false); InetAddress local = InetAddress.getLocalHost(); final InetSocketAddress address = new InetSocketAddress(local, port); mServerSocket.socket().bind(address); // Register 'accept' I/O on the server socket. mServerSocket.register(mSelector, SelectionKey.OP_ACCEPT); } catch (IOException e) { mListener.onEmulatorBindResult(false, e); return; } mListener.onEmulatorBindResult(true, null); Logv("EmulatorConnection listener is created for port " + port); // Start I/O looper and dispatcher. new Thread(new Runnable() { @Override public void run() { runIOLooper(); } }, "EmuCnxIoLoop").start(); } /** * Sends a notification message to the emulator via 'event' channel. *

* Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main * thread. The caller is responsible to make sure this is NOT called from a main UI thread. * * @param msg */ public void sendNotification(String msg) { if (mIsConnected) { try { mEventChannel.sendMessage(msg); } catch (IOException e) { onLostConnection(); } } else { Logw("Attempt to send '" + msg + "' to a disconnected EmulatorConnection"); } } /** * Sets or removes a listener to the events generated by this emulator * instance. * * @param listener Listener to set. Passing null with this parameter will * remove the current listener (if there was one). */ public void setEmulatorListener(EmulatorListener listener) { synchronized (this) { mListener = listener; } // Make sure that new listener knows the connection status. if (mListener != null) { if (mIsConnected) { mListener.onEmulatorConnected(); } else if (mIsDisconnected) { mListener.onEmulatorDisconnected(); } } } /*************************************************************************** * EmulatorConnection events **************************************************************************/ /** * Called when emulator is connected. NOTE: This method is called from the * I/O loop, so all communication with the emulator will be "on hold" until * this method returns. */ private void onConnected() { EmulatorListener listener; synchronized (this) { listener = mListener; } if (listener != null) { listener.onEmulatorConnected(); } } /** * Called when emulator is disconnected. NOTE: This method could be called * from the I/O loop, in which case all communication with the emulator will * be "on hold" until this method returns. */ private void onDisconnected() { EmulatorListener listener; synchronized (this) { listener = mListener; } if (listener != null) { listener.onEmulatorDisconnected(); } } /** * Called when a query is received from the emulator. NOTE: This method * could be called from the I/O loop, in which case all communication with * the emulator will be "on hold" until this method returns. * * @param query Name of the query received from the emulator. * @param param Query parameters. * @return Zero-terminated reply string. String must be formatted as such: * "ok|ko[:reply data]" */ private String onQuery(String query, String param) { EmulatorListener listener; synchronized (this) { listener = mListener; } if (listener != null) { return listener.onEmulatorQuery(query, param); } else { return "ko:Service is detached.\0"; } } /** * Called when a BLOB query is received from the emulator. NOTE: This method * could be called from the I/O loop, in which case all communication with * the emulator will be "on hold" until this method returns. * * @param array Array containing blob data. * @return Zero-terminated reply string. String must be formatted as such: * "ok|ko[:reply data]" */ private String onBlobQuery(byte[] array) { EmulatorListener listener; synchronized (this) { listener = mListener; } if (listener != null) { return listener.onEmulatorBlobQuery(array); } else { return "ko:Service is detached.\0"; } } /*************************************************************************** * EmulatorConnection implementation **************************************************************************/ /** * Loops on the selector, handling and dispatching I/O events. */ private void runIOLooper() { try { Logv("Waiting on EmulatorConnection to connect..."); // Check mExitIoLoop before calling 'select', and after in order to // detect condition when mSelector has been waken up to exit the // I/O loop. while (!mExitIoLoop && !mDisconnect && mSelector.select() >= 0 && !mExitIoLoop && !mDisconnect) { Set readyKeys = mSelector.selectedKeys(); Iterator i = readyKeys.iterator(); while (i.hasNext()) { SelectionKey sk = i.next(); i.remove(); if (sk.isAcceptable()) { final int ready = sk.readyOps(); if ((ready & SelectionKey.OP_ACCEPT) != 0) { // Accept new connection. onAccept(((ServerSocketChannel) sk.channel()).accept()); } } else { // Read / write events are expected only on a 'query', // or 'event' asynchronous channels. EmulatorAsyncChannel esc = (EmulatorAsyncChannel) sk.attachment(); if (esc != null) { final int ready = sk.readyOps(); if ((ready & SelectionKey.OP_READ) != 0) { // Read data. esc.onRead(); } if ((ready & SelectionKey.OP_WRITE) != 0) { // Write data. esc.onWrite(); } } else { Loge("No emulator channel found in selection key."); } } } } } catch (ClosedSelectorException e) { } catch (IOException e) { } // Destroy connection on any I/O failure. if (!mExitIoLoop) { onLostConnection(); } } /** * Accepts new connection from the emulator. * * @param channel Connecting socket channel. * @throws IOException */ private void onAccept(SocketChannel channel) throws IOException { // Make sure we're not connected yet. if (mEventChannel != null && mQueryChannel != null) { // We don't accept any more connections after both channels were // connected. Loge("EmulatorConnection is connecting to the already connected instance."); channel.close(); return; } // According to the protocol, each channel identifies itself as a query // or event channel, sending a "cmd", or "event" message right after // the connection. Socket socket = channel.socket(); String socket_type = readSocketString(socket); if (socket_type.contentEquals(EmulatorChannel.QUERY_CHANNEL)) { if (mQueryChannel == null) { // TODO: Find better way to do that! socket.getOutputStream().write("ok\0".getBytes()); if (mConnectionType == EmulatorConnectionType.ASYNC_CONNECTION) { mQueryChannel = new EmulatorAsyncChannel(channel); Logv("Asynchronous query channel is registered."); } else { mQueryChannel = new EmulatorSyncChannel(channel.socket()); Logv("Synchronous query channel is registered."); } } else { // TODO: Find better way to do that! Loge("Duplicate query channel."); socket.getOutputStream().write("ko:Duplicate\0".getBytes()); channel.close(); return; } } else if (socket_type.contentEquals(EmulatorChannel.EVENT_CHANNEL)) { if (mEventChannel == null) { // TODO: Find better way to do that! socket.getOutputStream().write("ok\0".getBytes()); if (mConnectionType == EmulatorConnectionType.ASYNC_CONNECTION) { mEventChannel = new EmulatorAsyncChannel(channel); Logv("Asynchronous event channel is registered."); } else { mEventChannel = new EmulatorSyncChannel(channel.socket()); Logv("Synchronous event channel is registered."); } } else { Loge("Duplicate event channel."); socket.getOutputStream().write("ko:Duplicate\0".getBytes()); channel.close(); return; } } else { Loge("Unknown channel is connecting: " + socket_type); socket.getOutputStream().write("ko:Unknown channel type\0".getBytes()); channel.close(); return; } // Lets see if connection is complete... if (mEventChannel != null && mQueryChannel != null) { // When both, query and event channels are connected, the emulator // is considered to be connected. Logv("... EmulatorConnection is connected."); mIsConnected = true; onConnected(); } } /** * Called when connection to any of the channels has been lost. */ private void onLostConnection() { // Since we're multithreaded, there can be multiple "bangs" from those // threads. We should only handle the first one. boolean first_time = false; synchronized (this) { first_time = mIsConnected; mIsConnected = false; mIsDisconnected = true; } if (first_time) { Logw("Connection with the emulator is lost!"); // Close all channels, exit the I/O loop, and close the selector. try { if (mEventChannel != null) { mEventChannel.closeChannel(); } if (mQueryChannel != null) { mQueryChannel.closeChannel(); } if (mServerSocket != null) { mServerSocket.close(); } if (mSelector != null) { mExitIoLoop = true; mSelector.wakeup(); mSelector.close(); } } catch (IOException e) { Loge("onLostConnection exception: " + e.getMessage()); } // Notify the app about lost connection. onDisconnected(); } } /** * Reads zero-terminated string from a synchronous socket. * * @param socket Socket to read string from. Must be a synchronous socket. * @return String read from the socket. * @throws IOException */ private static String readSocketString(Socket socket) throws IOException { String str = ""; // Current characted received from the input stream. int current_byte = 0; // With port forwarding there is no reliable way how to detect // socket disconnection, other than checking on the input stream // to die ("end of stream" condition). That condition is reported // when input stream's read() method returns -1. while (socket.isConnected() && current_byte != -1) { // Character by character read the input stream, and accumulate // read characters in the command string. The end of the command // is indicated with zero character. current_byte = socket.getInputStream().read(); if (current_byte != -1) { if (current_byte == 0) { // String is completed. return str; } else { // Append read character to the string. str += (char) current_byte; } } } // Got disconnected! throw new ClosedChannelException(); } /** * Reads a block of data from a socket. * * @param socket Socket to read data from. Must be a synchronous socket. * @param array Array where to read data. * @return Number of bytes read from the socket, or -1 on an error. * @throws IOException */ private static int readSocketArray(Socket socket, byte[] array) throws IOException { int in = 0; while (in < array.length) { final int ret = socket.getInputStream().read(array, in, array.length - in); if (ret == -1) { // Got disconnected! throw new ClosedChannelException(); } in += ret; } return in; } /*************************************************************************** * Logging wrappers **************************************************************************/ private void Loge(String log) { Log.e(TAG, log); } private void Logw(String log) { Log.w(TAG, log); } private void Logv(String log) { Log.v(TAG, log); } }