/* * 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: *

* 'adb forward tcp: localabstract:android.sdk.controller' *

* The way communication between the emulator and SDK controller service works * is as follows: *

* 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. *

* 2. Channels are connected to emulator via separate socket instance (though all * of the connections share the same socket address). *

* 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. *

* Given that, the main responsibilities of this class are: *

* 1. Bind to "android.sdk.controller" socket, listening to emulator connections. *

* 2. Maintain a list of service-side channels registered by the application. *

* 3. Bind emulator connection with service-side channel via port name, provided by * the emulator. *

* 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. *

* 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 mPendingSockets = new ArrayList(); /** * List of registered app-side channels. *

* 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 mChannels = new ArrayList(); /** * 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); } }