From c87a0a0ac401d121ddd282ca1dd03d505f7ab5af Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 14 Mar 2012 14:50:03 -0700 Subject: SdkController: Refactor Emulator + Listener names Simple rename refactoring in the lib: - Emulator => EmulatorConnection - OnEmulatorListener => EmulatorListener Change-Id: I0827b564dd7edec065c634d0860014cb0e15af35 --- .../android/tools/sdkcontroller/lib/Emulator.java | 941 --------------------- .../sdkcontroller/lib/EmulatorConnection.java | 941 +++++++++++++++++++++ .../tools/sdkcontroller/lib/EmulatorListener.java | 61 ++ .../sdkcontroller/lib/OnEmulatorListener.java | 61 -- .../SdkControllerMultitouchActivity.java | 24 +- .../SdkControllerSensorActivity.java | 22 +- 6 files changed, 1027 insertions(+), 1023 deletions(-) delete mode 100755 apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/Emulator.java create mode 100755 apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java create mode 100644 apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java delete mode 100644 apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/OnEmulatorListener.java (limited to 'apps') diff --git a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/Emulator.java b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/Emulator.java deleted file mode 100755 index d1c9669..0000000 --- a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/Emulator.java +++ /dev/null @@ -1,941 +0,0 @@ -/* - * 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.*; -import java.nio.ByteBuffer; -import java.nio.channels.*; -import java.nio.channels.spi.*; -import java.net.*; -import java.util.*; -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. - *

- * Emulator is considered to be "connected" when both channels are connected. - * Emulator 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 Emulator class implements two types of connections. One is - * using synchronous sockets, and another is using asynchronous sockets. The - * type of connection is selected when Emulator instance is created (see - * comments to Emulator'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 Emulator { - /** Defines connection types supported by the Emulator 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 = "Emulator"; - /** Emulator events listener. */ - private OnEmulatorListener 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(); - } - }).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 - - /*************************************************************************** - * Emulator public API - **************************************************************************/ - - /** - * Constructs Emulator instance. - * - * @param port TCP port where emulator connects. - * @param ctype Defines connection type to use (sync / async). See comments - * to Emulator class for more info. - * @throws IOException - */ - public Emulator(int port, EmulatorConnectionType ctype) throws IOException { - constructEmulator(port, ctype); - } - - /** - * Constructs Emulator instance. - * - * @param port TCP port where emulator connects. - * @param ctype Defines connection type to use (sync / async). See comments - * to Emulator class for more info. - * @param listener Emulator event listener. - * @throws IOException - */ - public Emulator(int port, EmulatorConnectionType ctype, OnEmulatorListener listener) - throws IOException { - mListener = listener; - constructEmulator(port, ctype); - } - - - /** - * Disconnects the emulator. - */ - public void disconnect() { - mDisconnect = true; - mSelector.wakeup(); - } - - /** - * Constructs Emulator instance. - * - * @param port TCP port where emulator connects. - * @param ctype Defines connection type to use (sync / async). See comments - * to Emulator class for more info. - * @throws IOException - */ - private void constructEmulator(int port, EmulatorConnectionType ctype) throws IOException { - 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(); - InetSocketAddress address = new InetSocketAddress(local, port); - mServerSocket.socket().bind(address); - - // Register 'accept' I/O on the server socket. - mServerSocket.register(mSelector, SelectionKey.OP_ACCEPT); - - Logv("Emulator listener is created for port " + port); - // Start I/O looper and dispatcher. - new Thread(new Runnable() { - @Override - public void run() { - runIOLooper(); - } - }).start(); - } - - /** - * Sends a notification message to the emulator via 'event' channel. - * - * @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 Emulator"); - } - } - - /** - * 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 setOnEmulatorListener(OnEmulatorListener 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(); - } - } - } - - /*************************************************************************** - * Emulator 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() { - OnEmulatorListener 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() { - OnEmulatorListener 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) { - OnEmulatorListener 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) { - OnEmulatorListener listener; - synchronized (this) { - listener = mListener; - } - if (listener != null) { - return listener.onEmulatorBlobQuery(array); - } else { - return "ko:Service is detached.\0"; - } - } - - /*************************************************************************** - * Emulator implementation - **************************************************************************/ - - /** - * Loops on the selector, handling and dispatching I/O events. - */ - private void runIOLooper() { - try { - Logv("Waiting on Emulator 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("Emulator 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("... Emulator 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); - } -} diff --git a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java new file mode 100755 index 0000000..8bf1fb2 --- /dev/null +++ b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java @@ -0,0 +1,941 @@ +/* + * 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.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.nio.channels.spi.*; +import java.net.*; +import java.util.*; +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(); + } + }).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. + * + * @param port TCP port where emulator connects. + * @param ctype Defines connection type to use (sync / async). See comments + * to EmulatorConnection class for more info. + * @throws IOException + */ + public EmulatorConnection(int port, EmulatorConnectionType ctype) throws IOException { + constructEmulator(port, ctype); + } + + /** + * Constructs EmulatorConnection instance. + * + * @param port TCP port where emulator connects. + * @param ctype Defines connection type to use (sync / async). See comments + * to EmulatorConnection class for more info. + * @param listener EmulatorConnection event listener. + * @throws IOException + */ + public EmulatorConnection(int port, EmulatorConnectionType ctype, EmulatorListener listener) + throws IOException { + mListener = listener; + constructEmulator(port, ctype); + } + + + /** + * Disconnects the emulator. + */ + public void disconnect() { + mDisconnect = true; + mSelector.wakeup(); + } + + /** + * Constructs EmulatorConnection instance. + * + * @param port TCP port where emulator connects. + * @param ctype Defines connection type to use (sync / async). See comments + * to EmulatorConnection class for more info. + * @throws IOException + */ + private void constructEmulator(int port, EmulatorConnectionType ctype) throws IOException { + 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(); + InetSocketAddress address = new InetSocketAddress(local, port); + mServerSocket.socket().bind(address); + + // Register 'accept' I/O on the server socket. + mServerSocket.register(mSelector, SelectionKey.OP_ACCEPT); + + Logv("EmulatorConnection listener is created for port " + port); + // Start I/O looper and dispatcher. + new Thread(new Runnable() { + @Override + public void run() { + runIOLooper(); + } + }).start(); + } + + /** + * Sends a notification message to the emulator via 'event' channel. + * + * @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); + } +} diff --git a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java new file mode 100644 index 0000000..7a5c1af --- /dev/null +++ b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * Encapsulates a listener to emulator events. An object implementing this + * interface must be registered with the EmulatorConnection instance via + * setEmulatorListener method of the EmulatorConnection class. + */ +public interface EmulatorListener { + /** + * 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. + */ + public void 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. + */ + public void onEmulatorDisconnected(); + + /** + * Called when a query is received from the emulator. NOTE: This method is + * called from the I/O loop, so 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. If not an empty string is returned, + * it must be formatted as such: "ok|ko[:reply data]" + */ + public String onEmulatorQuery(String query, String param); + + /** + * Called when a BLOB query is received from the emulator. NOTE: This method + * is called from the I/O loop, so all communication with the emulator will + * be "on hold" until this method returns. + * + * @param array contains BLOB data for the query. + * @return Zero-terminated reply string. If not an empty string is returned, + * it must be formatted as such: "ok|ko[:reply data]" + */ + public String onEmulatorBlobQuery(byte[] array); +} diff --git a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/OnEmulatorListener.java b/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/OnEmulatorListener.java deleted file mode 100644 index 8d4dffa..0000000 --- a/apps/SdkController/SdkControllerLib/src/com/android/tools/sdkcontroller/lib/OnEmulatorListener.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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; - -/** - * Encapsulates a listener to emulator events. An object implementing this - * interface must be registered with the Emulator instance via - * setOnEmulatorListener method of the Emulator class. - */ -public interface OnEmulatorListener { - /** - * 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. - */ - public void 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. - */ - public void onEmulatorDisconnected(); - - /** - * Called when a query is received from the emulator. NOTE: This method is - * called from the I/O loop, so 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. If not an empty string is returned, - * it must be formatted as such: "ok|ko[:reply data]" - */ - public String onEmulatorQuery(String query, String param); - - /** - * Called when a BLOB query is received from the emulator. NOTE: This method - * is called from the I/O loop, so all communication with the emulator will - * be "on hold" until this method returns. - * - * @param array contains BLOB data for the query. - * @return Zero-terminated reply string. If not an empty string is returned, - * it must be formatted as such: "ok|ko[:reply data]" - */ - public String onEmulatorBlobQuery(byte[] array); -} diff --git a/apps/SdkController/SdkControllerMultitouch/src/com/android/tools/sdkcontroller/sdkcontrollermultitouch/SdkControllerMultitouchActivity.java b/apps/SdkController/SdkControllerMultitouch/src/com/android/tools/sdkcontroller/sdkcontrollermultitouch/SdkControllerMultitouchActivity.java index 5d5f2ef..1318a68 100644 --- a/apps/SdkController/SdkControllerMultitouch/src/com/android/tools/sdkcontroller/sdkcontrollermultitouch/SdkControllerMultitouchActivity.java +++ b/apps/SdkController/SdkControllerMultitouch/src/com/android/tools/sdkcontroller/sdkcontrollermultitouch/SdkControllerMultitouchActivity.java @@ -30,9 +30,9 @@ import android.view.View; import android.view.View.OnTouchListener; import android.view.WindowManager; -import com.android.tools.sdkcontroller.lib.Emulator; -import com.android.tools.sdkcontroller.lib.Emulator.EmulatorConnectionType; -import com.android.tools.sdkcontroller.lib.OnEmulatorListener; +import com.android.tools.sdkcontroller.lib.EmulatorConnection; +import com.android.tools.sdkcontroller.lib.EmulatorConnection.EmulatorConnectionType; +import com.android.tools.sdkcontroller.lib.EmulatorListener; /** * Encapsulates an application that monitors multi-touch activities on a device, @@ -40,7 +40,7 @@ import com.android.tools.sdkcontroller.lib.OnEmulatorListener; * machine. This application is used to provide a realistic multi-touch * emulation in Android Emulator. */ -public class SdkControllerMultitouchActivity extends Activity implements OnEmulatorListener { +public class SdkControllerMultitouchActivity extends Activity implements EmulatorListener { /** Tag for logging messages. */ private static final String TAG = "SdkControllerMultitouch"; /** Received frame is JPEG image. */ @@ -51,7 +51,7 @@ public class SdkControllerMultitouchActivity extends Activity implements OnEmula private static final int FRAME_RGB888 = 3; /** TCP over USB connection to the emulator. */ - private Emulator mEmulator; + private EmulatorConnection mEmulator; /** View for this application. */ private MultiTouchView mView; /** Listener to touch events. */ @@ -132,8 +132,9 @@ public class SdkControllerMultitouchActivity extends Activity implements OnEmula // Instantiate emulator connector. try { - mEmulator = new Emulator(Emulator.MULTITOUCH_PORT, - EmulatorConnectionType.SYNC_CONNECTION, this); + mEmulator = new EmulatorConnection(EmulatorConnection.MULTITOUCH_PORT, + EmulatorConnectionType.SYNC_CONNECTION, + this); } catch (IOException e) { Loge("Exception while creating server socket: " + e.getMessage()); finish(); @@ -146,7 +147,7 @@ public class SdkControllerMultitouchActivity extends Activity implements OnEmula super.onPause(); if (mEmulator != null) { - mEmulator.setOnEmulatorListener(null); + mEmulator.setEmulatorListener(null); mEmulator.disconnect(); mEmulator = null; } @@ -183,7 +184,7 @@ public class SdkControllerMultitouchActivity extends Activity implements OnEmula } /*************************************************************************** - * OnEmulatorListener implementation + * EmulatorListener implementation **************************************************************************/ /** @@ -211,8 +212,9 @@ public class SdkControllerMultitouchActivity extends Activity implements OnEmula // Instantiate emulator connector for the next client. try { - mEmulator = new Emulator(Emulator.MULTITOUCH_PORT, - EmulatorConnectionType.SYNC_CONNECTION, this); + mEmulator = new EmulatorConnection(EmulatorConnection.MULTITOUCH_PORT, + EmulatorConnectionType.SYNC_CONNECTION, + this); } catch (IOException e) { Loge("Exception while recreating server socket: " + e.getMessage()); finish(); diff --git a/apps/SdkController/SdkControllerSensor/src/com/android/tools/sdkcontroller/sdkcontrollersensor/SdkControllerSensorActivity.java b/apps/SdkController/SdkControllerSensor/src/com/android/tools/sdkcontroller/sdkcontrollersensor/SdkControllerSensorActivity.java index 1e7d970..d640f2b 100755 --- a/apps/SdkController/SdkControllerSensor/src/com/android/tools/sdkcontroller/sdkcontrollersensor/SdkControllerSensorActivity.java +++ b/apps/SdkController/SdkControllerSensor/src/com/android/tools/sdkcontroller/sdkcontrollersensor/SdkControllerSensorActivity.java @@ -34,9 +34,9 @@ import android.widget.CompoundButton; import android.view.LayoutInflater; import android.util.Log; -import com.android.tools.sdkcontroller.lib.Emulator; -import com.android.tools.sdkcontroller.lib.Emulator.EmulatorConnectionType; -import com.android.tools.sdkcontroller.lib.OnEmulatorListener; +import com.android.tools.sdkcontroller.lib.EmulatorConnection; +import com.android.tools.sdkcontroller.lib.EmulatorConnection.EmulatorConnectionType; +import com.android.tools.sdkcontroller.lib.EmulatorListener; /** * Encapsulates an application that monitors all sensors available on a device, @@ -44,12 +44,12 @@ import com.android.tools.sdkcontroller.lib.OnEmulatorListener; * on the host machine. This application is used to provide a realistic sensor * emulation in Android Emulator. */ -public class SdkControllerSensorActivity extends Activity implements OnEmulatorListener { +public class SdkControllerSensorActivity extends Activity implements EmulatorListener { /** Tag for logging messages. */ private static final String TAG = "SdkControllerSensor"; /** TCP over USB connection to the emulator. */ - private Emulator mEmulator; + private EmulatorConnection mEmulator; /** Array containing monitored sensors. */ private List mSensors; /** Controls displayed list of sensors. */ @@ -371,8 +371,9 @@ public class SdkControllerSensorActivity extends Activity implements OnEmulatorL // Sensor emulator starts very early during emulator startup. So, as // discussed in comments to Emulator class, we must use synchronous // type of connection with the emulator. - mEmulator = new Emulator(Emulator.SENSORS_PORT, EmulatorConnectionType.SYNC_CONNECTION, - this); + mEmulator = new EmulatorConnection(EmulatorConnection.SENSORS_PORT, + EmulatorConnectionType.SYNC_CONNECTION, + this); } catch (IOException e) { Loge("Exception while creating server socket: " + e.getMessage()); finish(); @@ -389,7 +390,7 @@ public class SdkControllerSensorActivity extends Activity implements OnEmulatorL } /*************************************************************************** - * OnEmulatorListener implementation + * EmulatorListener implementation **************************************************************************/ /** @@ -415,8 +416,9 @@ public class SdkControllerSensorActivity extends Activity implements OnEmulatorL // Instantiate emulator connector for the next client. try { - mEmulator = new Emulator(Emulator.SENSORS_PORT, EmulatorConnectionType.SYNC_CONNECTION, - this); + mEmulator = new EmulatorConnection(EmulatorConnection.SENSORS_PORT, + EmulatorConnectionType.SYNC_CONNECTION, + this); } catch (IOException e) { Loge("Exception while creating server socket: " + e.getMessage()); finish(); -- cgit v1.1