diff options
Diffstat (limited to 'apps/SdkController/src/com/android/tools/sdkcontroller/lib')
4 files changed, 1566 insertions, 0 deletions
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java new file mode 100644 index 000000000..639f4cfd4 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2012 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 android.os.Message; +import android.util.Log; + +import com.android.tools.sdkcontroller.service.ControllerService; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Encapsulates basics of a connection with the emulator. + * This class must be used as a base class for all the channelss that provide + * particular type of emulation (such as sensors, multi-touch, etc.) + * <p/> + * Essentially, Channel is an implementation of a particular emulated functionality, + * that defines logical format of the data transferred between the emulator and + * SDK controller. For instance, "sensors" is a channel that emulates sensors, + * and transfers sensor value changes from the device to the emulator. "Multi-touch" + * is a channel that supports multi-touch emulation, and transfers multi-touch + * events to the emulator, while receiving frame buffer updates from the emulator. + * <p/> + * Besides connection with the emulator, each channel may contain one or more UI + * components associated with it. This class provides some basics for UI support, + * including: + * <p/> + * - Providing a way to register / unregister a UI component with the channel. + * <p/> + * - Implementing posting of messages to emulator in opposite to direct message + * sent. This is due to requirement that UI threads are prohibited from doing + * network I/O. + */ +public abstract class Channel { + + /** + * Encapsulates a message posted to be sent to the emulator from a worker + * thread. This class is used to describe a message that is posted in UI + * thread, and then picked up in the worker thread. + */ + private class SdkControllerMessage { + /** Message type. */ + private int mMessageType; + /** Message data (can be null). */ + private byte[] mMessage; + /** Message data size */ + private int mMessageSize; + + /** + * Construct message from an array. + * + * @param type Message type. + * @param message Message data. Message data size is defined by size of + * the array. + */ + public SdkControllerMessage(int type, byte[] message) { + mMessageType = type; + mMessage = message; + mMessageSize = (message != null) ? message.length : 0; + } + + /** + * Construct message from a ByteBuffer. + * + * @param type Message type. + * @param message Message data. Message data size is defined by + * position() property of the ByteBuffer. + */ + public SdkControllerMessage(int type, ByteBuffer message) { + mMessageType = type; + if (message != null) { + mMessage = message.array(); + mMessageSize = message.position(); + } else { + mMessage = null; + mMessageSize = 0; + } + } + + /** + * Gets message type. + + * + * @return Message type. + */ + public int getMessageType() { + return mMessageType; + } + + /** + * Gets message buffer. + * + * @return Message buffer. + */ + public byte[] getMessage() { + return mMessage; + } + + /** + * Gets message buffer size. + * + * @return Message buffer size. + */ + public int getMessageSize() { + return mMessageSize; + } + } // SdkControllerMessage + + /* + * Names for currently implemented SDK controller channels. + */ + + /** Name for a channel that handles sensors emulation */ + public static final String SENSOR_CHANNEL = "sensors"; + /** Name for a channel that handles multi-touch emulation */ + public static final String MULTITOUCH_CHANNEL = "multi-touch"; + + /* + * Types of messages internally used by Channel class. + */ + + /** Service-side emulator is connected. */ + private static final int MSG_CONNECTED = -1; + /** Service-side emulator is disconnected. */ + private static final int MSG_DISCONNECTED = -2; + /** Service-side emulator is enabled. */ + private static final int MSG_ENABLED = -3; + /** Service-side emulator is disabled. */ + private static final int MSG_DISABLED = -4; + + /** Tag for logging messages. */ + private static final String TAG = "SdkControllerChannel"; + /** Controls debug log. */ + private static final boolean DEBUG = false; + + /** Service that has created this object. */ + protected ControllerService mService; + + /* + * Socket stuff. + */ + + /** Socket to use to to communicate with the emulator. */ + private Socket mSocket = null; + /** Channel name ("sensors", "multi-touch", etc.) */ + private String mChannelName; + /** Endianness of data transferred in this channel. */ + private ByteOrder mEndian; + + /* + * Message posting support. + */ + + /** Total number of messages posted in this channel */ + private final AtomicInteger mMsgCount = new AtomicInteger(0); + /** Flags whether or not message thread is running. */ + private volatile boolean mRunMsgQueue = true; + /** Queue of messages pending transmission. */ + private final BlockingQueue<SdkControllerMessage> + mMsgQueue = new LinkedBlockingQueue<SdkControllerMessage>(); + /** Message thread */ + private final Thread mMsgThread; + + /* + * UI support. + */ + + /** Lists UI handlers attached to this channel. */ + private final List<android.os.Handler> mUiHandlers = new ArrayList<android.os.Handler>(); + + /* + * Abstract methods. + */ + + /** + * This method is invoked when this channel is fully connected with its + * counterpart in the emulator. + */ + public abstract void onEmulatorConnected(); + + /** + * This method is invoked when this channel loses connection with its + * counterpart in the emulator. + */ + public abstract void onEmulatorDisconnected(); + + /** + * A message has been received from the emulator. + * + * @param msg_type Message type. + * @param msg_data Message data. Message data size is defined by the length + * of the array wrapped by the ByteBuffer. + */ + public abstract void onEmulatorMessage(int msg_type, ByteBuffer msg_data); + + /** + * A query has been received from the emulator. + * + * @param query_id Identifies the query. This ID must be used when replying + * to the query. + * @param query_type Query type. + * @param query_data Query data. Query data size is defined by the length of + * the array wrapped by the ByteBuffer. + */ + public abstract void onEmulatorQuery(int query_id, int query_type, ByteBuffer query_data); + + /* + * Channel implementation. + */ + + /** + * Constructs Channel instance. + * + * @param name Channel name. + */ + public Channel(ControllerService service, String name) { + mService = service; + mChannelName = name; + // Start the worker thread for posted messages. + mMsgThread = new Thread(new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "MsgThread.started-" + mChannelName); + while (mRunMsgQueue) { + try { + SdkControllerMessage msg = mMsgQueue.take(); + if (msg != null) { + sendMessage( + msg.getMessageType(), msg.getMessage(), msg.getMessageSize()); + mMsgCount.incrementAndGet(); + } + } catch (InterruptedException e) { + Log.e(TAG, "MsgThread-" + mChannelName, e); + } + } + if (DEBUG) Log.d(TAG, "MsgThread.terminate-" + mChannelName); + } + }, "MsgThread-" + name); + mMsgThread.start(); + if (DEBUG) Log.d(TAG, "Channel is constructed for " + mChannelName); + } + + /** + * Gets name for this channel. + * + * @return Emulator name. + */ + public String getChannelName() { + return mChannelName; + } + + /** + * Gets endianness for this channel. + * + * @return Channel endianness. + */ + public ByteOrder getEndian() { + return mEndian; + } + + /** + * Gets number of messages sent via postMessage method. + * + * @return Number of messages sent via postMessage method. + */ + public int getMsgSentCount() { + return mMsgCount.get(); + } + + /** + * Checks if this channel is connected with the emulator. + * + * @return true if this channel is connected with the emulator, or false if it is + * not connected. + */ + public boolean isConnected() { + // Use local copy of the socket, ensuring it's not going to NULL while + // we're working with it. If it gets closed, while we're in the middle + // of data transfer - it's OK, since it will produce an exception, and + // the caller will gracefully handle it. + // + // Same technique is used everywhere in this class where mSocket member + // is touched. + Socket socket = mSocket; + return socket != null && socket.isConnected(); + } + + /** + * Establishes connection with the emulator. This method is called by Connection + * object when emulator successfully connects to this channel, or this channel + * gets registered, and there is a pending socket connection for it. + * + * @param socket Channel connection socket. + */ + public void connect(Socket socket) { + mSocket = socket; + mEndian = socket.getEndian(); + Logv("Channel " + mChannelName + " is now connected with the emulator."); + // Notify the emulator that connection is established. + sendMessage(MSG_CONNECTED, (byte[]) null); + + // Let the derived class know that emulator is connected, and start the + // I/O loop in which we will receive data from the emulator. Note that + // we start the loop after onEmulatorConnected call, since we don't want + // to start dispatching messages before the derived class could set + // itself up for receiving them. + onEmulatorConnected(); + new Thread(new Runnable() { + @Override + public void run() { + runIOLooper(); + } + }, "ChannelIoLoop").start(); + mService.notifyStatusChanged(); + } + + /** + * Disconnects this channel from the emulator. + * + * @return true if this channel has been disconnected in this call, or false if + * channel has been already disconnected when this method has been called. + */ + public boolean disconnect() { + // This is the only place in this class where we will null the + // socket object. Since this method can be called concurrently from + // different threads, lets do this under the lock. + Socket socket; + synchronized (this) { + socket = mSocket; + mSocket = null; + } + if (socket != null) { + // Notify the emulator about channel disconnection before we close + // the communication socket. + try { + sendMessage(socket, MSG_DISCONNECTED, null, 0); + } catch (IOException e) { + // Ignore I/O exception at this point. We don't care about + // it, since the socket is being closed anyways. + } + // This will eventually stop I/O looper thread. + socket.close(); + mService.notifyStatusChanged(); + } + return socket != null; + } + + /** + * Enables the emulation. Typically, this method is called for channels that are + * dependent on UI to handle the emulation. For instance, multi-touch emulation is + * disabled until at least one UI component is attached to the channel. So, for + * multi-touch emulation this method is called when UI gets attached to the channel. + */ + public void enable() { + postMessage(MSG_ENABLED, (byte[]) null); + mService.notifyStatusChanged(); + } + + /** + * Disables the emulation. Just the opposite to enable(). For multi-touch this + * method is called when UI detaches from the channel. + */ + public void disable() { + postMessage(MSG_DISABLED, (byte[]) null); + mService.notifyStatusChanged(); + } + + /** + * Sends message to the emulator. + * + * @param socket Socket to send the message to. + * @param msg_type Message type. + * @param msg Message data to send. + * @param len Byte size of message data. + * @throws IOException + */ + private void sendMessage(Socket socket, int msg_type, byte[] msg, int len) + throws IOException { + // In async environment we must have message header and message data in + // one block to prevent messages from other threads getting between the + // header and the data. So, we can't sent header, and then the data. We + // must combine them in one data block instead. + ByteBuffer bb = ByteBuffer.allocate(ProtocolConstants.MESSAGE_HEADER_SIZE + len); + bb.order(mEndian); + + // Initialize message header. + bb.putInt(ProtocolConstants.PACKET_SIGNATURE); + bb.putInt(ProtocolConstants.MESSAGE_HEADER_SIZE + len); + bb.putInt(ProtocolConstants.PACKET_TYPE_MESSAGE); + bb.putInt(msg_type); + + // Save message data (if there is any). + if (len != 0) { + bb.put(msg, 0, len); + } + + socket.send(bb.array()); + } + + /** + * Sends message to the emulator. + * + * @param msg_type Message type. + * @param msg Message data to send. Message size is defined by the size of + * the array. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendMessage(int msg_type, byte[] msg, int msg_len) { + try { + Socket socket = mSocket; + if (socket != null) { + sendMessage(socket, msg_type, msg, msg_len); + return true; + } else { + Logw("sendMessage is called on disconnected Channel " + mChannelName); + } + } catch (IOException e) { + Loge("Exception " + e + " in sendMessage for Channel " + mChannelName); + onIoFailure(); + } + return false; + } + + /** + * Sends message to the emulator. + * + * @param msg_type Message type. + * @param msg Message data to send. Message size is defined by the size of + * the array. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendMessage(int msg_type, byte[] msg) { + try { + Socket socket = mSocket; + if (socket != null) { + if (msg != null) { + sendMessage(socket, msg_type, msg, msg.length); + } else { + sendMessage(socket, msg_type, null, 0); + } + return true; + } else { + Logw("sendMessage is called on disconnected Channel " + mChannelName); + } + } catch (IOException e) { + Loge("Exception " + e + " in sendMessage for Channel " + mChannelName); + onIoFailure(); + } + return false; + } + + /** + * Sends message to the emulator. + * + * @param msg_type Message type. + * @param msg Message data to send. Message size is defined by the + * position() property of the ByteBuffer. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendMessage(int msg_type, ByteBuffer msg) { + try { + Socket socket = mSocket; + if (socket != null) { + if (msg != null) { + sendMessage(socket, msg_type, msg.array(), msg.position()); + } else { + sendMessage(socket, msg_type, null, 0); + } + return true; + } else { + Logw("sendMessage is called on disconnected Channel " + mChannelName); + } + } catch (IOException e) { + Loge("Exception " + e + " in sendMessage for Channel " + mChannelName); + onIoFailure(); + } + return false; + } + + /** + * Posts message to the emulator. + * + * @param msg_type Message type. + * @param msg Message data to post. Message size is defined by the size of + * the array. + */ + public void postMessage(int msg_type, byte[] msg) { + try { + mMsgQueue.put(new SdkControllerMessage(msg_type, msg)); + } catch (InterruptedException e) { + Log.e(TAG, "mMessageQueue.put", e); + } + } + + /** + * Posts message to the emulator. + * + * @param msg_type Message type. + * @param msg Message data to post. Message size is defined by the + * position() property of the ByteBuffer. + */ + public void postMessage(int msg_type, ByteBuffer msg) { + try { + mMsgQueue.put(new SdkControllerMessage(msg_type, msg)); + } catch (InterruptedException e) { + Log.e(TAG, "mMessageQueue.put", e); + } + } + + /** + * Sends query response to the emulator. + * + * @param query_id Query identifier. + * @param qresp Response to the query. + * @param len Byte size of query response data. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendQueryResponse(int query_id, byte[] qresp, int len) { + // Just like with messages, we must combine header and data in a single + // transmitting block. + ByteBuffer bb = ByteBuffer.allocate(ProtocolConstants.QUERY_RESP_HEADER_SIZE + len); + bb.order(mEndian); + + // Initialize response header. + bb.putInt(ProtocolConstants.PACKET_SIGNATURE); + bb.putInt(ProtocolConstants.QUERY_RESP_HEADER_SIZE + len); + bb.putInt(ProtocolConstants.PACKET_TYPE_QUERY_RESPONSE); + bb.putInt(query_id); + + // Save response data (if there is any). + if (qresp != null && len != 0) { + bb.put(qresp, 0, len); + } + + // Send the response. + try { + Socket socket = mSocket; + if (socket != null) { + socket.send(bb.array()); + return true; + } else { + Logw("sendQueryResponse is called on disconnected Channel " + + mChannelName); + } + } catch (IOException e) { + Loge("Exception " + e + " in sendQueryResponse for Channel " + mChannelName); + onIoFailure(); + } + return false; + } + + /** + * Sends query response to the emulator. + * + * @param query_id Query identifier. + * @param qresp Response to the query. Query response size is defined by the + * size of the array. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendQueryResponse(int query_id, byte[] qresp) { + return (qresp != null) ? sendQueryResponse(query_id, qresp, qresp.length) : + sendQueryResponse(query_id, null, 0); + } + + /** + * Sends query response to the emulator. + * + * @param query_id Query identifier. + * @param qresp Response to the query. Query response size is defined by the + * position() property of the ByteBuffer. + * @return true on success, or false if data transmission has failed. + */ + public boolean sendQueryResponse(int query_id, ByteBuffer qresp) { + return (qresp != null) ? sendQueryResponse(query_id, qresp.array(), qresp.position()) : + sendQueryResponse(query_id, null, 0); + } + + /** + * Handles an I/O failure occurred in the channel. + */ + private void onIoFailure() { + // All I/O failures cause disconnection. + if (disconnect()) { + // Success of disconnect() indicates that I/O failure is not the + // result of a disconnection request, but is in deed an I/O + // failure. Report lost connection to the derived class. + Loge("Connection with the emulator has been lost in Channel " + mChannelName); + onEmulatorDisconnected(); + } + } + + /** + * Loops on the local socket, handling connection attempts. + */ + private void runIOLooper() { + if (DEBUG) Log.d(TAG, "In I/O looper for Channel " + mChannelName); + // Initialize byte buffer large enough to receive packet header. + ByteBuffer header = ByteBuffer.allocate(ProtocolConstants.PACKET_HEADER_SIZE); + header.order(mEndian); + try { + // Since disconnection (which will null the mSocket) can be + // requested from outside of this thread, it's simpler just to make + // a copy of mSocket here, and work with that copy. Otherwise we + // will have to go through a complex synchronization algorithm that + // would decrease performance on normal runs. If socket gets closed + // while we're in the middle of transfer, an exception will occur, + // which we will catch and handle properly. + Socket socket = mSocket; + while (socket != null) { + // Reset header position. + header.position(0); + // This will receive total packet size + packet type. + socket.receive(header.array()); + // First - signature. + final int signature = header.getInt(); + assert signature == ProtocolConstants.PACKET_SIGNATURE; + // Next - packet size (including header). + int remains = header.getInt() - ProtocolConstants.PACKET_HEADER_SIZE; + // After the size comes packet type. + final int packet_type = header.getInt(); + + // Get the remainder of the data, and dispatch the packet to + // an appropriate handler. + switch (packet_type) { + case ProtocolConstants.PACKET_TYPE_MESSAGE: + // Read message header (one int: message type). + final int ext = ProtocolConstants.MESSAGE_HEADER_SIZE - ProtocolConstants.PACKET_HEADER_SIZE; + header.position(0); + socket.receive(header.array(), ext); + final int msg_type = header.getInt(); + + // Read message data. + remains -= ext; + final ByteBuffer msg_data = ByteBuffer.allocate(remains); + msg_data.order(mEndian); + socket.receive(msg_data.array()); + + // Dispatch message for handling. + onEmulatorMessage(msg_type, msg_data); + break; + + case ProtocolConstants.PACKET_TYPE_QUERY: + // Read query ID and query type. + final int extq = ProtocolConstants.QUERY_HEADER_SIZE - ProtocolConstants.PACKET_HEADER_SIZE; + header.position(0); + socket.receive(header.array(), extq); + final int query_id = header.getInt(); + final int query_type = header.getInt(); + + // Read query data. + remains -= extq; + final ByteBuffer query_data = ByteBuffer.allocate(remains); + query_data.order(mEndian); + socket.receive(query_data.array()); + + // Dispatch query for handling. + onEmulatorQuery(query_id, query_type, query_data); + break; + + default: + // Unknown packet type. Just discard the remainder + // of the packet + Loge("Unknown packet type " + packet_type + " in Channel " + + mChannelName); + final byte[] discard_data = new byte[remains]; + socket.receive(discard_data); + break; + } + socket = mSocket; + } + } catch (IOException e) { + Loge("Exception " + e + " in I/O looper for Channel " + mChannelName); + onIoFailure(); + } + if (DEBUG) Log.d(TAG, "Exiting I/O looper for Channel " + mChannelName); + } + + /** + * Indicates any UI handler is currently registered with the channel. If no UI + * is displaying the channel's state, maybe the channel can skip UI related tasks. + * + * @return True if there's at least one UI handler registered. + */ + public boolean hasUiHandler() { + return !mUiHandlers.isEmpty(); + } + + /** + * Registers a new UI handler. + * + * @param uiHandler A non-null UI handler to register. Ignored if the UI + * handler is null or already registered. + */ + public void addUiHandler(android.os.Handler uiHandler) { + assert uiHandler != null; + if (uiHandler != null) { + if (!mUiHandlers.contains(uiHandler)) { + mUiHandlers.add(uiHandler); + } + } + } + + /** + * Unregisters an UI handler. + * + * @param uiHandler A non-null UI listener to unregister. Ignored if the + * listener is null or already registered. + */ + public void removeUiHandler(android.os.Handler uiHandler) { + assert uiHandler != null; + mUiHandlers.remove(uiHandler); + } + + /** + * Protected method to be used by handlers to send an event to all UI + * handlers. + * + * @param event An integer event code with no specific parameters. To be + * defined by the handler itself. + */ + protected void notifyUiHandlers(int event) { + for (android.os.Handler uiHandler : mUiHandlers) { + uiHandler.sendEmptyMessage(event); + } + } + + /** + * Protected method to be used by handlers to send an event to all UI + * handlers. + * + * @param msg An event with parameters. To be defined by the handler itself. + */ + protected void notifyUiHandlers(Message msg) { + for (android.os.Handler uiHandler : mUiHandlers) { + uiHandler.sendMessage(msg); + } + } + + /** + * A helper routine that expands ByteBuffer to contain given number of extra + * bytes. + * + * @param buff Buffer to expand. + * @param extra Number of bytes that are required to be available in the + * buffer after current position() + * @return ByteBuffer, containing required number of available bytes. + */ + public ByteBuffer ExpandIf(ByteBuffer buff, int extra) { + if (extra <= buff.remaining()) { + return buff; + } + ByteBuffer ret = ByteBuffer.allocate(buff.position() + extra); + ret.order(buff.order()); + ret.put(buff.array(), 0, buff.position()); + return ret; + } + + /*************************************************************************** + * Logging wrappers + **************************************************************************/ + + private void Loge(String log) { + mService.addError(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/src/com/android/tools/sdkcontroller/lib/Connection.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java new file mode 100644 index 000000000..cb5086905 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2012 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.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import android.util.Log; +import android.net.LocalServerSocket; +import android.net.LocalSocket; + +import com.android.tools.sdkcontroller.lib.Channel; +import com.android.tools.sdkcontroller.service.ControllerService; + +/** + * Encapsulates a connection between SdkController service and the emulator. On + * the device side, the connection is bound to the UNIX-domain socket named + * 'android.sdk.controller'. On the emulator side the connection is established + * via TCP port that is used to forward I/O traffic on the host machine to + * 'android.sdk.controller' socket on the device. Typically, the port forwarding + * can be enabled using adb command: + * <p/> + * 'adb forward tcp:<TCP port number> localabstract:android.sdk.controller' + * <p/> + * The way communication between the emulator and SDK controller service works + * is as follows: + * <p/> + * 1. Both sides, emulator and the service have components that implement a particular + * type of emulation. For instance, AndroidSensorsPort in the emulator, and + * SensorChannel in the application implement sensors emulation. + * Emulation channels are identified by unique names. For instance, sensor emulation + * is done via "sensors" channel, multi-touch emulation is done via "multi-touch" + * channel, etc. + * <p/> + * 2. Channels are connected to emulator via separate socket instance (though all + * of the connections share the same socket address). + * <p/> + * 3. Connection is initiated by the emulator side, while the service provides + * its side (a channel) that implement functionality and exchange protocol required + * by the requested type of emulation. + * <p/> + * Given that, the main responsibilities of this class are: + * <p/> + * 1. Bind to "android.sdk.controller" socket, listening to emulator connections. + * <p/> + * 2. Maintain a list of service-side channels registered by the application. + * <p/> + * 3. Bind emulator connection with service-side channel via port name, provided by + * the emulator. + * <p/> + * 4. Monitor connection state with the emulator, and automatically restore the + * connection once it is lost. + */ +public class Connection { + /** UNIX-domain name reserved for SDK controller. */ + public static final String SDK_CONTROLLER_PORT = "android.sdk.controller"; + /** Tag for logging messages. */ + private static final String TAG = "SdkControllerConnection"; + /** Controls debug logging */ + private static final boolean DEBUG = false; + + /** Server socket used to listen to emulator connections. */ + private LocalServerSocket mServerSocket = null; + /** Service that has created this object. */ + private ControllerService mService; + /** + * List of connected emulator sockets, pending for a channel to be registered. + * <p/> + * Emulator may connect to SDK controller before the app registers a channel + * for that connection. In this case (when app-side channel is not registered + * with this class) we will keep emulator connection in this list, pending + * for the app-side channel to register. + */ + private List<Socket> mPendingSockets = new ArrayList<Socket>(); + /** + * List of registered app-side channels. + * <p/> + * Channels that are kept in this list may be disconnected from (or pending + * connection with) the emulator, or they may be connected with the + * emulator. + */ + private List<Channel> mChannels = new ArrayList<Channel>(); + + /** + * Constructs Connection instance. + */ + public Connection(ControllerService service) { + mService = service; + if (DEBUG) Log.d(TAG, "SdkControllerConnection is constructed."); + } + + /** + * Binds to the socket, and starts the listening thread. + */ + public void connect() { + if (DEBUG) Log.d(TAG, "SdkControllerConnection is connecting..."); + // Start connection listener. + new Thread(new Runnable() { + @Override + public void run() { + runIOLooper(); + } + }, "SdkControllerConnectionIoLoop").start(); + } + + /** + * Stops the listener, and closes the socket. + * + * @return true if connection has been stopped in this call, or false if it + * has been already stopped when this method has been called. + */ + public boolean disconnect() { + // This is the only place in this class where we will null the + // socket object. Since this method can be called concurrently from + // different threads, lets do this under the lock. + LocalServerSocket socket; + synchronized (this) { + socket = mServerSocket; + mServerSocket = null; + } + if (socket != null) { + if (DEBUG) Log.d(TAG, "SdkControllerConnection is stopping I/O looper..."); + // Stop accepting new connections. + wakeIOLooper(socket); + try { + socket.close(); + } catch (Exception e) { + } + + // Close all the pending sockets, and clear pending socket list. + if (DEBUG) Log.d(TAG, "SdkControllerConnection is closing pending sockets..."); + for (Socket pending_socket : mPendingSockets) { + pending_socket.close(); + } + mPendingSockets.clear(); + + // Disconnect all the emualtors. + if (DEBUG) Log.d(TAG, "SdkControllerConnection is disconnecting channels..."); + for (Channel channel : mChannels) { + if (channel.disconnect()) { + channel.onEmulatorDisconnected(); + } + } + if (DEBUG) Log.d(TAG, "SdkControllerConnection is disconnected."); + } + return socket != null; + } + + /** + * Registers SDK controller channel. + * + * @param channel SDK controller emulator to register. + * @return true if channel has been registered successfully, or false if channel + * with the same name is already registered. + */ + public boolean registerChannel(Channel channel) { + for (Channel check_channel : mChannels) { + if (check_channel.getChannelName().equals(channel.getChannelName())) { + Loge("Registering a duplicate Channel " + channel.getChannelName()); + return false; + } + } + if (DEBUG) Log.d(TAG, "Registering Channel " + channel.getChannelName()); + mChannels.add(channel); + + // Lets see if there is a pending socket for this channel. + for (Socket pending_socket : mPendingSockets) { + if (pending_socket.getChannelName().equals(channel.getChannelName())) { + // Remove the socket from the pending list, and connect the registered channel with it. + if (DEBUG) Log.d(TAG, "Found pending Socket for registering Channel " + + channel.getChannelName()); + mPendingSockets.remove(pending_socket); + channel.connect(pending_socket); + } + } + return true; + } + + /** + * Checks if at least one socket connection exists with channel. + * + * @return true if at least one socket connection exists with channel. + */ + public boolean isEmulatorConnected() { + for (Channel channel : mChannels) { + if (channel.isConnected()) { + return true; + } + } + return !mPendingSockets.isEmpty(); + } + + /** + * Gets Channel instance for the given channel name. + * + * @param name Channel name to get Channel instance for. + * @return Channel instance for the given channel name, or NULL if no + * channel has been registered for that name. + */ + public Channel getChannel(String name) { + for (Channel channel : mChannels) { + if (channel.getChannelName().equals(name)) { + return channel; + } + } + return null; + } + + /** + * Gets connected emulator socket that is pending for service-side channel + * registration. + * + * @param name Channel name to lookup Socket for. + * @return Connected emulator socket that is pending for service-side channel + * registration, or null if no socket is pending for service-size + * channel registration. + */ + private Socket getPendingSocket(String name) { + for (Socket socket : mPendingSockets) { + if (socket.getChannelName().equals(name)) { + return socket; + } + } + return null; + } + + /** + * Wakes I/O looper waiting on connection with the emulator. + * + * @param socket Server socket waiting on connection. + */ + private void wakeIOLooper(LocalServerSocket socket) { + // We wake the looper by connecting to the socket. + LocalSocket waker = new LocalSocket(); + try { + waker.connect(socket.getLocalSocketAddress()); + } catch (IOException e) { + Loge("Exception " + e + " in SdkControllerConnection while waking up the I/O looper."); + } + } + + /** + * Loops on the local socket, handling emulator connection attempts. + */ + private void runIOLooper() { + if (DEBUG) Log.d(TAG, "In SdkControllerConnection I/O looper."); + do { + try { + // Create non-blocking server socket that would listen for connections, + // and bind it to the given port on the local host. + mServerSocket = new LocalServerSocket(SDK_CONTROLLER_PORT); + LocalServerSocket socket = mServerSocket; + while (socket != null) { + final LocalSocket sk = socket.accept(); + if (mServerSocket != null) { + onAccept(sk); + } else { + break; + } + socket = mServerSocket; + } + } catch (IOException e) { + Loge("Exception " + e + "SdkControllerConnection I/O looper."); + } + if (DEBUG) Log.d(TAG, "Exiting SdkControllerConnection I/O looper."); + + // If we're exiting the internal loop for reasons other than an explicit + // disconnect request, we should reconnect again. + } while (disconnect()); + } + + /** + * Accepts new connection from the emulator. + * + * @param sock Connecting socket. + * @throws IOException + */ + private void onAccept(LocalSocket sock) throws IOException { + final ByteBuffer handshake = ByteBuffer.allocate(ProtocolConstants.QUERY_HEADER_SIZE); + + // By protocol, first byte received from newly connected emulator socket + // indicates host endianness. + Socket.receive(sock, handshake.array(), 1); + final ByteOrder endian = (handshake.getChar() == 0) ? ByteOrder.LITTLE_ENDIAN : + ByteOrder.BIG_ENDIAN; + handshake.order(endian); + + // Right after that follows the handshake query header. + handshake.position(0); + Socket.receive(sock, handshake.array(), handshake.array().length); + + // First int - signature + final int signature = handshake.getInt(); + assert signature == ProtocolConstants.PACKET_SIGNATURE; + // Second int - total query size (including fixed query header) + final int remains = handshake.getInt() - ProtocolConstants.QUERY_HEADER_SIZE; + // After that - header type (which must be SDKCTL_PACKET_TYPE_QUERY) + final int msg_type = handshake.getInt(); + assert msg_type == ProtocolConstants.PACKET_TYPE_QUERY; + // After that - query ID. + final int query_id = handshake.getInt(); + // And finally, query type (which must be ProtocolConstants.QUERY_HANDSHAKE for + // handshake query) + final int query_type = handshake.getInt(); + assert query_type == ProtocolConstants.QUERY_HANDSHAKE; + // Verify that received is a query. + if (msg_type != ProtocolConstants.PACKET_TYPE_QUERY) { + // Message type is not a query. Lets read and discard the remainder + // of the message. + if (remains > 0) { + Loge("Unexpected handshake message type: " + msg_type); + byte[] discard = new byte[remains]; + Socket.receive(sock, discard, discard.length); + } + return; + } + + // Receive query data. + final byte[] name_array = new byte[remains]; + Socket.receive(sock, name_array, name_array.length); + + // Prepare response header. + handshake.position(0); + handshake.putInt(ProtocolConstants.PACKET_SIGNATURE); + // Handshake reply is just one int. + handshake.putInt(ProtocolConstants.QUERY_RESP_HEADER_SIZE + 4); + handshake.putInt(ProtocolConstants.PACKET_TYPE_QUERY_RESPONSE); + handshake.putInt(query_id); + + // Verify that received query is in deed a handshake query. + if (query_type != ProtocolConstants.QUERY_HANDSHAKE) { + // Query is not a handshake. Reply with failure. + Loge("Unexpected handshake query type: " + query_type); + handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_QUERY_UNKNOWN); + sock.getOutputStream().write(handshake.array()); + return; + } + + // Handshake query data consist of SDK controller channel name. + final String channel_name = new String(name_array); + if (DEBUG) Log.d(TAG, "Handshake received for channel " + channel_name); + + // Respond to query depending on service-side channel availability + final Channel channel = getChannel(channel_name); + Socket sk = null; + + if (channel != null) { + if (channel.isConnected()) { + // This is a duplicate connection. + Loge("Duplicate connection to a connected Channel " + channel_name); + handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_DUP); + } else { + // Connecting to a registered channel. + if (DEBUG) Log.d(TAG, "Emulator is connected to a registered Channel " + channel_name); + handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_CONNECTED); + } + } else { + // Make sure that there are no other channel connections for this + // channel name. + if (getPendingSocket(channel_name) != null) { + // This is a duplicate. + Loge("Duplicate connection to a pending Socket " + channel_name); + handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_DUP); + } else { + // Connecting to a channel that has not been registered yet. + if (DEBUG) Log.d(TAG, "Emulator is connected to a pending Socket " + channel_name); + handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_NOPORT); + sk = new Socket(sock, channel_name, endian); + mPendingSockets.add(sk); + } + } + + // Send handshake reply. + sock.getOutputStream().write(handshake.array()); + + // If a disconnected channel for emulator connection has been found, + // connect it. + if (channel != null && !channel.isConnected()) { + if (DEBUG) Log.d(TAG, "Connecting Channel " + channel_name + " with emulator."); + sk = new Socket(sock, channel_name, endian); + channel.connect(sk); + } + + mService.notifyStatusChanged(); + } + + /*************************************************************************** + * Logging wrappers + **************************************************************************/ + + private void Loge(String log) { + mService.addError(log); + Log.e(TAG, log); + } +} diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java new file mode 100644 index 000000000..32abf2bc0 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java @@ -0,0 +1,146 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.android.tools.sdkcontroller.lib; + +/** + * Contains declarations of constants that are tied to emulator implementation. + * These constants can be changed only simultaneously in both places. + */ +public final class ProtocolConstants { + /* + * Constants related to data transfer. + */ + + /** Signature of a packet sent via SDK controller socket ('SDKC') */ + public static final int PACKET_SIGNATURE = 0x53444B43; + + /* + * Header sizes for packets sent / received by SDK controller emulator. + */ + + /** + * 12 bytes (3 ints) for the packet header: + * <p/> + * - Signature. + * <p/> + * - Total packet size. + * <p/> + * - Packet type. + */ + public static final int PACKET_HEADER_SIZE = 12; + /** + * 16 bytes (4 ints) for the message header: + * <p/> + * - Common packet header. + * <p/> + * - Message type. + */ + public static final int MESSAGE_HEADER_SIZE = 16; + /** + * 20 bytes (5 ints) for the query header: + * <p/> + * - Common packet header. + * <p/> + * - Query ID. + * <p/> + * - Query type. + */ + public static final int QUERY_HEADER_SIZE = 20; + /** + * 16 bytes (4 ints) for the query response: + * <p/> + * - Common packet header. + * <p/> + * - Query ID. + */ + public static final int QUERY_RESP_HEADER_SIZE = 16; + + /* + * Types of packets transferred via SDK Controller channel. + */ + + /** Packet is a message. */ + public static final int PACKET_TYPE_MESSAGE = 1; + /** Packet is a query. */ + public static final int PACKET_TYPE_QUERY = 2; + /** Packet is a response to a query. */ + public static final int PACKET_TYPE_QUERY_RESPONSE = 3; + + /* + * Constants related to handshake protocol between the emulator and a channel. + */ + + /** + * Query type for a special "handshake" query. + * <p/> + * When emulator connects to SDK controller, the first thing that goes + * through the socket is a special "handshake" query that delivers channel name + * to the service. + */ + public static final int QUERY_HANDSHAKE = -1; + /** + * Handshake query response on condition that service-side channel is available + * (registered). + */ + public static final int HANDSHAKE_RESP_CONNECTED = 0; + /** + * Handshake query response on condition that service-side channel is not + * available (not registered). + */ + public static final int HANDSHAKE_RESP_NOPORT = 1; + /** + * Handshake query response on condition that there is already an existing + * emulator connection for this channel. Emulator should stop connection + * attempts in this case. + */ + public static final int HANDSHAKE_RESP_DUP = -1; + /** Response to an unknown handshake query type. */ + public static final int HANDSHAKE_RESP_QUERY_UNKNOWN = -2; + + /* + * Constants related to multi-touch emulation. + */ + + /** Received frame is JPEG image. */ + public static final int MT_FRAME_JPEG = 1; + /** Received frame is RGB565 bitmap. */ + public static final int MT_FRAME_RGB565 = 2; + /** Received frame is RGB888 bitmap. */ + public static final int MT_FRAME_RGB888 = 3; + + /** Pointer(s) moved. */ + public static final int MT_MOVE = 1; + /** First pointer down message. */ + public static final int MT_FISRT_DOWN = 2; + /** Last pointer up message. */ + public static final int MT_LAST_UP = 3; + /** Pointer down message. */ + public static final int MT_POINTER_DOWN = 4; + /** Pointer up message. */ + public static final int MT_POINTER_UP = 5; + /** Sends framebuffer update. */ + public static final int MT_FB_UPDATE = 6; + /** Frame buffer update has been received. */ + public static final int MT_FB_ACK = 7; + /** Frame buffer update has been handled. */ + public static final int MT_FB_HANDLED = 8; + /** Size of an event entry in the touch event message to the emulator. */ + public static final int MT_EVENT_ENTRY_SIZE = 16; + + /* + * Constants related to sensor emulation. + */ + + /** Query type for a query that should return the list of available sensors. */ + public static final int SENSORS_QUERY_LIST = 1; + /** Message that starts sensor emulation. */ + public static final int SENSORS_START = 1; + /** Message that stops sensor emulation. */ + public static final int SENSORS_STOP = 2; + /** Message that enables emulation of a particular sensor. */ + public static final int SENSORS_ENABLE = 3; + /** Message that disables emulation of a particular sensor. */ + public static final int SENSORS_DISABLE = 4; + /** Message that delivers sensor events to emulator. */ + public static final int SENSORS_SENSOR_EVENT = 5; +} diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java new file mode 100644 index 000000000..08e6b2813 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2012 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 android.net.LocalSocket; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.nio.channels.ClosedChannelException; + +/** + * Encapsulates a connection with the emulator over a UNIX-domain socket. + */ +public class Socket { + /** UNIX-domain socket connected with the emulator. */ + private LocalSocket mSocket = null; + /** Channel name for the connection established via this socket. */ + private String mChannelName; + /** Endianness of data transferred in this connection. */ + private ByteOrder mEndian; + + /** Tag for message logging. */ + private static final String TAG = "SdkControllerSocket"; + /** Controls debug log. */ + private static boolean DEBUG = false; + + /** + * Constructs Socket instance. + * + * @param socket Socket connection with the emulator. + * @param name Channel port name for this connection. + * @param endian Endianness of data transferred in this connection. + */ + public Socket(LocalSocket socket, String name, ByteOrder endian) { + mSocket = socket; + mChannelName = name; + mEndian = endian; + if (DEBUG) Log.d(TAG, "Socket is constructed for " + mChannelName); + } + + /** + * Gets connection status of this socket. + * + * @return true if socket is connected, or false if socket is not connected. + */ + public boolean isConnected() { + return mSocket != null; + } + + /** + * Gets channel name for this socket. + * + * @return Channel name for this socket. + */ + public String getChannelName() { + return mChannelName; + } + + /** + * Gets endianness of data transferred via this socket. + * + * @return Endianness of data transferred via this socket. + */ + public ByteOrder getEndian() { + return mEndian; + } + + /** + * Sends data to the socket. + * + * @param data Data to send. Data size is defined by the length of the + * array. + * @throws IOException + */ + public void send(byte[] data) throws IOException { + // Use local copy of the socket, ensuring it's not going to NULL while + // we're working with it. If it gets closed, while we're in the middle + // of data transfer - it's OK, since it will produce an exception, and + // the caller will gracefully handle it. + // + // Same technique is used everywhere in this class where mSocket member + // is touched. + LocalSocket socket = mSocket; + if (socket == null) { + Logw("'send' request on closed Socket " + mChannelName); + throw new ClosedChannelException(); + } + socket.getOutputStream().write(data); + } + + /** + * Sends data to the socket. + * + * @param data Data to send. + * @param offset The start position in data from where to get bytes. + * @param len The number of bytes from data to write to this socket. + * @throws IOException + */ + public void send(byte[] data, int offset, int len) throws IOException { + LocalSocket socket = mSocket; + if (socket == null) { + Logw("'send' request on closed Socket " + mChannelName); + throw new ClosedChannelException(); + } + socket.getOutputStream().write(data, offset, len); + } + + /** + * Receives data from the socket. + * + * @param socket Socket from where to receive data. + * @param data Array where to save received data. + * @param len Number of bytes to receive. + * @throws IOException + */ + public static void receive(LocalSocket socket, byte[] data, int len) throws IOException { + final InputStream is = socket.getInputStream(); + int received = 0; + while (received != len) { + final int chunk = is.read(data, received, len - received); + if (chunk < 0) { + throw new IOException( + "I/O failure while receiving SDK controller data from socket."); + } + received += chunk; + } + } + + /** + * Receives data from the socket. + * + * @param data Array where to save received data. + * @param len Number of bytes to receive. + * @throws IOException + */ + public void receive(byte[] data, int len) throws IOException { + LocalSocket socket = mSocket; + if (socket == null) { + Logw("'receive' request on closed Socket " + mChannelName); + throw new ClosedChannelException(); + } + receive(socket, data, len); + } + + /** + * Receives data from the socket. + * + * @param data Array where to save received data. Data size is defined by + * the size of the array. + * @throws IOException + */ + public void receive(byte[] data) throws IOException { + receive(data, data.length); + } + + /** + * Closes the socket. + * + * @return true if socket has been closed in this call, or false if it had + * been already closed when this method has been called. + */ + public boolean close() { + // This is the only place in this class where we will null the socket + // object. Since this method can be called concurrently from different + // threads, lets do this under the lock. + LocalSocket socket; + synchronized (this) { + socket = mSocket; + mSocket = null; + } + if (socket != null) { + try { + // Force all I/O to stop before closing the socket. + socket.shutdownInput(); + socket.shutdownOutput(); + socket.close(); + if (DEBUG) Log.d(TAG, "Socket is closed for " + mChannelName); + return true; + } catch (IOException e) { + Loge("Exception " + e + " while closing Socket for " + mChannelName); + } + } + return false; + } + + /*************************************************************************** + * Logging wrappers + **************************************************************************/ + + private void Loge(String log) { + Log.e(TAG, log); + } + + private void Logw(String log) { + Log.w(TAG, log); + } +} |