/* * 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); } }