auto import from //branches/cupcake_rel/...@140373
diff --git a/emulator/qemud/qemud.c b/emulator/qemud/qemud.c
index ae6797e..bd49e52 100644
--- a/emulator/qemud/qemud.c
+++ b/emulator/qemud/qemud.c
@@ -10,96 +10,79 @@
 #include <cutils/sockets.h>
 
 /*
- *  the qemud program is only used within the Android emulator as a bridge
+ *  the qemud daemon program is only used within Android as a bridge
  *  between the emulator program and the emulated system. it really works as
  *  a simple stream multiplexer that works as follows:
  *
+ *    - qemud is started by init following instructions in
+ *      /system/etc/init.goldfish.rc (i.e. it is never started on real devices)
+ *
  *    - qemud communicates with the emulator program through a single serial
  *      port, whose name is passed through a kernel boot parameter
  *      (e.g. android.qemud=ttyS1)
  *
- *    - qemud setups one or more unix local stream sockets in the
- *      emulated system each one of these represent a different communication
- *      'channel' between the emulator program and the emulated system.
+ *    - qemud binds one unix local stream socket (/dev/socket/qemud, created
+ *      by init through /system/etc/init.goldfish.rc).
  *
- *      as an example, one channel is used for the emulated GSM modem
- *      (AT command channel), another channel is used for the emulated GPS,
- *      etc...
  *
- *    - the protocol used on the serial connection is pretty simple:
+ *      emulator <==serial==> qemud <---> /dev/socket/qemud <-+--> client1
+ *                                                            |
+ *                                                            +--> client2
  *
- *          offset    size    description
- *              0       4     4-char hex string giving the payload size
- *              4       2     2-char hex string giving the destination or
- *                            source channel
- *              6       n     the message payload
+ *   - the special channel index 0 is used by the emulator and qemud only.
+ *     other channel numbers correspond to clients. More specifically,
+ *     connection are created like this:
  *
- *      for emulator->system messages, the 'channel' index indicates
- *      to which channel the payload must be sent
+ *     * the client connects to /dev/socket/qemud
  *
- *      for system->emulator messages, the 'channel' index indicates from
- *      which channel the payload comes from.
+ *     * the client sends the service name through the socket, as
+ *            <service-name>
  *
- *   - a special channel index (0) is used to communicate with the qemud
- *     program directly from the emulator. this is used for the following
- *     commands:  (content of the payload):
+ *     * qemud creates a "Client" object internally, assigns it an
+ *       internal unique channel number > 0, then sends a connection
+ *       initiation request to the emulator (i.e. through channel 0):
  *
- *        request:  connect:<name>
- *        answer:   ok:connect:<name>:XX       // succesful name lookup
- *        answer:   ko:connect:bad name        // failed lookup
+ *           connect:<hxid>:<name>
  *
- *           the emulator queries the index of a given channel given
- *           its human-readable name. the answer contains a 2-char hex
- *           string for the channel index.
+ *       where <name> is the service name, and <hxid> is a 4-hexchar
+ *       number corresponding to the channel number.
  *
- *           not all emulated systems may need the same communication
- *           channels, so this function may fail.
+ *     * in case of success, the emulator responds through channel 0
+ *       with:
  *
- *     any invalid request will get an answer of:
+ *           ok:connect:<hxid>
+ *
+ *       after this, all messages between the client and the emulator
+ *       are passed in pass-through mode.
+ *
+ *     * if the emulator refuses the service connection, it will
+ *       send the following through channel 0:
+ *
+ *           ko:connect:<hxid>:reason-for-failure
+ *
+ *     * If the client closes the connection, qemud sends the following
+ *       to the emulator:
+ *
+ *           disconnect:<hxid>
+ *
+ *       The same message is the opposite direction if the emulator
+ *       chooses to close the connection.
+ *
+ *     * any command sent through channel 0 to the emulator that is
+ *       not properly recognized will be answered by:
  *
  *           ko:unknown command
  *
  *
- *  here's a diagram of how things work:
- *
- *
- *                                                  _________
- *                        _____________   creates  |         |
- *         ________      |             |==========>| Channel |--*--
- *        |        |---->| Multiplexer |           |_________|
- *   --*--| Serial |     |_____________|               || creates
- *        |________|            |                 _____v___
- *             A                +--------------->|         |
- *             |                                 | Client  |--*--
- *             +---------------------------------|_________|
- *
- *  which really means that:
- *
- *    - the multiplexer creates one Channel object per control socket qemud
- *      handles (e.g. /dev/socket/qemud_gsm, /dev/socket/qemud_gps)
- *
- *    - each Channel object has a numerical index that is >= 1, and waits
- *      for client connection. it will create a Client object when this
- *      happens
- *
- *    - the Serial object receives packets from the serial port and sends them
- *      to the multiplexer
- *
- *    - the multiplexer tries to find a channel the packet is addressed to,
- *      and will send the packet to all clients that correspond to it
- *
- *    - when a Client receives data, it sends it directly to the Serial object
- *
- *    - there are two kinds of Channel objects:
- *
- *         CHANNEL_BROADCAST :: used for emulator -> clients broadcasts only
- *
- *         CHANNEL_DUPLEX    :: used for bidirectional communication with the
- *                              emulator, with only *one* client allowed per
- *                              duplex channel
+ *  Internally, the daemon maintains a "Client" object for each client
+ *  connection (i.e. accepting socket connection).
  */
 
-#define  DEBUG  0
+/* name of the single control socket used by the daemon */
+#define CONTROL_SOCKET_NAME  "qemud"
+
+#define  DEBUG     1
+#define  T_ACTIVE  0  /* set to 1 to dump traffic */
 
 #if DEBUG
 #  define LOG_TAG  "qemud"
@@ -107,6 +90,13 @@
 #  define  D(...)   LOGD(__VA_ARGS__)
 #else
 #  define  D(...)  ((void)0)
+#  define  T(...)  ((void)0)
+#endif
+
+#if T_ACTIVE
+#  define  T(...)   D(__VA_ARGS__)
+#else
+#  define  T(...)   ((void)0)
 #endif
 
 /** UTILITIES
@@ -262,30 +252,84 @@
     }
 }
 
+
+static int
+fd_accept(int  fd)
+{
+    struct sockaddr  from;
+    socklen_t        fromlen = sizeof(from);
+    int              ret;
+
+    do {
+        ret = accept(fd, &from, &fromlen);
+    } while (ret < 0 && errno == EINTR);
+
+    return ret;
+}
+
 /** FD EVENT LOOP
  **/
 
+/* A Looper object is used to monitor activity on one or more
+ * file descriptors (e.g sockets).
+ *
+ * - call looper_add() to register a function that will be
+ *   called when events happen on the file descriptor.
+ *
+ * - call looper_enable() or looper_disable() to enable/disable
+ *   the set of monitored events for a given file descriptor.
+ *
+ * - call looper_del() to unregister a file descriptor.
+ *   this does *not* close the file descriptor.
+ *
+ * Note that you can only provide a single function to handle
+ * all events related to a given file descriptor.
+
+ * You can call looper_enable/_disable/_del within a function
+ * callback.
+ */
+
+/* the current implementation uses Linux's epoll facility
+ * the event mask we use are simply combinations of EPOLLIN
+ * EPOLLOUT, EPOLLHUP and EPOLLERR
+ */
 #include <sys/epoll.h>
 
 #define  MAX_CHANNELS  16
 #define  MAX_EVENTS    (MAX_CHANNELS+1)  /* each channel + the serial fd */
 
+/* the event handler function type, 'user' is a user-specific
+ * opaque pointer passed to looper_add().
+ */
 typedef void (*EventFunc)( void*  user, int  events );
 
+/* bit flags for the LoopHook structure.
+ *
+ * HOOK_PENDING means that an event happened on the
+ * corresponding file descriptor.
+ *
+ * HOOK_CLOSING is used to delay-close monitored
+ * file descriptors.
+ */
 enum {
     HOOK_PENDING = (1 << 0),
     HOOK_CLOSING = (1 << 1),
 };
 
+/* A LoopHook structure is used to monitor a given
+ * file descriptor and record its event handler.
+ */
 typedef struct {
     int        fd;
-    int        wanted;
-    int        events;
-    int        state;
-    void*      ev_user;
-    EventFunc  ev_func;
+    int        wanted;  /* events we are monitoring */
+    int        events;  /* events that occured */
+    int        state;   /* see HOOK_XXX constants */
+    void*      ev_user; /* user-provided handler parameter */
+    EventFunc  ev_func; /* event handler callback */
 } LoopHook;
 
+/* Looper is the main object modeling a looper object
+ */
 typedef struct {
     int                  epoll_fd;
     int                  num_fds;
@@ -294,6 +338,7 @@
     LoopHook*            hooks;
 } Looper;
 
+/* initialize a looper object */
 static void
 looper_init( Looper*  l )
 {
@@ -304,6 +349,7 @@
     l->hooks    = NULL;
 }
 
+/* finalize a looper object */
 static void
 looper_done( Looper*  l )
 {
@@ -316,6 +362,9 @@
     l->epoll_fd  = -1;
 }
 
+/* return the LoopHook corresponding to a given
+ * monitored file descriptor, or NULL if not found
+ */
 static LoopHook*
 looper_find( Looper*  l, int  fd )
 {
@@ -329,6 +378,7 @@
     return NULL;
 }
 
+/* grow the arrays in the looper object */
 static void
 looper_grow( Looper*  l )
 {
@@ -351,6 +401,9 @@
     }
 }
 
+/* register a file descriptor and its event handler.
+ * no event mask will be enabled
+ */
 static void
 looper_add( Looper*  l, int  fd, EventFunc  func, void*  user )
 {
@@ -378,6 +431,8 @@
     l->num_fds += 1;
 }
 
+/* unregister a file descriptor and its event handler
+ */
 static void
 looper_del( Looper*  l, int  fd )
 {
@@ -393,6 +448,10 @@
     epoll_ctl( l->epoll_fd, EPOLL_CTL_DEL, fd, NULL );
 }
 
+/* enable monitoring of certain events for a file
+ * descriptor. This adds 'events' to the current
+ * event mask
+ */
 static void
 looper_enable( Looper*  l, int  fd, int  events )
 {
@@ -414,6 +473,10 @@
     }
 }
 
+/* disable monitoring of certain events for a file
+ * descriptor. This ignores events that are not
+ * currently enabled.
+ */
 static void
 looper_disable( Looper*  l, int  fd, int  events )
 {
@@ -435,6 +498,9 @@
     }
 }
 
+/* wait until an event occurs on one of the registered file
+ * descriptors. Only returns in case of error !!
+ */
 static void
 looper_loop( Looper*  l )
 {
@@ -450,6 +516,11 @@
             return;
         }
 
+        if (count == 0) {
+            D("%s: huh ? epoll returned count=0", __FUNCTION__);
+            continue;
+        }
+
         /* mark all pending hooks */
         for (n = 0; n < count; n++) {
             LoopHook*  hook = l->events[n].data.ptr;
@@ -483,13 +554,87 @@
     }
 }
 
+#if T_ACTIVE
+char*
+quote( const void*  data, int  len )
+{
+    const char*  p   = data;
+    const char*  end = p + len;
+    int          count = 0;
+    int          phase = 0;
+    static char*  buff = NULL;
+
+    for (phase = 0; phase < 2; phase++) {
+        if (phase != 0) {
+            xfree(buff);
+            buff = xalloc(count+1);
+        }
+        count = 0;
+        for (p = data; p < end; p++) {
+            int  c = *p;
+
+            if (c == '\\') {
+                if (phase != 0) {
+                    buff[count] = buff[count+1] = '\\';
+                }
+                count += 2;
+                continue;
+            }
+
+            if (c >= 32 && c < 127) {
+                if (phase != 0)
+                    buff[count] = c;
+                count += 1;
+                continue;
+            }
+
+
+            if (c == '\t') {
+                if (phase != 0) {
+                    memcpy(buff+count, "<TAB>", 5);
+                }
+                count += 5;
+                continue;
+            }
+            if (c == '\n') {
+                if (phase != 0) {
+                    memcpy(buff+count, "<LN>", 4);
+                }
+                count += 4;
+                continue;
+            }
+            if (c == '\r') {
+                if (phase != 0) {
+                    memcpy(buff+count, "<CR>", 4);
+                }
+                count += 4;
+                continue;
+            }
+
+            if (phase != 0) {
+                buff[count+0] = '\\';
+                buff[count+1] = 'x';
+                buff[count+2] = "0123456789abcdef"[(c >> 4) & 15];
+                buff[count+3] = "0123456789abcdef"[     (c) & 15];
+            }
+            count += 4;
+        }
+    }
+    buff[count] = 0;
+    return buff;
+}
+#endif /* T_ACTIVE */
+
 /** PACKETS
+ **
+ ** We need a way to buffer data before it can be sent to the
+ ** corresponding file descriptor. We use linked list of Packet
+ ** objects to do this.
  **/
 
 typedef struct Packet   Packet;
 
-/* we want to ensure that Packet is no more than a single page */
-#define  MAX_PAYLOAD  (4096-16-6)
+#define  MAX_PAYLOAD  4000
 
 struct Packet {
     Packet*   next;
@@ -498,8 +643,13 @@
     uint8_t   data[ MAX_PAYLOAD ];
 };
 
+/* we expect to alloc/free a lot of packets during
+ * operations so use a single linked list of free packets
+ * to keep things speedy and simple.
+ */
 static Packet*   _free_packets;
 
+/* Allocate a packet */
 static Packet*
 packet_alloc(void)
 {
@@ -509,12 +659,16 @@
     } else {
         xnew(p);
     }
-    p->next = NULL;
-    p->len  = 0;
+    p->next    = NULL;
+    p->len     = 0;
     p->channel = -1;
     return p;
 }
 
+/* Release a packet. This takes the address of a packet
+ * pointer that will be set to NULL on exit (avoids
+ * referencing dangling pointers in case of bugs)
+ */
 static void
 packet_free( Packet*  *ppacket )
 {
@@ -526,18 +680,15 @@
     }
 }
 
-static Packet*
-packet_dup( Packet*  p )
-{
-    Packet*  p2 = packet_alloc();
-
-    p2->len     = p->len;
-    p2->channel = p->channel;
-    memcpy(p2->data, p->data, p->len);
-    return p2;
-}
-
 /** PACKET RECEIVER
+ **
+ ** Simple abstraction for something that can receive a packet
+ ** from a FDHandler (see below) or something else.
+ **
+ ** Send a packet to it with 'receiver_post'
+ **
+ ** Call 'receiver_close' to indicate that the corresponding
+ ** packet source was closed.
  **/
 
 typedef void (*PostFunc) ( void*  user, Packet*  p );
@@ -549,41 +700,130 @@
     void*      user;
 } Receiver;
 
+/* post a packet to a receiver. Note that this transfers
+ * ownership of the packet to the receiver.
+ */
 static __inline__ void
 receiver_post( Receiver*  r, Packet*  p )
 {
-    r->post( r->user, p );
+    if (r->post)
+        r->post( r->user, p );
+    else
+        packet_free(&p);
 }
 
+/* tell a receiver the packet source was closed.
+ * this will also prevent further posting to the
+ * receiver.
+ */
 static __inline__ void
 receiver_close( Receiver*  r )
 {
-    r->close( r->user );
+    if (r->close) {
+        r->close( r->user );
+        r->close = NULL;
+    }
+    r->post  = NULL;
 }
 
 
 /** FD HANDLERS
  **
  ** these are smart listeners that send incoming packets to a receiver
- ** and can queue one or more outgoing packets and send them when possible
+ ** and can queue one or more outgoing packets and send them when
+ ** possible to the FD.
+ **
+ ** note that we support clean shutdown of file descriptors,
+ ** i.e. we try to send all outgoing packets before destroying
+ ** the FDHandler.
  **/
 
-typedef struct FDHandler {
-    int          fd;
+typedef struct FDHandler      FDHandler;
+typedef struct FDHandlerList  FDHandlerList;
+
+struct FDHandler {
+    int             fd;
+    FDHandlerList*  list;
+    char            closing;
+    Receiver        receiver[1];
+
+    /* queue of outgoing packets */
+    int             out_pos;
+    Packet*         out_first;
+    Packet**        out_ptail;
+
+    FDHandler*      next;
+    FDHandler**     pref;
+
+};
+
+struct FDHandlerList {
+    /* the looper that manages the fds */
     Looper*      looper;
-    Receiver     receiver[1];
-    int          out_pos;
-    Packet*      out_first;
-    Packet**     out_ptail;
 
-} FDHandler;
+    /* list of active FDHandler objects */
+    FDHandler*   active;
 
+    /* list of closing FDHandler objects.
+     * these are waiting to push their
+     * queued packets to the fd before
+     * freeing themselves.
+     */
+    FDHandler*   closing;
 
+};
+
+/* remove a FDHandler from its current list */
 static void
-fdhandler_done( FDHandler*  f )
+fdhandler_remove( FDHandler*  f )
 {
-    /* get rid of unsent packets */
-    if (f->out_first) {
+    f->pref[0] = f->next;
+    if (f->next)
+        f->next->pref = f->pref;
+}
+
+/* add a FDHandler to a given list */
+static void
+fdhandler_prepend( FDHandler*  f, FDHandler**  list )
+{
+    f->next = list[0];
+    f->pref = list;
+    list[0] = f;
+    if (f->next)
+        f->next->pref = &f->next;
+}
+
+/* initialize a FDHandler list */
+static void
+fdhandler_list_init( FDHandlerList*  list, Looper*  looper )
+{
+    list->looper  = looper;
+    list->active  = NULL;
+    list->closing = NULL;
+}
+
+
+/* close a FDHandler (and free it). Note that this will not
+ * perform a graceful shutdown, i.e. all packets in the
+ * outgoing queue will be immediately free.
+ *
+ * this *will* notify the receiver that the file descriptor
+ * was closed.
+ *
+ * you should call fdhandler_shutdown() if you want to
+ * notify the FDHandler that its packet source is closed.
+ */
+static void
+fdhandler_close( FDHandler*  f )
+{
+    /* notify receiver */
+    receiver_close(f->receiver);
+
+    /* remove the handler from its list */
+    fdhandler_remove(f);
+
+    /* get rid of outgoing packet queue */
+    if (f->out_first != NULL) {
         Packet*  p;
         while ((p = f->out_first) != NULL) {
             f->out_first = p->next;
@@ -593,14 +833,41 @@
 
     /* get rid of file descriptor */
     if (f->fd >= 0) {
-        looper_del( f->looper, f->fd );
+        looper_del( f->list->looper, f->fd );
         close(f->fd);
         f->fd = -1;
     }
-    f->looper = NULL;
+
+    f->list = NULL;
+    xfree(f);
 }
 
+/* Ask the FDHandler to cleanly shutdown the connection,
+ * i.e. send any pending outgoing packets then auto-free
+ * itself.
+ */
+static void
+fdhandler_shutdown( FDHandler*  f )
+{
 
+    if (f->out_first != NULL && !f->closing)
+    {
+        /* move the handler to the 'closing' list */
+        f->closing = 1;
+        fdhandler_remove(f);
+        fdhandler_prepend(f, &f->list->closing);
+
+        /* notify the receiver that we're closing */
+        receiver_close(f->receiver);
+        return;
+    }
+
+    fdhandler_close(f);
+}
+
+/* Enqueue a new packet that the FDHandler will
+ * send through its file descriptor.
+ */
 static void
 fdhandler_enqueue( FDHandler*  f, Packet*  p )
 {
@@ -612,16 +879,24 @@
 
     if (first == NULL) {
         f->out_pos = 0;
-        looper_enable( f->looper, f->fd, EPOLLOUT );
+        looper_enable( f->list->looper, f->fd, EPOLLOUT );
     }
 }
 
 
+/* FDHandler file descriptor event callback for read/write ops */
 static void
 fdhandler_event( FDHandler*  f, int  events )
 {
    int  len;
 
+    /* in certain cases, it's possible to have both EPOLLIN and
+     * EPOLLHUP at the same time. This indicates that there is incoming
+     * data to read, but that the connection was nonetheless closed
+     * by the sender. Be sure to read the data before closing
+     * the receiver to avoid packet loss.
+     */
+
     if (events & EPOLLIN) {
         Packet*  p = packet_alloc();
         int      len;
@@ -629,23 +904,17 @@
         if ((len = fd_read(f->fd, p->data, MAX_PAYLOAD)) < 0) {
             D("%s: can't recv: %s", __FUNCTION__, strerror(errno));
             packet_free(&p);
-        } else {
+        } else if (len > 0) {
             p->len     = len;
-            p->channel = -101;  /* special debug value */
+            p->channel = -101;  /* special debug value, not used */
             receiver_post( f->receiver, p );
         }
     }
 
-    /* in certain cases, it's possible to have both EPOLLIN and
-     * EPOLLHUP at the same time. This indicates that there is incoming
-     * data to read, but that the connection was nonetheless closed
-     * by the sender. Be sure to read the data before closing
-     * the receiver to avoid packet loss.
-     */
     if (events & (EPOLLHUP|EPOLLERR)) {
         /* disconnection */
         D("%s: disconnect on fd %d", __FUNCTION__, f->fd);
-        receiver_close( f->receiver );
+        fdhandler_close(f);
         return;
     }
 
@@ -664,7 +933,7 @@
                 packet_free(&p);
                 if (f->out_first == NULL) {
                     f->out_ptail = &f->out_first;
-                    looper_disable( f->looper, f->fd, EPOLLOUT );
+                    looper_disable( f->list->looper, f->fd, EPOLLOUT );
                 }
             }
         }
@@ -672,24 +941,34 @@
 }
 
 
-static void
-fdhandler_init( FDHandler*      f,
-                int             fd,
-                Looper*         looper,
-                Receiver*       receiver )
+/* Create a new FDHandler that monitors read/writes */
+static FDHandler*
+fdhandler_new( int             fd,
+               FDHandlerList*  list,
+               Receiver*       receiver )
 {
+    FDHandler*  f = xalloc0(sizeof(*f));
+
     f->fd          = fd;
-    f->looper      = looper;
+    f->list        = list;
     f->receiver[0] = receiver[0];
     f->out_first   = NULL;
     f->out_ptail   = &f->out_first;
     f->out_pos     = 0;
 
-    looper_add( looper, fd, (EventFunc) fdhandler_event, f );
-    looper_enable( looper, fd, EPOLLIN );
+    fdhandler_prepend(f, &list->active);
+
+    looper_add( list->looper, fd, (EventFunc) fdhandler_event, f );
+    looper_enable( list->looper, fd, EPOLLIN );
+
+    return f;
 }
 
 
+/* event callback function to monitor accepts() on server sockets.
+ * the convention used here is that the receiver will receive a
+ * dummy packet with the new client socket in p->channel
+ */
 static void
 fdhandler_accept_event( FDHandler*  f, int  events )
 {
@@ -700,296 +979,116 @@
         D("%s: accepting on fd %d", __FUNCTION__, f->fd);
         p->data[0] = 1;
         p->len     = 1;
+        p->channel = fd_accept(f->fd);
+        if (p->channel < 0) {
+            D("%s: accept failed ?: %s", __FUNCTION__, strerror(errno));
+            packet_free(&p);
+            return;
+        }
         receiver_post( f->receiver, p );
     }
 
     if (events & (EPOLLHUP|EPOLLERR)) {
         /* disconnecting !! */
-        D("%s: closing fd %d", __FUNCTION__, f->fd);
-        receiver_close( f->receiver );
+        D("%s: closing accept fd %d", __FUNCTION__, f->fd);
+        fdhandler_close(f);
         return;
     }
 }
 
 
-static void
-fdhandler_init_accept( FDHandler*  f,
-                       int         fd,
-                       Looper*     looper,
-                       Receiver*   receiver )
+/* Create a new FDHandler used to monitor new connections on a
+ * server socket. The receiver must expect the new connection
+ * fd in the 'channel' field of a dummy packet.
+ */
+static FDHandler*
+fdhandler_new_accept( int             fd,
+                      FDHandlerList*  list,
+                      Receiver*       receiver )
 {
+    FDHandler*  f = xalloc0(sizeof(*f));
+
     f->fd          = fd;
-    f->looper      = looper;
+    f->list        = list;
     f->receiver[0] = receiver[0];
 
-    looper_add( looper, fd, (EventFunc) fdhandler_accept_event, f );
-    looper_enable( looper, fd, EPOLLIN );
-}
+    fdhandler_prepend(f, &list->active);
 
-/** CLIENTS
- **/
-
-typedef struct Client {
-    struct Client*   next;
-    struct Client**  pref;
-    int              channel;
-    FDHandler        fdhandler[1];
-    Receiver         receiver[1];
-} Client;
-
-static Client*   _free_clients;
-
-static void
-client_free( Client*  c )
-{
-    c->pref[0] = c->next;
-    c->next    = NULL;
-    c->pref    = &c->next;
-
-    fdhandler_done( c->fdhandler );
-    free(c);
-}
-
-static void
-client_receive( Client*  c, Packet*  p )
-{
-    p->channel = c->channel;
-    receiver_post( c->receiver, p );
-}
-
-static void
-client_send( Client*  c, Packet*  p )
-{
-    fdhandler_enqueue( c->fdhandler, p );
-}
-
-static void
-client_close( Client*  c )
-{
-    D("disconnecting client on fd %d", c->fdhandler->fd);
-    client_free(c);
-}
-
-static Client*
-client_new( int         fd,
-            int         channel,
-            Looper*     looper,
-            Receiver*   receiver )
-{
-    Client*   c;
-    Receiver  recv;
-
-    xnew(c);
-
-    c->next = NULL;
-    c->pref = &c->next;
-    c->channel = channel;
-    c->receiver[0] = receiver[0];
-
-    recv.user  = c;
-    recv.post  = (PostFunc)  client_receive;
-    recv.close = (CloseFunc) client_close;
-
-    fdhandler_init( c->fdhandler, fd, looper, &recv );
-    return c;
-}
-
-static void
-client_link( Client*  c, Client**  plist )
-{
-    c->next  = plist[0];
-    c->pref  = plist;
-    plist[0] = c;
-}
-
-
-/** CHANNELS
- **/
-
-typedef enum {
-    CHANNEL_BROADCAST = 0,
-    CHANNEL_DUPLEX,
-
-    CHANNEL_MAX  /* do not remove */
-
-} ChannelType;
-
-#define  CHANNEL_CONTROL   0
-
-typedef struct Channel {
-    struct Channel*     next;
-    struct Channel**    pref;
-    FDHandler           fdhandler[1];
-    ChannelType         ctype;
-    const char*         name;
-    int                 index;
-    Receiver            receiver[1];
-    Client*             clients;
-} Channel;
-
-static void
-channel_free( Channel*  c )
-{
-    while (c->clients)
-        client_free(c->clients);
-
-    c->pref[0] = c->next;
-    c->pref    = &c->next;
-    c->next    = NULL;
-
-    fdhandler_done( c->fdhandler );
-    free(c);
-}
-
-static void
-channel_close( Channel*  c )
-{
-    D("closing channel '%s' on fd %d", c->name, c->fdhandler->fd);
-    channel_free(c);
-}
-
-
-static void
-channel_accept( Channel*  c, Packet*  p )
-{
-    int   fd;
-    struct sockaddr  from;
-    socklen_t        fromlen = sizeof(from);
-
-    /* get rid of dummy packet (see fdhandler_event_accept) */
-    packet_free(&p);
-
-    do {
-        fd = accept( c->fdhandler->fd, &from, &fromlen );
-    } while (fd < 0 && errno == EINTR);
-
-    if (fd >= 0) {
-        Client*  client;
-
-        /* DUPLEX channels can only have one client at a time */
-        if (c->ctype == CHANNEL_DUPLEX && c->clients != NULL) {
-            D("refusing client connection on duplex channel '%s'", c->name);
-            close(fd);
-            return;
-        }
-        client = client_new( fd, c->index, c->fdhandler->looper, c->receiver );
-        client_link( client, &c->clients );
-        D("new client for channel '%s' on fd %d", c->name, fd);
-    }
-    else
-        D("could not accept connection: %s", strerror(errno));
-}
-
-
-static Channel*
-channel_new( int          fd,
-             ChannelType  ctype,
-             const char*  name,
-             int          index,
-             Looper*      looper,
-             Receiver*    receiver )
-{
-    Channel*  c;
-    Receiver  recv;
-
-    xnew(c);
-
-    c->next  = NULL;
-    c->pref  = &c->next;
-    c->ctype = ctype;
-    c->name  = name;
-    c->index = index;
-
-    /* saved for future clients */
-    c->receiver[0] = receiver[0];
-
-    recv.user  = c;
-    recv.post  = (PostFunc)  channel_accept;
-    recv.close = (CloseFunc) channel_close;
-
-    fdhandler_init_accept( c->fdhandler, fd, looper, &recv );
+    looper_add( list->looper, fd, (EventFunc) fdhandler_accept_event, f );
+    looper_enable( list->looper, fd, EPOLLIN );
     listen( fd, 5 );
 
-    return c;
+    return f;
 }
 
-static void
-channel_link( Channel*  c, Channel** plist )
-{
-    c->next  = plist[0];
-    c->pref  = plist;
-    plist[0] = c;
-}
-
-static void
-channel_send( Channel*  c, Packet*  p )
-{
-    Client*  client = c->clients;
-    for ( ; client; client = client->next ) {
-        Packet*  q = packet_dup(p);
-        client_send( client, q );
-    }
-    packet_free( &p );
-}
-
+/** SERIAL CONNECTION STATE
+ **
+ ** The following is used to handle the framing protocol
+ ** used on the serial port connection.
+ **/
 
 /* each packet is made of a 6 byte header followed by a payload
  * the header looks like:
  *
  *   offset   size    description
- *       0       4    a 4-char hex string for the size of the payload
- *       4       2    a 2-byte hex string for the channel number
+ *       0       2    a 2-byte hex string for the channel number
+ *       4       4    a 4-char hex string for the size of the payload
  *       6       n    the payload itself
  */
 #define  HEADER_SIZE    6
-#define  LENGTH_OFFSET  0
-#define  LENGTH_SIZE    4
-#define  CHANNEL_OFFSET 4
+#define  CHANNEL_OFFSET 0
+#define  LENGTH_OFFSET  2
 #define  CHANNEL_SIZE   2
+#define  LENGTH_SIZE    4
 
-#define  CHANNEL_INDEX_NONE     0
-#define  CHANNEL_INDEX_CONTROL  1
+#define  CHANNEL_CONTROL  0
 
-#define  TOSTRING(x)   _TOSTRING(x)
-#define  _TOSTRING(x)  #x
-
-/** SERIAL HANDLER
- **/
-
+/* The Serial object receives data from the serial port,
+ * extracts the payload size and channel index, then sends
+ * the resulting messages as a packet to a generic receiver.
+ *
+ * You can also use serial_send to send a packet through
+ * the serial port.
+ */
 typedef struct Serial {
-    FDHandler   fdhandler[1];
-    Receiver    receiver[1];
-    int         in_len;
-    int         in_datalen;
-    int         in_channel;
-    Packet*     in_packet;
+    FDHandler*  fdhandler;   /* used to monitor serial port fd */
+    Receiver    receiver[1]; /* send payload there */
+    int         in_len;      /* current bytes in input packet */
+    int         in_datalen;  /* payload size, or 0 when reading header */
+    int         in_channel;  /* extracted channel number */
+    Packet*     in_packet;   /* used to read incoming packets */
 } Serial;
 
-static void
-serial_done( Serial*  s )
-{
-    packet_free(&s->in_packet);
-    s->in_len     = 0;
-    s->in_datalen = 0;
-    s->in_channel = 0;
-    fdhandler_done(s->fdhandler);
-}
 
+/* a callback called when the serial port's fd is closed */
 static void
-serial_close( Serial*  s )
+serial_fd_close( Serial*  s )
 {
     fatal("unexpected serial port close !!");
 }
 
-/* receive packets from the serial port */
 static void
-serial_receive( Serial*  s, Packet*  p )
+serial_dump( Packet*  p, const char*  funcname )
+{
+    T("%s: %03d bytes: '%s'",
+      funcname, p->len, quote(p->data, p->len));
+}
+
+/* a callback called when a packet arrives from the serial port's FDHandler.
+ *
+ * This will essentially parse the header, extract the channel number and
+ * the payload size and store them in 'in_datalen' and 'in_channel'.
+ *
+ * After that, the payload is sent to the receiver once completed.
+ */
+static void
+serial_fd_receive( Serial*  s, Packet*  p )
 {
     int      rpos  = 0, rcount = p->len;
     Packet*  inp   = s->in_packet;
     int      inpos = s->in_len;
 
-    //D("received from serial: %d bytes: '%.*s'", p->len, p->len, p->data);
+    serial_dump( p, __FUNCTION__ );
 
     while (rpos < rcount)
     {
@@ -1009,8 +1108,11 @@
                 s->in_datalen = hex2int( inp->data + LENGTH_OFFSET,  LENGTH_SIZE );
                 s->in_channel = hex2int( inp->data + CHANNEL_OFFSET, CHANNEL_SIZE );
 
-                if (s->in_datalen <= 0)
-                    D("ignoring empty packet from serial port");
+                if (s->in_datalen <= 0) {
+                    D("ignoring %s packet from serial port",
+                      s->in_datalen ? "empty" : "malformed");
+                    s->in_datalen = 0;
+                }
 
                 //D("received %d bytes packet for channel %d", s->in_datalen, s->in_channel);
                 inpos = 0;
@@ -1047,7 +1149,10 @@
 }
 
 
-/* send a packet to the serial port */
+/* send a packet to the serial port.
+ * this assumes that p->len and p->channel contain the payload's
+ * size and channel and will add the appropriate header.
+ */
 static void
 serial_send( Serial*  s, Packet*  p )
 {
@@ -1060,54 +1165,446 @@
     int2hex( p->len,     h->data + LENGTH_OFFSET,  LENGTH_SIZE );
     int2hex( p->channel, h->data + CHANNEL_OFFSET, CHANNEL_SIZE );
 
+    serial_dump( h, __FUNCTION__ );
+    serial_dump( p, __FUNCTION__ );
+
     fdhandler_enqueue( s->fdhandler, h );
     fdhandler_enqueue( s->fdhandler, p );
 }
 
 
+/* initialize serial reader */
 static void
-serial_init( Serial*    s,
-             int        fd,
-             Looper*    looper,
-             Receiver*  receiver )
+serial_init( Serial*         s,
+             int             fd,
+             FDHandlerList*  list,
+             Receiver*       receiver )
 {
     Receiver  recv;
 
     recv.user  = s;
-    recv.post  = (PostFunc)  serial_receive;
-    recv.close = (CloseFunc) serial_close;
+    recv.post  = (PostFunc)  serial_fd_receive;
+    recv.close = (CloseFunc) serial_fd_close;
 
     s->receiver[0] = receiver[0];
 
-    fdhandler_init( s->fdhandler, fd, looper, &recv );
+    s->fdhandler = fdhandler_new( fd, list, &recv );
     s->in_len     = 0;
     s->in_datalen = 0;
     s->in_channel = 0;
     s->in_packet  = packet_alloc();
 }
 
+
+/** CLIENTS
+ **/
+
+typedef struct Client       Client;
+typedef struct Multiplexer  Multiplexer;
+
+/* A Client object models a single qemud client socket
+ * connection in the emulated system.
+ *
+ * the client first sends the name of the system service
+ * it wants to contact (no framing), then waits for a 2
+ * byte answer from qemud.
+ *
+ * the answer is either "OK" or "KO" to indicate
+ * success or failure.
+ *
+ * In case of success, the client can send messages
+ * to the service.
+ *
+ * In case of failure, it can disconnect or try sending
+ * the name of another service.
+ */
+struct Client {
+    Client*       next;
+    Client**      pref;
+    int           channel;
+    char          registered;
+    FDHandler*    fdhandler;
+    Multiplexer*  multiplexer;
+};
+
+struct Multiplexer {
+    Client*        clients;
+    int            last_channel;
+    Serial         serial[1];
+    Looper         looper[1];
+    FDHandlerList  fdhandlers[1];
+};
+
+
+static int   multiplexer_open_channel( Multiplexer*  mult, Packet*  p );
+static void  multiplexer_close_channel( Multiplexer*  mult, int  channel );
+static void  multiplexer_serial_send( Multiplexer* mult, int  channel, Packet*  p );
+
+static void
+client_dump( Client*  c, Packet*  p, const char*  funcname )
+{
+    T("%s: client %p (%d): %3d bytes: '%s'",
+      funcname, c, c->fdhandler->fd,
+      p->len, quote(p->data, p->len));
+}
+
+/* destroy a client */
+static void
+client_free( Client*  c )
+{
+    /* remove from list */
+    c->pref[0] = c->next;
+    if (c->next)
+        c->next->pref = c->pref;
+
+    c->channel    = -1;
+    c->registered = 0;
+
+    /* gently ask the FDHandler to shutdown to
+     * avoid losing queued outgoing packets */
+    if (c->fdhandler != NULL) {
+        fdhandler_shutdown(c->fdhandler);
+        c->fdhandler = NULL;
+    }
+
+    xfree(c);
+}
+
+
+/* a function called when a client socket receives data */
+static void
+client_fd_receive( Client*  c, Packet*  p )
+{
+    client_dump(c, p, __FUNCTION__);
+
+    if (c->registered) {
+        /* the client is registered, just send the
+         * data through the serial port
+         */
+        multiplexer_serial_send(c->multiplexer, c->channel, p);
+        return;
+    }
+
+    if (c->channel > 0) {
+        /* the client is waiting registration results.
+         * this should not happen because the client
+         * should wait for our 'ok' or 'ko'.
+         * close the connection.
+         */
+         D("%s: bad client sending data before end of registration",
+           __FUNCTION__);
+     BAD_CLIENT:
+         packet_free(&p);
+         client_free(c);
+         return;
+    }
+
+    /* the client hasn't registered a service yet,
+     * so this must be the name of a service, call
+     * the multiplexer to start registration for
+     * it.
+     */
+    D("%s: attempting registration for service '%.*s'",
+      __FUNCTION__, p->len, p->data);
+    c->channel = multiplexer_open_channel(c->multiplexer, p);
+    if (c->channel < 0) {
+        D("%s: service name too long", __FUNCTION__);
+        goto BAD_CLIENT;
+    }
+    D("%s:    -> received channel id %d", __FUNCTION__, c->channel);
+    packet_free(&p);
+}
+
+
+/* a function called when the client socket is closed. */
+static void
+client_fd_close( Client*  c )
+{
+    T("%s: client %p (%d)", __FUNCTION__, c, c->fdhandler->fd);
+
+    /* no need to shutdown the FDHandler */
+    c->fdhandler = NULL;
+
+    /* tell the emulator we're out */
+    if (c->channel > 0)
+        multiplexer_close_channel(c->multiplexer, c->channel);
+
+    /* free the client */
+    client_free(c);
+}
+
+/* a function called when the multiplexer received a registration
+ * response from the emulator for a given client.
+ */
+static void
+client_registration( Client*  c, int  registered )
+{
+    Packet*  p = packet_alloc();
+
+    /* sends registration status to client */
+    if (!registered) {
+        D("%s: registration failed for client %d", __FUNCTION__, c->channel);
+        memcpy( p->data, "KO", 2 );
+        p->len = 2;
+    } else {
+        D("%s: registration succeeded for client %d", __FUNCTION__, c->channel);
+        memcpy( p->data, "OK", 2 );
+        p->len = 2;
+    }
+    client_dump(c, p, __FUNCTION__);
+    fdhandler_enqueue(c->fdhandler, p);
+
+    /* now save registration state
+     */
+    c->registered = registered;
+    if (!registered) {
+        /* allow the client to try registering another service */
+        c->channel = -1;
+    }
+}
+
+/* send data to a client */
+static void
+client_send( Client*  c, Packet*  p )
+{
+    client_dump(c, p, __FUNCTION__);
+    fdhandler_enqueue(c->fdhandler, p);
+}
+
+
+/* Create new client socket handler */
+static Client*
+client_new( Multiplexer*    mult,
+            int             fd,
+            FDHandlerList*  pfdhandlers,
+            Client**        pclients )
+{
+    Client*   c;
+    Receiver  recv;
+
+    xnew(c);
+
+    c->multiplexer = mult;
+    c->next        = NULL;
+    c->pref        = &c->next;
+    c->channel     = -1;
+    c->registered  = 0;
+
+    recv.user  = c;
+    recv.post  = (PostFunc)  client_fd_receive;
+    recv.close = (CloseFunc) client_fd_close;
+
+    c->fdhandler = fdhandler_new( fd, pfdhandlers, &recv );
+
+    /* add to client list */
+    c->next   = *pclients;
+    c->pref   = pclients;
+    *pclients = c;
+    if (c->next)
+        c->next->pref = &c->next;
+
+    return c;
+}
+
 /**  GLOBAL MULTIPLEXER
  **/
 
-typedef struct {
-    Looper     looper[1];
-    Serial     serial[1];
-    Channel*   channels;
-    uint16_t   channel_last;
-} Multiplexer;
+/* find a client by its channel */
+static Client*
+multiplexer_find_client( Multiplexer*  mult, int  channel )
+{
+    Client* c = mult->clients;
 
-/* receive a packet from the serial port, send it to the relevant client/channel */
-static void  multiplexer_receive_serial( Multiplexer*  m, Packet*  p );
+    for ( ; c != NULL; c = c->next ) {
+        if (c->channel == channel)
+            return c;
+    }
+    return NULL;
+}
+
+/* handle control messages coming from the serial port
+ * on CONTROL_CHANNEL.
+ */
+static void
+multiplexer_handle_control( Multiplexer*  mult, Packet*  p )
+{
+    /* connection registration success */
+    if (p->len == 15 && !memcmp(p->data, "ok:connect:", 11)) {
+        int      channel = hex2int(p->data+11, 4);
+        Client*  client  = multiplexer_find_client(mult, channel);
+
+        /* note that 'client' can be NULL if the corresponding
+         * socket was closed before the emulator response arrived.
+         */
+        if (client != NULL) {
+            client_registration(client, 1);
+        }
+        goto EXIT;
+    }
+
+    /* connection registration failure */
+    if (p->len >= 15 && !memcmp(p->data, "ko:connect:",11)) {
+        int     channel = hex2int(p->data+11, 4);
+        Client* client  = multiplexer_find_client(mult, channel);
+
+        if (client != NULL)
+            client_registration(client, 0);
+
+        goto EXIT;
+    }
+
+    /* emulator-induced client disconnection */
+    if (p->len == 15 && !memcmp(p->data, "disconnect:",11)) {
+        int      channel = hex2int(p->data+11, 4);
+        Client*  client  = multiplexer_find_client(mult, channel);
+
+        if (client != NULL)
+            client_free(client);
+
+        goto EXIT;
+    }
+
+    D("%s: unknown control message: '%.*s'",
+      __FUNCTION__, p->len, p->data);
+
+EXIT:
+    packet_free(&p);
+}
+
+/* a function called when an incoming packet comes from the serial port */
+static void
+multiplexer_serial_receive( Multiplexer*  mult, Packet*  p )
+{
+    Client*  client;
+
+    if (p->channel == CHANNEL_CONTROL) {
+        multiplexer_handle_control(mult, p);
+        return;
+    }
+
+    client = multiplexer_find_client(mult, p->channel);
+    if (client != NULL) {
+        client_send(client, p);
+        return;
+    }
+
+    D("%s: discarding packet for unknown channel %d", __FUNCTION__, p->channel);
+    packet_free(&p);
+}
+
+/* a function called when the serial reader closes */
+static void
+multiplexer_serial_close( Multiplexer*  mult )
+{
+    fatal("unexpected close of serial reader");
+}
+
+/* a function called to send a packet to the serial port */
+static void
+multiplexer_serial_send( Multiplexer*  mult, int  channel, Packet*  p )
+{
+    p->channel = channel;
+    serial_send( mult->serial, p );
+}
+
+
+
+/* a function used by a client to allocate a new channel id and
+ * ask the emulator to open it. 'service' must be a packet containing
+ * the name of the service in its payload.
+ *
+ * returns -1 if the service name is too long.
+ *
+ * notice that client_registration() will be called later when
+ * the answer arrives.
+ */
+static int
+multiplexer_open_channel( Multiplexer*  mult, Packet*  service )
+{
+    Packet*   p = packet_alloc();
+    int       len, channel;
+
+    /* find a free channel number, assume we don't have many
+     * clients here. */
+    {
+        Client*  c;
+    TRY_AGAIN:
+        channel = (++mult->last_channel) & 0xff;
+
+        for (c = mult->clients; c != NULL; c = c->next)
+            if (c->channel == channel)
+                goto TRY_AGAIN;
+    }
+
+    len = snprintf((char*)p->data, sizeof p->data, "connect:%.*s:%04x", service->len, service->data, channel);
+    if (len >= (int)sizeof(p->data)) {
+        D("%s: weird, service name too long (%d > %d)", __FUNCTION__, len, sizeof(p->data));
+        packet_free(&p);
+        return -1;
+    }
+    p->channel = CHANNEL_CONTROL;
+    p->len     = len;
+
+    serial_send(mult->serial, p);
+    return channel;
+}
+
+/* used to tell the emulator a channel was closed by a client */
+static void
+multiplexer_close_channel( Multiplexer*  mult, int  channel )
+{
+    Packet*  p   = packet_alloc();
+    int      len = snprintf((char*)p->data, sizeof(p->data), "disconnect:%04x", channel);
+
+    if (len > (int)sizeof(p->data)) {
+        /* should not happen */
+        return;
+    }
+
+    p->channel = CHANNEL_CONTROL;
+    p->len     = len;
+
+    serial_send(mult->serial, p);
+}
+
+/* this function is used when a new connection happens on the control
+ * socket.
+ */
+static void
+multiplexer_control_accept( Multiplexer*  m, Packet*  p )
+{
+    /* the file descriptor for the new socket connection is
+     * in p->channel. See fdhandler_accept_event() */
+    int      fd     = p->channel;
+    Client*  client = client_new( m, fd, m->fdhandlers, &m->clients );
+
+    D("created client %p listening on fd %d", client, fd);
+
+    /* free dummy packet */
+    packet_free(&p);
+}
+
+static void
+multiplexer_control_close( Multiplexer*  m )
+{
+    fatal("unexpected multiplexer control close");
+}
 
 static void
 multiplexer_init( Multiplexer*  m, const char*  serial_dev )
 {
-    int       fd;
+    int       fd, control_fd;
     Receiver  recv;
 
+    /* initialize looper and fdhandlers list */
     looper_init( m->looper );
+    fdhandler_list_init( m->fdhandlers, m->looper );
 
-    fd = open(serial_dev, O_RDWR);
+    /* open the serial port */
+    do {
+        fd = open(serial_dev, O_RDWR);
+    } while (fd < 0 && errno == EINTR);
+
     if (fd < 0) {
         fatal( "%s: could not open '%s': %s", __FUNCTION__, serial_dev,
                strerror(errno) );
@@ -1120,141 +1617,34 @@
         tcsetattr( fd, TCSANOW, &ios );
     }
 
+    /* initialize the serial reader/writer */
     recv.user  = m;
-    recv.post  = (PostFunc) multiplexer_receive_serial;
-    recv.close = NULL;
+    recv.post  = (PostFunc)  multiplexer_serial_receive;
+    recv.close = (CloseFunc) multiplexer_serial_close;
 
-    serial_init( m->serial, fd, m->looper, &recv );
+    serial_init( m->serial, fd, m->fdhandlers, &recv );
 
-    m->channels     = NULL;
-    m->channel_last = CHANNEL_CONTROL+1;
-}
+    /* open the qemud control socket */
+    recv.user  = m;
+    recv.post  = (PostFunc)  multiplexer_control_accept;
+    recv.close = (CloseFunc) multiplexer_control_close;
 
-static void
-multiplexer_add_channel( Multiplexer*  m, int  fd, const char*  name, ChannelType  ctype )
-{
-    Channel*  c;
-    Receiver  recv;
-
-    /* send channel client data directly to the serial port */
-    recv.user  = m->serial;
-    recv.post  = (PostFunc) serial_send;
-    recv.close = (CloseFunc) client_close;
-
-    /* connect each channel directly to the serial port */
-    c = channel_new( fd, ctype, name, m->channel_last, m->looper, &recv );
-    channel_link( c, &m->channels );
-
-    m->channel_last += 1;
-    if (m->channel_last <= CHANNEL_CONTROL)
-        m->channel_last += 1;
-}
-
-
-static void
-multiplexer_done( Multiplexer*  m )
-{
-    while (m->channels)
-        channel_close(m->channels);
-
-    serial_done( m->serial );
-    looper_done( m->looper );
-}
-
-
-static void
-multiplexer_send_answer( Multiplexer*  m, Packet*  p, const char*  answer )
-{
-    p->len = strlen( answer );
-    if (p->len >= MAX_PAYLOAD)
-        p->len = MAX_PAYLOAD-1;
-
-    memcpy( (char*)p->data, answer, p->len );
-    p->channel = CHANNEL_CONTROL;
-
-    serial_send( m->serial, p );
-}
-
-
-static void
-multiplexer_handle_connect( Multiplexer*  m, Packet*  p, char*  name )
-{
-    int       n;
-    Channel*  c;
-
-    if (p->len >= MAX_PAYLOAD) {
-        multiplexer_send_answer( m, p, "ko:connect:bad name" );
-        return;
-    }
-    p->data[p->len] = 0;
-
-    for (c = m->channels; c != NULL; c = c->next)
-        if ( !strcmp(c->name, name) )
-            break;
-
-    if (c == NULL) {
-        D("can't connect to unknown channel '%s'", name);
-        multiplexer_send_answer( m, p, "ko:connect:bad name" );
-        return;
+    fd = android_get_control_socket(CONTROL_SOCKET_NAME);
+    if (fd < 0) {
+        fatal("couldn't get fd for control socket '%s'", CONTROL_SOCKET_NAME);
     }
 
-    p->channel = CHANNEL_CONTROL;
-    p->len     = snprintf( (char*)p->data, MAX_PAYLOAD,
-                       "ok:connect:%s:%02x", c->name, c->index );
+    fdhandler_new_accept( fd, m->fdhandlers, &recv );
 
-    serial_send( m->serial, p );
+    /* initialize clients list */
+    m->clients = NULL;
 }
 
-
-static void
-multiplexer_receive_serial( Multiplexer*  m, Packet*  p )
-{
-    Channel*  c = m->channels;
-
-    /* check the destination channel index */
-    if (p->channel != CHANNEL_CONTROL) {
-        Channel*  c;
-
-        for (c = m->channels; c; c = c->next ) {
-            if (c->index == p->channel) {
-                channel_send( c, p );
-                break;
-            }
-        }
-        if (c == NULL) {
-            D("ignoring %d bytes packet for unknown channel index %d",
-                p->len, p->channel );
-            packet_free(&p);
-        }
-    }
-    else  /* packet addressed to the control channel */
-    {
-        D("received control message:  '%.*s'", p->len, p->data);
-        if (p->len > 8 && strncmp( (char*)p->data, "connect:", 8) == 0) {
-            multiplexer_handle_connect( m, p, (char*)p->data + 8 );
-        } else {
-            /* unknown command */
-            multiplexer_send_answer( m, p, "ko:unknown command" );
-        }
-        return;
-    }
-}
-
-
 /** MAIN LOOP
  **/
 
 static Multiplexer  _multiplexer[1];
 
-#define  QEMUD_PREFIX  "qemud_"
-
-static const struct { const char* name; ChannelType  ctype; }   default_channels[] = {
-    { "gsm", CHANNEL_DUPLEX },       /* GSM AT command channel, used by commands/rild/rild.c */
-    { "gps", CHANNEL_BROADCAST },    /* GPS NMEA commands, used by libs/hardware_legacy/qemu_gps.c  */
-    { "control", CHANNEL_DUPLEX },   /* Used for power/leds/vibrator/etc... */
-    { NULL, 0 }
-};
-
 int  main( void )
 {
     Multiplexer*  m = _multiplexer;
@@ -1303,31 +1693,6 @@
         multiplexer_init( m, buff );
     }
 
-    D("multiplexer inited, creating default channels");
-
-    /* now setup all default channels */
-    {
-        int  nn;
-
-        for (nn = 0; default_channels[nn].name != NULL; nn++) {
-            char         control_name[32];
-            int          fd;
-            Channel*     chan;
-            const char*  name  = default_channels[nn].name;
-            ChannelType  ctype = default_channels[nn].ctype;
-
-            snprintf(control_name, sizeof(control_name), "%s%s",
-                     QEMUD_PREFIX, name);
-
-            if ((fd = android_get_control_socket(control_name)) < 0) {
-                D("couldn't get fd for control socket '%s'", name);
-                continue;
-            }
-            D( "got control socket '%s' on fd %d", control_name, fd);
-            multiplexer_add_channel( m, fd, name, ctype );
-        }
-    }
-
     D( "entering main loop");
     looper_loop( m->looper );
     D( "unexpected termination !!" );
diff --git a/emulator/sensors/Android.mk b/emulator/sensors/Android.mk
new file mode 100644
index 0000000..402da82
--- /dev/null
+++ b/emulator/sensors/Android.mk
@@ -0,0 +1,28 @@
+# Copyright (C) 2009 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.
+
+
+LOCAL_PATH := $(call my-dir)
+
+ifneq ($(TARGET_PRODUCT),sim)
+# HAL module implemenation, not prelinked and stored in
+# hw/<SENSORS_HARDWARE_MODULE_ID>.<ro.hardware>.so
+include $(CLEAR_VARS)
+LOCAL_PRELINK_MODULE := false
+LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw
+LOCAL_SHARED_LIBRARIES := liblog libcutils
+LOCAL_SRC_FILES := sensors_qemu.c
+LOCAL_MODULE := sensors.goldfish
+include $(BUILD_SHARED_LIBRARY)
+endif
diff --git a/emulator/sensors/sensors_qemu.c b/emulator/sensors/sensors_qemu.c
new file mode 100644
index 0000000..85a5af4
--- /dev/null
+++ b/emulator/sensors/sensors_qemu.c
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+/* this implements a sensors hardware library for the Android emulator.
+ * the following code should be built as a shared library that will be
+ * placed into /system/lib/hw/sensors.goldfish.so
+ *
+ * it will be loaded by the code in hardware/libhardware/hardware.c
+ * which is itself called from com_android_server_SensorService.cpp
+ */
+
+
+/* we connect with the emulator through the "sensors" qemud service
+ */
+#define  SENSORS_SERVICE_NAME "sensors"
+
+#define LOG_TAG "QemuSensors"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+#include <cutils/log.h>
+#include <cutils/sockets.h>
+#include <hardware/sensors.h>
+
+#if 0
+#define  D(...)  LOGD(__VA_ARGS__)
+#else
+#define  D(...)  ((void)0)
+#endif
+
+#define  E(...)  LOGE(__VA_ARGS__)
+
+#include <hardware/qemud.h>
+
+/** SENSOR IDS AND NAMES
+ **/
+
+#define MAX_NUM_SENSORS 4
+
+#define SUPPORTED_SENSORS  ((1<<MAX_NUM_SENSORS)-1)
+
+#define  ID_BASE           SENSORS_HANDLE_BASE
+#define  ID_ACCELERATION   (ID_BASE+0)
+#define  ID_MAGNETIC_FIELD (ID_BASE+1)
+#define  ID_ORIENTATION    (ID_BASE+2)
+#define  ID_TEMPERATURE    (ID_BASE+3)
+
+#define  SENSORS_ACCELERATION   (1 << ID_ACCELERATION)
+#define  SENSORS_MAGNETIC_FIELD  (1 << ID_MAGNETIC_FIELD)
+#define  SENSORS_ORIENTATION     (1 << ID_ORIENTATION)
+#define  SENSORS_TEMPERATURE     (1 << ID_TEMPERATURE)
+
+#define  ID_CHECK(x)  ((unsigned)((x)-ID_BASE) < 4)
+
+#define  SENSORS_LIST  \
+    SENSOR_(ACCELERATION,"acceleration") \
+    SENSOR_(MAGNETIC_FIELD,"magnetic-field") \
+    SENSOR_(ORIENTATION,"orientation") \
+    SENSOR_(TEMPERATURE,"temperature") \
+
+static const struct {
+    const char*  name;
+    int          id; } _sensorIds[MAX_NUM_SENSORS] =
+{
+#define SENSOR_(x,y)  { y, ID_##x },
+    SENSORS_LIST
+#undef  SENSOR_
+};
+
+static const char*
+_sensorIdToName( int  id )
+{
+    int  nn;
+    for (nn = 0; nn < MAX_NUM_SENSORS; nn++)
+        if (id == _sensorIds[nn].id)
+            return _sensorIds[nn].name;
+    return "<UNKNOWN>";
+}
+
+static int
+_sensorIdFromName( const char*  name )
+{
+    int  nn;
+
+    if (name == NULL)
+        return -1;
+
+    for (nn = 0; nn < MAX_NUM_SENSORS; nn++)
+        if (!strcmp(name, _sensorIds[nn].name))
+            return _sensorIds[nn].id;
+
+    return -1;
+}
+
+/** SENSORS CONTROL DEVICE
+ **
+ ** This one is used to send commands to the sensors drivers.
+ ** We implement this by sending directly commands to the emulator
+ ** through the QEMUD channel.
+ **/
+
+typedef struct SensorControl {
+    struct sensors_control_device_t  device;
+    int                              fd;
+    uint32_t                         active_sensors;
+} SensorControl;
+
+/* this must return a file descriptor that will be used to read
+ * the sensors data (it is passed to data__data_open() below
+ */
+static int
+control__open_data_source(struct sensors_control_device_t *dev)
+{
+    SensorControl*  ctl = (void*)dev;
+
+    if (ctl->fd < 0) {
+        ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+    }
+    D("%s: fd=%d", __FUNCTION__, ctl->fd);
+    return ctl->fd;
+}
+
+static int
+control__activate(struct sensors_control_device_t *dev,
+                  int handle,
+                  int enabled)
+{
+    SensorControl*  ctl = (void*)dev;
+    uint32_t        mask, sensors, active, new_sensors, changed;
+    char            command[128];
+    int             ret;
+
+    D("%s: handle=%s (%d) enabled=%d", __FUNCTION__,
+        _sensorIdToName(handle), handle, enabled);
+
+    if (!ID_CHECK(handle)) {
+        E("%s: bad handle ID", __FUNCTION__);
+        return -1;
+    }
+
+    mask    = (1<<handle);
+    sensors = enabled ? mask : 0;
+
+    active      = ctl->active_sensors;
+    new_sensors = (active & ~mask) | (sensors & mask);
+    changed     = active ^ new_sensors;
+
+    if (!changed)
+        return 0;
+
+    snprintf(command, sizeof command, "set:%s:%d",
+                _sensorIdToName(handle), enabled != 0);
+
+    if (ctl->fd < 0) {
+        ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+    }
+
+    ret = qemud_channel_send(ctl->fd, command, -1);
+    if (ret < 0)
+        return -1;
+
+    ctl->active_sensors = new_sensors;
+
+    return 0;
+}
+
+static int
+control__set_delay(struct sensors_control_device_t *dev, int32_t ms)
+{
+    SensorControl*  ctl = (void*)dev;
+    char            command[128];
+
+    D("%s: dev=%p delay-ms=%d", __FUNCTION__, dev, ms);
+
+    snprintf(command, sizeof command, "set-delay:%d", ms);
+
+    return qemud_channel_send(ctl->fd, command, -1);
+}
+
+/* this function is used to force-stop the blocking read() in
+ * data__poll. In order to keep the implementation as simple
+ * as possible here, we send a command to the emulator which
+ * shall send back an appropriate data block to the system.
+ */
+static int
+control__wake(struct sensors_control_device_t *dev)
+{
+    SensorControl*  ctl = (void*)dev;
+    D("%s: dev=%p", __FUNCTION__, dev);
+    return qemud_channel_send(ctl->fd, "wake", -1);
+}
+
+
+static int
+control__close(struct hw_device_t *dev) 
+{
+    SensorControl*  ctl = (void*)dev;
+    close(ctl->fd);
+    free(ctl);
+    return 0;
+}
+
+/** SENSORS DATA DEVICE
+ **
+ ** This one is used to read sensor data from the hardware.
+ ** We implement this by simply reading the data from the
+ ** emulator through the QEMUD channel.
+ **/
+
+
+typedef struct SensorData {
+    struct sensors_data_device_t  device;
+    sensors_data_t                sensors[MAX_NUM_SENSORS];
+    int                           events_fd;
+    uint32_t                      pendingSensors;
+    int64_t                       timeStart;
+    int64_t                       timeOffset;
+} SensorData;
+
+/* return the current time in nanoseconds */
+static int64_t
+data__now_ns(void)
+{
+    struct timespec  ts;
+
+    clock_gettime(CLOCK_MONOTONIC, &ts);
+
+    return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
+}
+
+static int
+data__data_open(struct sensors_data_device_t *dev, int fd)
+{
+    SensorData*  data = (void*)dev;
+    int i;
+    D("%s: dev=%p fd=%d", __FUNCTION__, dev, fd);
+    memset(&data->sensors, 0, sizeof(data->sensors));
+
+    for (i=0 ; i<MAX_NUM_SENSORS ; i++) {
+        data->sensors[i].vector.status = SENSOR_STATUS_ACCURACY_HIGH;
+    }
+    data->pendingSensors = 0;
+    data->timeStart      = 0;
+    data->timeOffset     = 0;
+
+    data->events_fd = dup(fd);
+    return 0;
+}
+
+static int
+data__data_close(struct sensors_data_device_t *dev)
+{
+    SensorData*  data = (void*)dev;
+    D("%s: dev=%p", __FUNCTION__, dev);
+    if (data->events_fd > 0) {
+        close(data->events_fd);
+        data->events_fd = -1;
+    }
+    return 0;
+}
+
+static int
+pick_sensor(SensorData*      data,
+            sensors_data_t*  values)
+{
+    uint32_t mask = SUPPORTED_SENSORS;
+    while (mask) {
+        uint32_t i = 31 - __builtin_clz(mask);
+        mask &= ~(1<<i);
+        if (data->pendingSensors & (1<<i)) {
+            data->pendingSensors &= ~(1<<i);
+            *values = data->sensors[i];
+            values->sensor = (1<<i);
+            LOGD_IF(0, "%s: %d [%f, %f, %f]", __FUNCTION__,
+                    (1<<i),
+                    values->vector.x,
+                    values->vector.y,
+                    values->vector.z);
+            return i;
+        }
+    }
+    LOGE("No sensor to return!!! pendingSensors=%08x", data->pendingSensors);
+    // we may end-up in a busy loop, slow things down, just in case.
+    usleep(100000);
+    return -1;
+}
+
+static int
+data__poll(struct sensors_data_device_t *dev, sensors_data_t* values)
+{
+    SensorData*  data = (void*)dev;
+    int fd = data->events_fd;
+
+    D("%s: data=%p", __FUNCTION__, dev);
+
+    // there are pending sensors, returns them now...
+    if (data->pendingSensors) {
+        return pick_sensor(data, values);
+    }
+
+    // wait until we get a complete event for an enabled sensor
+    uint32_t new_sensors = 0;
+
+    while (1) {
+        /* read the next event */
+        char     buff[256];
+        int      len = qemud_channel_recv(data->events_fd, buff, sizeof buff-1);
+        float    params[3];
+        int64_t  event_time;
+
+        if (len < 0)
+            continue;
+
+        buff[len] = 0;
+
+        /* "wake" is sent from the emulator to exit this loop. This shall
+         * really be because another thread called "control__wake" in this
+         * process.
+         */
+        if (!strcmp((const char*)data, "wake")) {
+            return 0x7FFFFFFF;
+        }
+
+        /* "acceleration:<x>:<y>:<z>" corresponds to an acceleration event */
+        if (sscanf(buff, "acceleration:%g:%g:%g", params+0, params+1, params+2) == 3) {
+            new_sensors |= SENSORS_ACCELERATION;
+            data->sensors[ID_ACCELERATION].acceleration.x = params[0];
+            data->sensors[ID_ACCELERATION].acceleration.y = params[1];
+            data->sensors[ID_ACCELERATION].acceleration.z = params[2];
+            continue;
+        }
+
+        /* "orientation:<azimuth>:<pitch>:<roll>" is sent when orientation changes */
+        if (sscanf(buff, "orientation:%g:%g:%g", params+0, params+1, params+2) == 3) {
+            new_sensors |= SENSORS_ORIENTATION;
+            data->sensors[ID_ORIENTATION].orientation.azimuth = params[0];
+            data->sensors[ID_ORIENTATION].orientation.pitch   = params[1];
+            data->sensors[ID_ORIENTATION].orientation.roll    = params[2];
+            continue;
+        }
+
+        /* "magnetic:<x>:<y>:<z>" is sent for the params of the magnetic field */
+        if (sscanf(buff, "magnetic:%g:%g:%g", params+0, params+1, params+2) == 3) {
+            new_sensors |= SENSORS_MAGNETIC_FIELD;
+            data->sensors[ID_MAGNETIC_FIELD].magnetic.x = params[0];
+            data->sensors[ID_MAGNETIC_FIELD].magnetic.y = params[1];
+            data->sensors[ID_MAGNETIC_FIELD].magnetic.z = params[2];
+            continue;
+        }
+
+        /* "temperature:<celsius>" */
+        if (sscanf(buff, "temperature:%g", params+0) == 2) {
+            new_sensors |= SENSORS_TEMPERATURE;
+            data->sensors[ID_TEMPERATURE].temperature = params[0];
+            continue;
+        }
+
+        /* "sync:<time>" is sent after a series of sensor events.
+         * where 'time' is expressed in micro-seconds and corresponds
+         * to the VM time when the real poll occured.
+         */
+        if (sscanf(buff, "sync:%lld", &event_time) == 1) {
+            if (new_sensors) {
+                data->pendingSensors = new_sensors;
+                int64_t t = event_time * 1000LL;  /* convert to nano-seconds */
+
+                /* use the time at the first sync: as the base for later
+                 * time values */
+                if (data->timeStart == 0) {
+                    data->timeStart  = data__now_ns();
+                    data->timeOffset = data->timeStart - t;
+                }
+                t += data->timeOffset;
+
+                while (new_sensors) {
+                    uint32_t i = 31 - __builtin_clz(new_sensors);
+                    new_sensors &= ~(1<<i);
+                    data->sensors[i].time = t;
+                }
+                return pick_sensor(data, values);
+            } else {
+                D("huh ? sync without any sensor data ?");
+            }
+            continue;
+        }
+        D("huh ? unsupported command");
+    }
+}
+
+static int
+data__close(struct hw_device_t *dev) 
+{
+    SensorData* data = (SensorData*)dev;
+    if (data) {
+        if (data->events_fd > 0) {
+            //LOGD("(device close) about to close fd=%d", data->events_fd);
+            close(data->events_fd);
+        }
+        free(data);
+    }
+    return 0;
+}
+
+
+/** MODULE REGISTRATION SUPPORT
+ **
+ ** This is required so that hardware/libhardware/hardware.c
+ ** will dlopen() this library appropriately.
+ **/
+
+/*
+ * the following is the list of all supported sensors.
+ * this table is used to build sSensorList declared below
+ * according to which hardware sensors are reported as
+ * available from the emulator (see get_sensors_list below)
+ *
+ * note: numerical values for maxRange/resolution/power were
+ *       taken from the reference AK8976A implementation
+ */
+static const struct sensor_t sSensorListInit[] = {
+        { .name       = "Goldfish 3-axis Accelerometer",
+          .vendor     = "The Android Open Source Project",
+          .version    = 1,
+          .handle     = ID_ACCELERATION,
+          .type       = SENSOR_TYPE_ACCELEROMETER,
+          .maxRange   = 2.8f,
+          .resolution = 1.0f/4032.0f,
+          .power      = 3.0f,
+          .reserved   = {}
+        },
+
+        { .name       = "Goldfish 3-axis Magnetic field sensor",
+          .vendor     = "The Android Open Source Project",
+          .version    = 1,
+          .handle     = ID_MAGNETIC_FIELD,
+          .type       = SENSOR_TYPE_MAGNETIC_FIELD,
+          .maxRange   = 2000.0f,
+          .resolution = 1.0f,
+          .power      = 6.7f,
+          .reserved   = {}
+        },
+
+        { .name       = "Goldfish Orientation sensor",
+          .vendor     = "The Android Open Source Project",
+          .version    = 1,
+          .handle     = ID_ORIENTATION,
+          .type       = SENSOR_TYPE_ORIENTATION,
+          .maxRange   = 360.0f,
+          .resolution = 1.0f,
+          .power      = 9.7f,
+          .reserved   = {}
+        },
+
+        { .name       = "Goldfish Temperature sensor",
+          .vendor     = "The Android Open Source Project",
+          .version    = 1,
+          .handle     = ID_TEMPERATURE,
+          .type       = SENSOR_TYPE_TEMPERATURE,
+          .maxRange   = 80.0f,
+          .resolution = 1.0f,
+          .power      = 0.0f,
+          .reserved   = {}
+        },
+};
+
+static struct sensor_t  sSensorList[MAX_NUM_SENSORS];
+
+static uint32_t sensors__get_sensors_list(struct sensors_module_t* module,
+        struct sensor_t const** list) 
+{
+    int  fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+    char buffer[12];
+    int  mask, nn, count;
+
+    int  ret;
+    if (fd < 0) {
+        E("%s: no qemud connection", __FUNCTION__);
+        return 0;
+    }
+    ret = qemud_channel_send(fd, "list-sensors", -1);
+    if (ret < 0) {
+        E("%s: could not query sensor list: %s", __FUNCTION__,
+          strerror(errno));
+        close(fd);
+        return 0;
+    }
+    ret = qemud_channel_recv(fd, buffer, sizeof buffer-1);
+    if (ret < 0) {
+        E("%s: could not receive sensor list: %s", __FUNCTION__,
+          strerror(errno));
+        close(fd);
+        return 0;
+    }
+    buffer[ret] = 0;
+    close(fd);
+
+    /* the result is a integer used as a mask for available sensors */
+    mask  = atoi(buffer);
+    count = 0;
+    for (nn = 0; nn < MAX_NUM_SENSORS; nn++) {
+        if (((1 << nn) & mask) == 0)
+            continue;
+
+        sSensorList[count++] = sSensorListInit[nn];
+    }
+    D("%s: returned %d sensors (mask=%d)", __FUNCTION__, count, mask);
+    *list = sSensorList;
+    return count;
+}
+
+
+static int
+open_sensors(const struct hw_module_t* module,
+             const char*               name,
+             struct hw_device_t*      *device)
+{
+    int  status = -EINVAL;
+
+    D("%s: name=%s", __FUNCTION__, name);
+
+    if (!strcmp(name, SENSORS_HARDWARE_CONTROL))
+    {
+        SensorControl *dev = malloc(sizeof(*dev));
+
+        memset(dev, 0, sizeof(*dev));
+
+        dev->device.common.tag       = HARDWARE_DEVICE_TAG;
+        dev->device.common.version   = 0;
+        dev->device.common.module    = (struct hw_module_t*) module;
+        dev->device.common.close     = control__close;
+        dev->device.open_data_source = control__open_data_source;
+        dev->device.activate         = control__activate;
+        dev->device.set_delay        = control__set_delay;
+        dev->device.wake             = control__wake;
+        dev->fd                      = -1;
+
+        *device = &dev->device.common;
+        status  = 0;
+    }
+    else if (!strcmp(name, SENSORS_HARDWARE_DATA)) {
+        SensorData *dev = malloc(sizeof(*dev));
+
+        memset(dev, 0, sizeof(*dev));
+
+        dev->device.common.tag     = HARDWARE_DEVICE_TAG;
+        dev->device.common.version = 0;
+        dev->device.common.module  = (struct hw_module_t*) module;
+        dev->device.common.close   = data__close;
+        dev->device.data_open      = data__data_open;
+        dev->device.data_close     = data__data_close;
+        dev->device.poll           = data__poll;
+        dev->events_fd             = -1;
+
+        *device = &dev->device.common;
+        status  = 0;
+    }
+    return status;
+}
+
+
+static struct hw_module_methods_t sensors_module_methods = {
+    .open = open_sensors
+};
+
+const struct sensors_module_t HAL_MODULE_INFO_SYM = {
+    .common = {
+        .tag = HARDWARE_MODULE_TAG,
+        .version_major = 1,
+        .version_minor = 0,
+        .id = SENSORS_HARDWARE_MODULE_ID,
+        .name = "Goldfish SENSORS Module",
+        .author = "The Android Open Source Project",
+        .methods = &sensors_module_methods,
+    },
+    .get_sensors_list = sensors__get_sensors_list
+};
diff --git a/pdk/README b/pdk/README
index 03af4e8..8662155 100644
--- a/pdk/README
+++ b/pdk/README
@@ -1,6 +1,9 @@
 Building the pdk (platform development kit)
 
-1) get a cupcake source tree
+1) get a cupcake source tree with all the normal tools... and add doxygen 
+(We currently support version 1.4.6)
+
+  sudo apt-get install doxygen
 
 2) from the root
   . build/envsetup.sh
diff --git a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
index 9aeb2b5..50b3536 100644
--- a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
+++ b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
@@ -550,7 +550,7 @@
             boolean typedWordValid) {
         if (suggestions != null && suggestions.size() > 0) {
             setCandidatesViewShown(true);
-        } else if (isFullscreenMode()) {
+        } else if (isExtractViewShown()) {
             setCandidatesViewShown(true);
         }
         if (mCandidateView != null) {
diff --git a/testrunner/Android.mk b/testrunner/Android.mk
new file mode 100644
index 0000000..93c0928
--- /dev/null
+++ b/testrunner/Android.mk
@@ -0,0 +1,19 @@
+#
+# Install a list of test definitions on device
+#
+
+# where to install the sample files on the device
+# 
+local_target_dir := $(TARGET_OUT_DATA)/testinfo
+LOCAL_PATH := $(call my-dir)
+
+########################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := tests.xml
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_PATH := $(local_target_dir)
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+
+include $(BUILD_PREBUILT)
diff --git a/testrunner/android_build.py b/testrunner/android_build.py
new file mode 100644
index 0000000..ca43ece
--- /dev/null
+++ b/testrunner/android_build.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python2.4
+#
+#
+# Copyright 2008, 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.
+
+"""Contains utility functions for interacting with the Android build system."""
+
+# Python imports
+import os
+
+# local imports
+import errors
+import logger
+
+
+def GetTop():
+  """Returns the full pathname of the "top" of the Android development tree.
+
+  Assumes build environment has been properly configured by envsetup &
+  lunch/choosecombo.
+
+  Returns:
+    the absolute file path of the Android build root.
+
+  Raises:
+    AbortError: if Android build root could not be found.
+  """
+  # TODO: does this need to be reimplemented to be like gettop() in envsetup.sh
+  root_path = os.getenv('ANDROID_BUILD_TOP')
+  if root_path is None:
+    logger.Log('Error: ANDROID_BUILD_TOP not defined. Please run envsetup.sh')
+    raise errors.AbortError
+  return root_path
diff --git a/testrunner/runtest.py b/testrunner/runtest.py
new file mode 100755
index 0000000..bf5bb22
--- /dev/null
+++ b/testrunner/runtest.py
@@ -0,0 +1,280 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008, 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.
+
+"""Command line utility for running a pre-defined test.
+
+Based on previous <androidroot>/development/tools/runtest shell script.
+"""
+
+# Python imports
+import glob
+import optparse
+import os
+from sets import Set
+import sys
+
+# local imports
+import adb_interface
+import android_build
+import coverage
+import errors
+import logger
+import run_command
+import test_defs
+
+
+class TestRunner(object):
+  """Command line utility class for running pre-defined Android test(s)."""
+
+  # file path to android core platform tests, relative to android build root
+  # TODO move these test data files to another directory
+  _CORE_TEST_PATH = os.path.join("development", "testrunner", "tests.xml")
+
+  # vendor glob file path patterns to tests, relative to android
+  # build root
+  _VENDOR_TEST_PATH = os.path.join("vendor", "*", "tests", "testinfo",
+                                   "tests.xml")
+
+  _RUNTEST_USAGE = (
+      "usage: runtest.py [options] short-test-name[s]\n\n"
+      "The runtest script works in two ways.  You can query it "
+      "for a list of tests, or you can launch one or more tests.")
+
+  def _ProcessOptions(self):
+    """Processes command-line options."""
+    # TODO error messages on once-only or mutually-exclusive options.
+    user_test_default = os.path.join(os.environ.get("HOME"), ".android",
+                                     "tests.xml")
+
+    parser = optparse.OptionParser(usage=self._RUNTEST_USAGE)
+
+    parser.add_option("-l", "--list-tests", dest="only_list_tests",
+                      default=False, action="store_true",
+                      help="To view the list of tests")
+    parser.add_option("-b", "--skip-build", dest="skip_build", default=False,
+                      action="store_true", help="Skip build - just launch")
+    parser.add_option("-n", "--skip_execute", dest="preview", default=False,
+                      action="store_true",
+                      help="Do not execute, just preview commands")
+    parser.add_option("-r", "--raw-mode", dest="raw_mode", default=False,
+                      action="store_true",
+                      help="Raw mode (for output to other tools)")
+    parser.add_option("-a", "--suite-assign", dest="suite_assign_mode",
+                      default=False, action="store_true",
+                      help="Suite assignment (for details & usage see "
+                      "InstrumentationTestRunner)")
+    parser.add_option("-v", "--verbose", dest="verbose", default=False,
+                      action="store_true",
+                      help="Increase verbosity of %s" % sys.argv[0])
+    parser.add_option("-w", "--wait-for-debugger", dest="wait_for_debugger",
+                      default=False, action="store_true",
+                      help="Wait for debugger before launching tests")
+    parser.add_option("-c", "--test-class", dest="test_class",
+                      help="Restrict test to a specific class")
+    parser.add_option("-m", "--test-method", dest="test_method",
+                      help="Restrict test to a specific method")
+    parser.add_option("-u", "--user-tests-file", dest="user_tests_file",
+                      metavar="FILE", default=user_test_default,
+                      help="Alternate source of user test definitions")
+    parser.add_option("-o", "--coverage", dest="coverage",
+                      default=False, action="store_true",
+                      help="Generate code coverage metrics for test(s)")
+    parser.add_option("-t", "--all-tests", dest="all_tests",
+                      default=False, action="store_true",
+                      help="Run all defined tests")
+    parser.add_option("--continuous", dest="continuous_tests",
+                      default=False, action="store_true",
+                      help="Run all tests defined as part of the continuous "
+                      "test set")
+
+    group = optparse.OptionGroup(
+        parser, "Targets", "Use these options to direct tests to a specific "
+        "Android target")
+    group.add_option("-e", "--emulator", dest="emulator", default=False,
+                     action="store_true", help="use emulator")
+    group.add_option("-d", "--device", dest="device", default=False,
+                     action="store_true", help="use device")
+    group.add_option("-s", "--serial", dest="serial",
+                     help="use specific serial")
+    parser.add_option_group(group)
+
+    self._options, self._test_args = parser.parse_args()
+
+    if (not self._options.only_list_tests and not self._options.all_tests
+        and not self._options.continuous_tests and len(self._test_args) < 1):
+      parser.print_help()
+      logger.SilentLog("at least one test name must be specified")
+      raise errors.AbortError
+
+    self._adb = adb_interface.AdbInterface()
+    if self._options.emulator:
+      self._adb.SetEmulatorTarget()
+    elif self._options.device:
+      self._adb.SetDeviceTarget()
+    elif self._options.serial is not None:
+      self._adb.SetTargetSerial(self._options.serial)
+
+    if self._options.verbose:
+      logger.SetVerbose(True)
+
+    self._root_path = android_build.GetTop()
+
+    self._known_tests = self._ReadTests()
+
+    self._coverage_gen = coverage.CoverageGenerator(
+        android_root_path=self._root_path, adb_interface=self._adb)
+
+  def _ReadTests(self):
+    """Parses the set of test definition data.
+
+    Returns:
+      A TestDefinitions object that contains the set of parsed tests.
+    Raises:
+      AbortError: If a fatal error occurred when parsing the tests.
+    """
+    core_test_path = os.path.join(self._root_path, self._CORE_TEST_PATH)
+    try:
+      known_tests = test_defs.TestDefinitions()
+      known_tests.Parse(core_test_path)
+      # read all <android root>/vendor/*/tests/testinfo/tests.xml paths
+      vendor_tests_pattern = os.path.join(self._root_path,
+                                          self._VENDOR_TEST_PATH)
+      test_file_paths = glob.glob(vendor_tests_pattern)
+      for test_file_path in test_file_paths:
+        known_tests.Parse(test_file_path)
+      if os.path.isfile(self._options.user_tests_file):
+        known_tests.Parse(self._options.user_tests_file)
+      return known_tests
+    except errors.ParseError:
+      raise errors.AbortError
+
+  def _DumpTests(self):
+    """Prints out set of defined tests."""
+    print "The following tests are currently defined:"
+    for test in self._known_tests:
+      print test.GetName()
+
+  def _DoBuild(self):
+    logger.SilentLog("Building tests...")
+    target_set = Set()
+    for test_suite in self._GetTestsToRun():
+      self._AddBuildTarget(test_suite.GetBuildPath(), target_set)
+
+    if target_set:
+      if self._options.coverage:
+        self._coverage_gen.EnableCoverageBuild()
+        self._AddBuildTarget(self._coverage_gen.GetEmmaBuildPath(), target_set)
+      target_build_string = " ".join(list(target_set))
+      logger.Log("Building %s" % target_build_string)
+      cmd = 'ONE_SHOT_MAKEFILE="%s" make -C "%s" files' %  (target_build_string,
+                                                            self._root_path)
+      if not self._options.preview:
+        run_command.RunCommand(cmd, return_output=False)
+        logger.Log("Syncing to device...")
+        self._adb.Sync()
+
+  def _AddBuildTarget(self, build_dir, target_set):
+    if build_dir is not None:
+      build_file_path = os.path.join(build_dir, "Android.mk")
+      if os.path.isfile(os.path.join(self._root_path, build_file_path)):
+        target_set.add(build_file_path)
+
+  def _GetTestsToRun(self):
+    """Get a list of TestSuite objects to run, based on command line args."""
+    if self._options.all_tests:
+      return self._known_tests.GetTests()
+    if self._options.continuous_tests:
+      return self._known_tests.GetContinuousTests()
+    tests = []
+    for name in self._test_args:
+      test = self._known_tests.GetTest(name)
+      if test is None:
+        logger.Log("Error: Could not find test %s" % name)
+        self._DumpTests()
+        raise errors.AbortError
+      tests.append(test)
+    return tests
+
+  def _RunTest(self, test_suite):
+    """Run the provided test suite.
+
+    Builds up an adb instrument command using provided input arguments.
+
+    Args:
+      test_suite: TestSuite to run
+    """
+
+    test_class = test_suite.GetClassName()
+    if self._options.test_class is not None:
+      test_class = self._options.test_class
+    if self._options.test_method is not None:
+      test_class = "%s#%s" % (test_class, self._options.test_method)
+
+    instrumentation_args = {}
+    if test_class is not None:
+      instrumentation_args["class"] = test_class
+    if self._options.wait_for_debugger:
+      instrumentation_args["debug"] = "true"
+    if self._options.suite_assign_mode:
+      instrumentation_args["suiteAssignment"] = "true"
+    if self._options.coverage:
+      instrumentation_args["coverage"] = "true"
+    if self._options.preview:
+      adb_cmd = self._adb.PreviewInstrumentationCommand(
+          package_name=test_suite.GetPackageName(),
+          runner_name=test_suite.GetRunnerName(),
+          raw_mode=self._options.raw_mode,
+          instrumentation_args=instrumentation_args)
+      logger.Log(adb_cmd)
+    else:
+      self._adb.StartInstrumentationNoResults(
+          package_name=test_suite.GetPackageName(),
+          runner_name=test_suite.GetRunnerName(),
+          raw_mode=self._options.raw_mode,
+          instrumentation_args=instrumentation_args)
+      if self._options.coverage and test_suite.GetTargetName() is not None:
+        coverage_file = self._coverage_gen.ExtractReport(test_suite)
+        if coverage_file is not None:
+          logger.Log("Coverage report generated at %s" % coverage_file)
+
+  def RunTests(self):
+    """Main entry method - executes the tests according to command line args."""
+    try:
+      run_command.SetAbortOnError()
+      self._ProcessOptions()
+      if self._options.only_list_tests:
+        self._DumpTests()
+        return
+
+      if not self._options.skip_build:
+        self._DoBuild()
+
+      for test_suite in self._GetTestsToRun():
+        self._RunTest(test_suite)
+    except KeyboardInterrupt:
+      logger.Log("Exiting...")
+    except errors.AbortError:
+      logger.SilentLog("Exiting due to AbortError...")
+    except errors.WaitForResponseTimedOutError:
+      logger.Log("Timed out waiting for response")
+
+
+def RunTests():
+  runner = TestRunner()
+  runner.RunTests()
+
+if __name__ == "__main__":
+  RunTests()
diff --git a/testrunner/tests.xml b/testrunner/tests.xml
index 8d9c0ab..d186af4 100644
--- a/testrunner/tests.xml
+++ b/testrunner/tests.xml
@@ -87,6 +87,12 @@
     coverage_target="ApiDemos"
     continuous="true" />
 
+<test name="launchperf"
+    build_path="development/apps/launchperf"
+    package="com.android.launchperf"
+    class="com.android.launchperf.SimpleActivityLaunchPerformance"
+    coverage_target="framework" />
+
 <!--  targeted framework tests -->
 <test name="heap"
     build_path="frameworks/base/tests/AndroidTests"
@@ -114,6 +120,11 @@
     class="android.content.AbstractTableMergerTest"
     coverage_target="framework" />
 
+<test name="imf"
+    build_path="frameworks/base/tests/ImfTest"
+    package="com.android.imftest.tests"
+    coverage_target="framework"
+    continuous="true" />
 
 <!--  selected app tests -->
 <test name="browser"
@@ -169,12 +180,19 @@
     runner=".MediaFrameworkTestRunner"
     coverage_target="framework"
     continuous="true" />
-
+    
 <test name="mediaunit"
     build_path="frameworks/base/media/tests/MediaFrameworkTest"
     package="com.android.mediaframeworktest"
     runner=".MediaFrameworkUnitTestRunner"
     coverage_target="framework" />
+    
+<test name="musicplayer"
+    build_path="packages/apps/Music"
+    package="com.android.music.tests"
+    runner=".MusicPlayerFunctionalTestRunner"
+    coverage_target="Music"
+    continuous="true" />
 
 <!-- obsolete?
 <test name="mediaprov"
diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
index bc1834f..1a0b21f 100755
--- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
+++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
@@ -27,7 +27,14 @@
  * <p>Expects the following output:
  * 
  * <p>If fatal error occurred when attempted to run the tests:
- * <pre> INSTRUMENTATION_FAILED: </pre>  
+ * <pre>
+ * INSTRUMENTATION_STATUS: Error=error Message
+ * INSTRUMENTATION_FAILED: 
+ * </pre>
+ * <p>or
+ * <pre>
+ * INSTRUMENTATION_RESULT: shortMsg=error Message
+ * </pre>
  * 
  * <p>Otherwise, expect a series of test results, each one containing a set of status key/value
  * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test 
@@ -56,6 +63,8 @@
         private static final String CLASS = "class";
         private static final String STACK = "stack";
         private static final String NUMTESTS = "numtests";
+        private static final String ERROR = "Error";
+        private static final String SHORTMSG = "shortMsg";
     }
     
     /** Test result status codes. */
@@ -71,6 +80,8 @@
         private static final String STATUS = "INSTRUMENTATION_STATUS: ";
         private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
         private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
+        private static final String CODE = "INSTRUMENTATION_CODE: ";
+        private static final String RESULT = "INSTRUMENTATION_RESULT: ";
         private static final String TIME_REPORT = "Time: ";
     }
     
@@ -90,6 +101,23 @@
         boolean isComplete() {
             return mCode != null && mTestName != null && mTestClass != null;
         }
+        
+        /** Provides a more user readable string for TestResult, if possible */
+        @Override
+        public String toString() {
+            StringBuilder output = new StringBuilder();
+            if (mTestClass != null ) {
+                output.append(mTestClass);
+                output.append('#');
+            }    
+            if (mTestName != null) {
+                output.append(mTestName);
+            }
+            if (output.length() > 0) {
+                return output.toString();
+            }    
+            return "unknown result";
+        }
     }
     
     /** Stores the status values for the test result currently being parsed */
@@ -130,6 +158,8 @@
     public void processNewLines(String[] lines) {
         for (String line : lines) {
             parse(line);
+            // in verbose mode, dump all adb output to log
+            Log.v(LOG_TAG, line);
         }
     }
     
@@ -160,9 +190,15 @@
             // Previous status key-value has been collected. Store it.
             submitCurrentKeyValue();
             parseKey(line, Prefixes.STATUS.length());
-        } else if (line.startsWith(Prefixes.STATUS_FAILED)) {
-            Log.e(LOG_TAG, "test run failed " + line);
-            mTestListener.testRunFailed(line);
+        } else if (line.startsWith(Prefixes.RESULT)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            parseKey(line, Prefixes.RESULT.length());  
+        } else if (line.startsWith(Prefixes.STATUS_FAILED) || 
+                   line.startsWith(Prefixes.CODE)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            // just ignore the remaining data on this line            
         } else if (line.startsWith(Prefixes.TIME_REPORT)) {
             parseTime(line, Prefixes.TIME_REPORT.length());
         } else {
@@ -186,19 +222,19 @@
 
             if (mCurrentKey.equals(StatusKeys.CLASS)) {
                 testInfo.mTestClass = statusValue.trim();
-            }
-            else if (mCurrentKey.equals(StatusKeys.TEST)) {
+            } else if (mCurrentKey.equals(StatusKeys.TEST)) {
                 testInfo.mTestName = statusValue.trim();
-            }
-            else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
+            } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
                 try {
                     testInfo.mNumTests = Integer.parseInt(statusValue);
-                }
-                catch (NumberFormatException e) {
+                } catch (NumberFormatException e) {
                     Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue);
                 }
-            }
-            else if (mCurrentKey.equals(StatusKeys.STACK)) {
+            } else if (mCurrentKey.equals(StatusKeys.ERROR) || 
+                    mCurrentKey.equals(StatusKeys.SHORTMSG)) {
+                // test run must have failed
+                handleTestRunFailed(statusValue); 
+            } else if (mCurrentKey.equals(StatusKeys.STACK)) {
                 testInfo.mStackTrace = statusValue;
             }
 
@@ -229,7 +265,7 @@
         int endKeyPos = line.indexOf('=', keyStartPos);
         if (endKeyPos != -1) {
             mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
-            parseValue(line, endKeyPos+1);
+            parseValue(line, endKeyPos + 1);
         }
     }
     
@@ -252,8 +288,7 @@
         TestResult testInfo = getCurrentTestInfo();
         try {
             testInfo.mCode = Integer.parseInt(value);    
-        }
-        catch (NumberFormatException e) {
+        } catch (NumberFormatException e) {
             Log.e(LOG_TAG, "Expected integer status code, received: " + value);
         }
         
@@ -286,7 +321,7 @@
      */
     private void reportResult(TestResult testInfo) {
         if (!testInfo.isComplete()) {
-            Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
+            Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
             return;
         }
         reportTestRunStarted(testInfo);
@@ -337,8 +372,7 @@
     private String getTrace(TestResult testInfo) {
         if (testInfo.mStackTrace != null) {
             return testInfo.mStackTrace;    
-        }
-        else {
+        } else {
             Log.e(LOG_TAG, "Could not find stack trace for failed test ");
             return new Throwable("Unknown failure").toString();
         }
@@ -351,14 +385,20 @@
         String timeString = line.substring(startPos);
         try {
             float timeSeconds = Float.parseFloat(timeString);
-            mTestTime = (long)(timeSeconds * 1000); 
-        }
-        catch (NumberFormatException e) {
+            mTestTime = (long) (timeSeconds * 1000); 
+        } catch (NumberFormatException e) {
             Log.e(LOG_TAG, "Unexpected time format " + timeString);
         }
     }
     
     /**
+     * Process a instrumentation run failure
+     */
+    private void handleTestRunFailed(String errorMsg) {
+        mTestListener.testRunFailed(errorMsg == null ? "Unknown error" : errorMsg);
+    }
+    
+    /**
      * Called by parent when adb session is complete. 
      */
     @Override
diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
index 4edbbbb..9995426 100644
--- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
+++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
@@ -21,27 +21,35 @@
 import com.android.ddmlib.Log;
 
 import java.io.IOException;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Runs a Android test command remotely and reports results.
  */
 public class RemoteAndroidTestRunner  {
 
-    private static final char CLASS_SEPARATOR = ',';
-    private static final char METHOD_SEPARATOR = '#';
-    private static final char RUNNER_SEPARATOR = '/';
-    private String mClassArg;
     private final String mPackageName;
     private final  String mRunnerName;
-    private String mExtraArgs;
-    private boolean mLogOnlyMode;
     private IDevice mRemoteDevice;
+    /** map of name-value instrumentation argument pairs */
+    private Map<String, String> mArgMap;
     private InstrumentationResultParser mParser;
 
     private static final String LOG_TAG = "RemoteAndroidTest";
-    private static final String DEFAULT_RUNNER_NAME = 
-        "android.test.InstrumentationTestRunner";
-    
+    private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner";
+
+    private static final char CLASS_SEPARATOR = ',';
+    private static final char METHOD_SEPARATOR = '#';
+    private static final char RUNNER_SEPARATOR = '/';
+
+    // defined instrumentation argument names  
+    private static final String CLASS_ARG_NAME = "class";
+    private static final String LOG_ARG_NAME = "log";
+    private static final String DEBUG_ARG_NAME = "debug";
+    private static final String COVERAGE_ARG_NAME = "coverage";
+ 
     /**
      * Creates a remote Android test runner.
      * 
@@ -56,12 +64,10 @@
         
         mPackageName = packageName;
         mRunnerName = runnerName;
-        mRemoteDevice = remoteDevice;  
-        mClassArg = null;
-        mExtraArgs = "";
-        mLogOnlyMode = false;
+        mRemoteDevice = remoteDevice;
+        mArgMap = new Hashtable<String, String>();
     }
-    
+
     /**
      * Alternate constructor. Uses default instrumentation runner.
      * 
@@ -72,7 +78,7 @@
                                    IDevice remoteDevice) {
         this(packageName, null, remoteDevice);
     }
-    
+
     /**
      * Returns the application package name.
      */
@@ -89,14 +95,14 @@
         }
         return mRunnerName;
     }
-    
+
     /**
      * Returns the complete instrumentation component path.
      */
     private String getRunnerPath() {
         return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
     }
-    
+
     /**
      * Sets to run only tests in this class
      * Must be called before 'run'.
@@ -104,7 +110,7 @@
      * @param className fully qualified class name (eg x.y.z)
      */
     public void setClassName(String className) {
-        mClassArg = className;
+        addInstrumentationArg(CLASS_ARG_NAME, className);
     }
 
     /**
@@ -119,15 +125,15 @@
     public void setClassNames(String[] classNames) {
         StringBuilder classArgBuilder = new StringBuilder();
         
-        for (int i=0; i < classNames.length; i++) {
+        for (int i = 0; i < classNames.length; i++) {
             if (i != 0) {
                 classArgBuilder.append(CLASS_SEPARATOR);
             }
             classArgBuilder.append(classNames[i]);
         }
-        mClassArg = classArgBuilder.toString();
+        setClassName(classArgBuilder.toString());
     }
-    
+
     /**
      * Sets to run only specified test method
      * Must be called before 'run'.
@@ -136,47 +142,70 @@
      * @param testName method name
      */
     public void setMethodName(String className, String testName) {
-        mClassArg = className + METHOD_SEPARATOR + testName;
+        setClassName(className + METHOD_SEPARATOR + testName);
     }
-    
+
     /**
-     * Sets extra arguments to include in instrumentation command.
-     * Must be called before 'run'.
+     * Adds a argument to include in instrumentation command.
+     * <p/>
+     * Must be called before 'run'. If an argument with given name has already been provided, it's
+     * value will be overridden. 
      * 
-     * @param instrumentationArgs must not be null
+     * @param name the name of the instrumentation bundle argument
+     * @param value the value of the argument
      */
-    public void setExtraArgs(String instrumentationArgs) {
-        if (instrumentationArgs == null) {
-            throw new IllegalArgumentException("instrumentationArgs cannot be null");
+    public void addInstrumentationArg(String name, String value) {
+        if (name == null || value == null) {
+            throw new IllegalArgumentException("name or value arguments cannot be null");
         }
-        mExtraArgs = instrumentationArgs;  
+        mArgMap.put(name, value);  
     }
-    
+
     /**
-     * Returns the extra instrumentation arguments.
+     * Adds a boolean argument to include in instrumentation command.
+     * <p/>
+     * @see RemoteAndroidTestRunner#addInstrumentationArg
+     * 
+     * @param name the name of the instrumentation bundle argument
+     * @param value the value of the argument
      */
-    public String getExtraArgs() {
-        return mExtraArgs;
+    public void addBooleanArg(String name, boolean value) {
+        addInstrumentationArg(name, Boolean.toString(value));
     }
-    
+  
     /**
      * Sets this test run to log only mode - skips test execution.
      */
     public void setLogOnly(boolean logOnly) {
-        mLogOnlyMode = logOnly;
+        addBooleanArg(LOG_ARG_NAME, logOnly);
     }
-    
+
+    /**
+     * Sets this debug mode of this test run. If true, the Android test runner will wait for a 
+     * debugger to attach before proceeding with test execution.
+     */
+    public void setDebug(boolean debug) {
+        addBooleanArg(DEBUG_ARG_NAME, debug);
+    }
+
+    /**
+     * Sets this code coverage mode of this test run. 
+     */
+    public void setCoverage(boolean coverage) {
+        addBooleanArg(COVERAGE_ARG_NAME, coverage);
+    }
+
     /**
      * Execute this test run.
      * 
      * @param listener listens for test results
      */
     public void run(ITestRunListener listener) {
-        final String runCaseCommandStr = "am instrument -w -r "
-            + getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath();
+        final String runCaseCommandStr = String.format("am instrument -w -r %s %s",
+            getArgsCommand(), getRunnerPath());
         Log.d(LOG_TAG, runCaseCommandStr);
         mParser = new InstrumentationResultParser(listener);
-        
+
         try {
             mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser);
         } catch (IOException e) {
@@ -184,7 +213,7 @@
             listener.testRunFailed(e.toString());
         }
     }
-    
+
     /**
      * Requests cancellation of this test run.
      */
@@ -193,36 +222,19 @@
             mParser.cancel();
         }
     }
-    
-    /**
-     * Returns the test class argument.
-     */
-    private String getClassArg() {
-        return mClassArg;
-    }
-    
-    /**
-     * Returns the full instrumentation command which specifies the test classes to execute. 
-     * Returns an empty string if no classes were specified.
-     */
-    private String getClassCmd() {
-        String classArg = getClassArg();
-        if (classArg != null) {
-            return "-e class " + classArg;
-        }
-        return "";
-    }
 
     /**
-     * Returns the full command to enable log only mode - if specified. Otherwise returns an 
-     * empty string.
+     * Returns the full instrumentation command line syntax for the provided instrumentation 
+     * arguments.  
+     * Returns an empty string if no arguments were specified.
      */
-    private String getLogCmd() {
-        if (mLogOnlyMode) {
-            return "-e log true";
+    private String getArgsCommand() {
+        StringBuilder commandBuilder = new StringBuilder();
+        for (Entry<String, String> argPair : mArgMap.entrySet()) {
+            final String argCmd = String.format(" -e %s %s", argPair.getKey(),
+                    argPair.getValue());
+            commandBuilder.append(argCmd);
         }
-        else {
-            return "";
-        }
+        return commandBuilder.toString();
     }
 }
diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
index 77d10c1..7742dd6 100644
--- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
+++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
@@ -103,9 +103,43 @@
         injectTestString(timeString);
         assertEquals(4900, mTestResult.mTestTime);
     }
+    
+    /**
+     * Test basic parsing of a test run failure.
+     */
+    public void testRunFailed() {
+        StringBuilder output = new StringBuilder();        
+        final String errorMessage = "Unable to find instrumentation info";
+        addStatusKey(output, "Error", errorMessage);
+        addStatusCode(output, "-1");
+        output.append("INSTRUMENTATION_FAILED: com.dummy/android.test.InstrumentationTestRunner");
+        addLineBreak(output);
+        
+        injectTestString(output.toString());
+        
+        assertEquals(errorMessage, mTestResult.mRunFailedMessage);
+    }
+    
+    /**
+     * Test parsing of a test run failure, where an instrumentation component failed to load
+     * Parsing input takes the from of INSTRUMENTATION_RESULT: fff
+     */
+    public void testRunFailedResult() {
+        StringBuilder output = new StringBuilder();        
+        final String errorMessage = "Unable to instantiate instrumentation";
+        output.append("INSTRUMENTATION_RESULT: shortMsg=");
+        output.append(errorMessage);
+        addLineBreak(output);
+        output.append("INSTRUMENTATION_CODE: 0");
+        addLineBreak(output);
+        
+        injectTestString(output.toString());
+        
+        assertEquals(errorMessage, mTestResult.mRunFailedMessage);
+    }
 
     /**
-     * builds a common test result using TEST_NAME and TEST_CLASS.
+     * Builds a common test result using TEST_NAME and TEST_CLASS.
      */
     private StringBuilder buildCommonResult() {
         StringBuilder output = new StringBuilder();
@@ -146,6 +180,13 @@
         outputBuilder.append(key);
         outputBuilder.append('=');
         outputBuilder.append(value);
+        addLineBreak(outputBuilder);
+    }
+
+    /**
+     * Append line break characters to output
+     */
+    private void addLineBreak(StringBuilder outputBuilder) {
         outputBuilder.append("\r\n");
     }
 
@@ -164,7 +205,7 @@
     private void addStatusCode(StringBuilder outputBuilder, String value) {
         outputBuilder.append("INSTRUMENTATION_STATUS_CODE: ");
         outputBuilder.append(value);
-        outputBuilder.append("\r\n");
+        addLineBreak(outputBuilder);
     }
 
     /**
@@ -197,11 +238,14 @@
         TestFailure mTestStatus;
         String mTrace;
         boolean mStopped;
+        /** stores the error message provided to testRunFailed */
+        String mRunFailedMessage;
 
         VerifyingTestResult() {
             mNumTestsRun = 0;
             mTestStatus = null;
             mStopped = false;
+            mRunFailedMessage = null;
         }
 
         public void testEnded(TestIdentifier test) {
@@ -238,8 +282,7 @@
         }
 
         public void testRunFailed(String errorMessage) {
-            // ignored
+            mRunFailedMessage = errorMessage;
         }
     }
-
 }
diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
index 9acaaf9..6a653ad 100644
--- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
+++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
@@ -17,18 +17,17 @@
 package com.android.ddmlib.testrunner;
 
 import com.android.ddmlib.Client;
+import com.android.ddmlib.Device.DeviceState;
 import com.android.ddmlib.FileListingService;
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.log.LogReceiver;
 import com.android.ddmlib.RawImage;
 import com.android.ddmlib.SyncService;
-import com.android.ddmlib.Device.DeviceState;
-import com.android.ddmlib.log.LogReceiver;
-
-import junit.framework.TestCase;
 
 import java.io.IOException;
 import java.util.Map;
+import junit.framework.TestCase;
 
 /**
  * Tests RemoteAndroidTestRunner.
@@ -82,14 +81,15 @@
     }
 
     /**
-     * Test the building of the instrumentation runner command with extra args set.
+     * Test the building of the instrumentation runner command with extra argument added.
      */
-    public void testRunWithExtraArgs() {
-        final String extraArgs = "blah";
-        mRunner.setExtraArgs(extraArgs);
+    public void testRunWithAddInstrumentationArg() {
+        final String extraArgName = "blah";
+        final String extraArgValue = "blahValue";
+        mRunner.addInstrumentationArg(extraArgName, extraArgValue);
         mRunner.run(new EmptyListener());
-        assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs,
-                TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+        assertStringsEquals(String.format("am instrument -w -r -e %s %s %s/%s", extraArgName, 
+                extraArgValue, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
     }
 
 
@@ -243,6 +243,5 @@
         public void testStarted(TestIdentifier test) {
             // ignore
         }
-
     }
 }
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
index 3750f66..55c18bb 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
@@ -40,7 +40,8 @@
  org.eclipse.wst.sse.ui,
  org.eclipse.wst.xml.core,
  org.eclipse.wst.xml.ui,
- org.eclipse.jdt.junit
+ org.eclipse.jdt.junit,
+ org.eclipse.jdt.junit.runtime
 Eclipse-LazyStart: true
 Export-Package: com.android.ide.eclipse.adt,
  com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests",
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index 39e6dd5..c18c72f 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -510,4 +510,59 @@
              type="org.eclipse.jdt.junit.launchconfig">
        </launchDelegate>
    </extension>
+   <extension
+         point="org.eclipse.debug.core.launchConfigurationTypes">
+      <launchConfigurationType
+            delegate="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchConfigDelegate"
+            id="com.android.ide.eclipse.adt.junit.launchConfigurationType"
+            modes="run,debug"
+            name="Android Instrumentation"
+            public="true"
+            sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"
+            sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer">
+      </launchConfigurationType>
+   </extension>
+   <extension
+         point="org.eclipse.debug.ui.launchConfigurationTypeImages">
+      <launchConfigurationTypeImage
+            configTypeID="com.android.ide.eclipse.adt.junit.launchConfigurationType"
+            icon="icons/android.png"
+            id="com.android.ide.eclipse.adt.junit.launchConfigurationTypeImage">
+      </launchConfigurationTypeImage>
+   </extension>
+   <extension
+         point="org.eclipse.debug.ui.launchConfigurationTabGroups">
+      <launchConfigurationTabGroup
+            class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitTabGroup"
+            description="Android Instrumentation"
+            id="com.android.ide.eclipse.adt.junit.AndroidJUnitLaunchConfigTabGroup"
+            type="com.android.ide.eclipse.adt.junit.launchConfigurationType"/>
+   </extension>
+   <extension
+         point="org.eclipse.debug.ui.launchShortcuts">
+      <shortcut
+            class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchShortcut"
+            icon="icons/android.png"
+            id="com.android.ide.eclipse.adt.junit.launchShortcut"
+            label="Android Instrumentation"
+            modes="run,debug">
+          <contextualLaunch>
+            <enablement>
+               <with variable="selection">
+                  <count value="1"/>
+                  <iterate>
+                      <adapt type="org.eclipse.jdt.core.IJavaElement">
+                        <test property="org.eclipse.jdt.core.isInJavaProjectWithNature" value="com.android.ide.eclipse.adt.AndroidNature"/>
+                        <test property="org.eclipse.jdt.core.hasTypeOnClasspath" value="junit.framework.Test"/>
+                        <test property="org.eclipse.jdt.junit.canLaunchAsJUnit" forcePluginActivation="true"/>
+                     </adapt>
+                  </iterate>
+               </with>
+            </enablement>
+         </contextualLaunch>
+         <configurationType
+             id="com.android.ide.eclipse.adt.junit.launchConfigurationType">
+         </configurationType>
+      </shortcut>
+   </extension>
 </plugin>
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
index 48a21d1..e0708f3 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
@@ -1040,6 +1040,16 @@
                             mSdkIsLoaded = LoadStatus.LOADED;
 
                             progress.setTaskName("Check Projects");
+                            
+                            ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
+                            for (IJavaProject javaProject : mPostLoadProjectsToResolve) {
+                                if (javaProject.getProject().isOpen()) {
+                                    list.add(javaProject);
+                                }
+                            }
+
+                            // done with this list.
+                            mPostLoadProjectsToResolve.clear();
 
                             // check the projects that need checking.
                             // The method modifies the list (it removes the project that
@@ -1047,14 +1057,13 @@
                             AndroidClasspathContainerInitializer.checkProjectsCache(
                                     mPostLoadProjectsToCheck);
                             
-                            mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck);
+                            list.addAll(mPostLoadProjectsToCheck);
                             
                             // update the project that needs recompiling.
-                            if (mPostLoadProjectsToResolve.size() > 0) {
-                                IJavaProject[] array = mPostLoadProjectsToResolve.toArray(
-                                        new IJavaProject[mPostLoadProjectsToResolve.size()]);
+                            if (list.size() > 0) {
+                                IJavaProject[] array = list.toArray(
+                                        new IJavaProject[list.size()]);
                                 AndroidClasspathContainerInitializer.updateProjects(array);
-                                mPostLoadProjectsToResolve.clear();
                             }
                             
                             progress.worked(10);
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
index c508283..8aa1aba 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
@@ -222,6 +222,7 @@
             
             PreCompilerDeltaVisitor dv = null;
             String javaPackage = null;
+            int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
     
             if (kind == FULL_BUILD) {
                 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
@@ -253,6 +254,7 @@
                     
                     // get the java package from the visitor
                     javaPackage = dv.getManifestPackage();
+                    minSdkVersion = dv.getMinSdkVersion();
                 }
             }
     
@@ -276,7 +278,7 @@
             if (manifest == null) {
                 String msg = String.format(Messages.s_File_Missing,
                         AndroidConstants.FN_ANDROID_MANIFEST);
-                AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+                AdtPlugin.printErrorToConsole(project, msg);
                 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
     
                 // This interrupts the build. The next builders will not run.
@@ -304,19 +306,34 @@
                 
                 // get the java package from the parser
                 javaPackage = parser.getPackage();
+                minSdkVersion = parser.getApiLevelRequirement();
             }
-    
-            if (javaPackage == null || javaPackage.length() == 0) {
-                // looks like the AndroidManifest file isn't valid.
-                String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
-                        AndroidConstants.FN_ANDROID_MANIFEST);
-                AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
-                        msg);
+
+            if (minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK &&
+                    minSdkVersion < projectTarget.getApiVersionNumber()) {
+                // check it against the target api level
+                String msg = String.format(
+                        "Manifest min SDK version (%1$d) is lower than project target API level (%2$d)",
+                        minSdkVersion, projectTarget.getApiVersionNumber());
+                AdtPlugin.printErrorToConsole(project, msg);
+                BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg,
+                        IMarker.SEVERITY_ERROR);
     
                 // This interrupts the build. The next builders will not run.
                 stopBuild(msg);
             }
+
+            if (javaPackage == null || javaPackage.length() == 0) {
+                // looks like the AndroidManifest file isn't valid.
+                String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
+                        AndroidConstants.FN_ANDROID_MANIFEST);
+                AdtPlugin.printErrorToConsole(project, msg);
+                markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
     
+                // This interrupts the build. The next builders will not run.
+                stopBuild(msg);
+            }
+            
             // at this point we have the java package. We need to make sure it's not a different
             // package than the previous one that were built.
             if (javaPackage.equals(mManifestPackage) == false) {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
index 6841830..29942e8 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
@@ -72,8 +72,10 @@
     /** Manifest check/parsing flag. */
     private boolean mCheckedManifestXml = false;
 
-    /** Application Pacakge, gathered from the parsing of the manifest */
+    /** Application Package, gathered from the parsing of the manifest */
     private String mJavaPackage = null;
+    /** minSDKVersion attribute value, gathered from the parsing of the manifest */
+    private int mMinSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
 
     // Internal usage fields.
     /**
@@ -137,6 +139,22 @@
         return mJavaPackage;
     }
 
+    /**
+     * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
+     * <p/>
+     * This can return {@link AndroidManifestParser#INVALID_MIN_SDK} in two cases:
+     * <ul>
+     * <li>The manifest was not part of the resource change delta, and the manifest was
+     * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
+     * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
+     * but the package declaration is missing</li>
+     * </ul>
+     * @return the minSdkVersion or {@link AndroidManifestParser#INVALID_MIN_SDK}.
+     */
+    public int getMinSdkVersion() {
+        return mMinSdkVersion;
+    }
+
     /*
      * (non-Javadoc)
      *
@@ -184,6 +202,7 @@
                     
                     if (parser != null) {
                         mJavaPackage = parser.getPackage();
+                        mMinSdkVersion = parser.getApiLevelRequirement();
                     }
 
                     mCheckedManifestXml = true;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
index 88ee8b6..499cca7 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
@@ -307,7 +307,8 @@
      *      <code>DEBUG_MODE</code>.
      * @param apk the resource to the apk to launch.
      * @param debuggable the debuggable value of the app, or null if not set.
-     * @param requiredApiVersionNumber the api version required by the app, or -1 if none.
+     * @param requiredApiVersionNumber the api version required by the app, or
+     * {@link AndroidManifestParser#INVALID_MIN_SDK} if none.
      * @param launchAction the action to perform after app sync
      * @param config the launch configuration
      * @param launch the launch object
@@ -638,20 +639,21 @@
             
             String deviceApiVersionName = device.getProperty(IDevice.PROP_BUILD_VERSION);
             String value = device.getProperty(IDevice.PROP_BUILD_VERSION_NUMBER);
-            int deviceApiVersionNumber = 0;
+            int deviceApiVersionNumber = AndroidManifestParser.INVALID_MIN_SDK;
             try {
                 deviceApiVersionNumber = Integer.parseInt(value);
             } catch (NumberFormatException e) {
                 // pass, we'll keep the deviceVersionNumber value at 0.
             }
             
-            if (launchInfo.getRequiredApiVersionNumber() == 0) {
+            if (launchInfo.getRequiredApiVersionNumber() == AndroidManifestParser.INVALID_MIN_SDK) {
                 // warn the API level requirement is not set.
                 AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                         "WARNING: Application does not specify an API level requirement!");
 
                 // and display the target device API level (if known)
-                if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+                if (deviceApiVersionName == null ||
+                        deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                             "WARNING: Unknown device API version!");
                 } else {
@@ -660,7 +662,8 @@
                             deviceApiVersionName));
                 }
             } else { // app requires a specific API level
-                if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+                if (deviceApiVersionName == null ||
+                        deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
                     AdtPlugin.printToConsole(launchInfo.getProject(),
                             "WARNING: Unknown device API version!");
                 } else if (deviceApiVersionNumber < launchInfo.getRequiredApiVersionNumber()) {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
index a59518c..7dae56d 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
@@ -16,12 +16,13 @@
 
 package com.android.ide.eclipse.adt.launch;
 
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.runtime.IProgressMonitor;
 
-import com.android.ddmlib.IDevice;
-
 /**
  * A delayed launch waiting for a device to be present or ready before the
  * application is launched.
@@ -50,7 +51,8 @@
     /** debuggable attribute of the manifest file. */
     private final Boolean mDebuggable;
     
-    /** Required ApiVersionNumber by the app. 0 means no requirements */
+    /** Required ApiVersionNumber by the app. {@link AndroidManifestParser#INVALID_MIN_SDK} means
+     * no requirements */
     private final int mRequiredApiVersionNumber;
     
     private InstallRetryMode mRetryMode = InstallRetryMode.NEVER;
@@ -81,7 +83,8 @@
      * @param launchAction action to perform after app install
      * @param pack IFile to the package (.apk) file
      * @param debuggable debuggable attribute of the app's manifest file.
-     * @param requiredApiVersionNumber required SDK version by the app. 0 means no requirements.
+     * @param requiredApiVersionNumber required SDK version by the app.
+     * {@link AndroidManifestParser#INVALID_MIN_SDK} means no requirements.
      * @param launch the launch object
      * @param monitor progress monitor for launch
      */
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
index 599da5f..30b0723 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
@@ -55,6 +55,11 @@
  */
 public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
 
+    /**
+     * 
+     */
+    public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab.png";
+
     protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
     
     protected Text mProjText;
@@ -194,7 +199,7 @@
 
     @Override
     public Image getImage() {
-        return AdtPlugin.getImageLoader().loadImage("mainLaunchTab.png", null);
+        return AdtPlugin.getImageLoader().loadImage(LAUNCH_TAB_IMAGE, null);
     }
 
 
@@ -310,21 +315,8 @@
         }
         mProjText.setText(projectName);
 
-        // get the list of projects
-        IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
-
-        if (projects != null) {
-            // look for the currently selected project
-            IProject proj = null;
-            for (IJavaProject p : projects) {
-                if (p.getElementName().equals(projectName)) {
-                    proj = p.getProject();
-                    break;
-                }
-            }
-
-            loadActivities(proj);
-        }
+        IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+        loadActivities(proj);
         
         // load the launch action.
         mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java
new file mode 100644
index 0000000..4dfe37d
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit;
+
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo;
+import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.launch.junit.runtime.AndroidJUnitLaunchInfo;
+import com.android.ide.eclipse.adt.launch.junit.runtime.RemoteADTTestRunner;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IProcess;
+import org.eclipse.debug.core.model.IStreamsProxy;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
+import org.eclipse.jdt.launching.IVMRunner;
+import org.eclipse.jdt.launching.VMRunnerConfiguration;
+
+/**
+ * A launch action that executes a instrumentation test run on an Android device.
+ */
+class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
+
+    private String mTestPackage;
+    private String mRunner;
+    
+    /**
+     * Creates a AndroidJUnitLaunchAction.
+     * 
+     * @param testPackage the Android application package that contains the tests to run 
+     * @param runner the InstrumentationTestRunner that will execute the tests
+     */
+    public AndroidJUnitLaunchAction(String testPackage, String runner) {
+        mTestPackage = testPackage;
+        mRunner = runner;
+    }
+    
+    /**
+     * Launch a instrumentation test run on given Android device. 
+     * Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
+     * 
+     * @see com.android.ide.eclipse.adt.launch.IAndroidLaunchAction#doLaunchAction(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo, com.android.ddmlib.Device)
+     */
+    public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
+        String msg = String.format("Launching instrumentation %s on device %s", mRunner,
+                device.getSerialNumber());
+        AdtPlugin.printToConsole(info.getProject(), msg);
+        
+        try {
+           JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(info, device);
+           final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE : 
+               ILaunchManager.RUN_MODE; 
+           junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
+                   info.getMonitor());
+           
+           // TODO: need to add AMReceiver-type functionality somewhere
+        } catch (CoreException e) {
+            AdtPlugin.printErrorToConsole(info.getProject(), "Failed to launch test");
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getLaunchDescription() {
+        return String.format("%s JUnit launch", mRunner);
+    }
+
+    /**
+     * Extends the JDT JUnit launch delegate to allow for JUnit UI reuse. 
+     */
+    private class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
+        
+        private IDevice mDevice;
+        private DelayedLaunchInfo mLaunchInfo;
+
+        public JUnitLaunchDelegate(DelayedLaunchInfo info, IDevice device) {
+            mLaunchInfo = info;
+            mDevice = device;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
+         */
+        @Override
+        public synchronized void launch(ILaunchConfiguration configuration, String mode,
+                ILaunch launch, IProgressMonitor monitor) throws CoreException {
+            // TODO: is progress monitor adjustment needed here?
+            super.launch(configuration, mode, launch, monitor);
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
+         */
+        @Override
+        public String verifyMainTypeName(ILaunchConfiguration configuration) throws CoreException {
+            return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
+        }
+
+        /**
+         * Overrides parent to return a VM Runner implementation which launches a thread, rather
+         * than a separate VM process
+         */
+        @Override
+        public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode) 
+                throws CoreException {
+            return new VMTestRunner(new AndroidJUnitLaunchInfo(mLaunchInfo.getProject(), 
+                    mTestPackage, mRunner, mLaunchInfo.isDebugMode(), mDevice));
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
+         */
+        @Override
+        public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
+                throws CoreException {
+            return mLaunchInfo.getLaunch();
+        }     
+    }
+
+    /**
+     * Provides a VM runner implementation which starts a thread implementation of a launch process
+     */
+    private static class VMTestRunner implements IVMRunner {
+        
+        private final AndroidJUnitLaunchInfo mJUnitInfo;
+        
+        VMTestRunner(AndroidJUnitLaunchInfo info) {
+            mJUnitInfo = info;
+        }
+
+        public void run(final VMRunnerConfiguration config, ILaunch launch,
+                IProgressMonitor monitor) throws CoreException {
+            
+            TestRunnerProcess runnerProcess = 
+                new TestRunnerProcess(config, launch, mJUnitInfo);
+            runnerProcess.start();
+            launch.addProcess(runnerProcess);
+        }
+    }
+
+    /**
+     * Launch process that executes the tests.
+     */
+    private static class TestRunnerProcess extends Thread implements IProcess  {
+
+        private final VMRunnerConfiguration mRunConfig;
+        private final ILaunch mLaunch;
+        private final AndroidJUnitLaunchInfo mJUnitInfo;
+        private RemoteADTTestRunner mTestRunner = null;
+        private boolean mIsTerminated = false;
+        
+        TestRunnerProcess(VMRunnerConfiguration runConfig, ILaunch launch,
+                AndroidJUnitLaunchInfo info) {
+            mRunConfig = runConfig;
+            mLaunch = launch;
+            mJUnitInfo = info;
+        }
+        
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
+         */
+        public String getAttribute(String key) {
+            return null;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#getExitValue()
+         */
+        public int getExitValue() throws DebugException {
+            return 0;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#getLabel()
+         */
+        public String getLabel() {
+            return mLaunch.getLaunchMode();
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#getLaunch()
+         */
+        public ILaunch getLaunch() {
+            return mLaunch;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
+         */
+        public IStreamsProxy getStreamsProxy() {
+            return null;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String, 
+         * java.lang.String)
+         */
+        public void setAttribute(String key, String value) {
+            // ignore           
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
+         */
+        @SuppressWarnings("unchecked")
+        public Object getAdapter(Class adapter) {
+            return null;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
+         */
+        public boolean canTerminate() {
+            return true;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
+         */
+        public boolean isTerminated() {
+            return mIsTerminated;
+        }
+
+        /* (non-Javadoc)
+         * @see org.eclipse.debug.core.model.ITerminate#terminate()
+         */
+        public void terminate() throws DebugException {
+            if (mTestRunner != null) {
+                mTestRunner.terminate();
+            }    
+            mIsTerminated = true;
+        } 
+
+        /**
+         * Launches a test runner that will communicate results back to JDT JUnit UI
+         */
+        @Override
+        public void run() {
+            mTestRunner = new RemoteADTTestRunner();
+            mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
+        }
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java
new file mode 100755
index 0000000..05cc6ae
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.launch.AndroidLaunch;
+import com.android.ide.eclipse.adt.launch.AndroidLaunchController;
+import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.launch.LaunchConfigDelegate;
+import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+
+/**
+ * Run configuration that can execute JUnit tests on an Android platform
+ * <p/>
+ * Will deploy apps on target Android platform by reusing functionality from ADT 
+ * LaunchConfigDelegate, and then run JUnits tests by reusing functionality from JDT 
+ * JUnitLaunchConfigDelegate.
+ */
+@SuppressWarnings("restriction") //$NON-NLS-1$
+public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
+
+    /** Launch config attribute that stores instrumentation runner */
+    static final String ATTR_INSTR_NAME = AdtPlugin.PLUGIN_ID + ".instrumentation"; //$NON-NLS-1$
+    private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+    @Override
+    protected void doLaunch(final ILaunchConfiguration configuration, final String mode,
+            IProgressMonitor monitor, IProject project, final AndroidLaunch androidLaunch,
+            AndroidLaunchConfiguration config, AndroidLaunchController controller,
+            IFile applicationPackage, AndroidManifestParser manifestParser) {
+
+        String testPackage = manifestParser.getPackage();        
+        String runner = getRunnerFromConfig(configuration);
+        if (runner == null) {
+            AdtPlugin.displayError("Android Launch",
+                    "An instrumention test runner is not specified!");
+            androidLaunch.stopLaunch();
+            return;
+        }
+
+        IAndroidLaunchAction junitLaunch = new AndroidJUnitLaunchAction(testPackage, runner);
+
+        controller.launch(project, mode, applicationPackage, manifestParser.getPackage(),
+                manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(),
+                junitLaunch, config, androidLaunch, monitor);
+    }
+
+    private String getRunnerFromConfig(ILaunchConfiguration configuration) {
+        String runner = EMPTY_STRING;
+        try {
+            runner = configuration.getAttribute(ATTR_INSTR_NAME, EMPTY_STRING);
+        } catch (CoreException e) {
+            AdtPlugin.log(e, "Error when retrieving instrumentation info from launch config"); //$NON-NLS-1$           
+        }
+        if (runner.length() < 1) {
+            return null;
+        }
+        return runner;
+    }
+
+    /**
+     * Helper method to return the set of instrumentations for the Android project
+     * 
+     * @param project the {@link IProject} to get instrumentations for
+     * @return null if no error occurred parsing instrumentations
+     */
+    static String[] getInstrumentationsForProject(IProject project) {
+        if (project != null) {
+            try {
+                // parse the manifest for the list of instrumentations
+                AndroidManifestParser manifestParser = AndroidManifestParser.parse(
+                        BaseProjectHelper.getJavaProject(project), null /* errorListener */,
+                        true /* gatherData */, false /* markErrors */);
+                if (manifestParser != null) {
+                    return manifestParser.getInstrumentations(); 
+                }
+            } catch (CoreException e) {
+                AdtPlugin.log(e, "%s: Error parsing AndroidManifest.xml",  //$NON-NLS-1$ 
+                        project.getName());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Helper method to set JUnit-related attributes expected by JDT JUnit runner
+     * 
+     * @param config the launch configuration to modify
+     */
+    static void setJUnitDefaults(ILaunchConfigurationWorkingCopy config) {
+        // set the test runner to JUnit3 to placate JDT JUnit runner logic
+        config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND, 
+                TestKindRegistry.JUNIT3_TEST_KIND_ID);
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java
new file mode 100644
index 0000000..5fbda98
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.launch.MainLaunchConfigTab;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.ISourceReference;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.Messages;
+import org.eclipse.jdt.internal.junit.launcher.ITestKind;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.JUnitMigrationDelegate;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.internal.junit.launcher.TestSelectionDialog;
+import org.eclipse.jdt.internal.junit.ui.JUnitMessages;
+import org.eclipse.jdt.internal.junit.util.LayoutUtil;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator;
+import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.ui.JavaElementComparator;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
+import org.eclipse.ui.dialogs.SelectionDialog;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * The launch config UI tab for Android JUnit
+ * <p/>
+ * Based on org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationTab
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab {
+
+    // Project UI widgets
+    private Label mProjLabel;
+    private Text mProjText;
+    private Button mProjButton;
+    
+    // Test class UI widgets
+    private Text mTestText;
+    private Button mSearchButton;
+    private String mOriginalTestMethodName;
+    private Label mTestMethodLabel;
+    private Text mContainerText;
+    private IJavaElement mContainerElement;
+    private final ILabelProvider mJavaElementLabelProvider = new JavaElementLabelProvider();
+
+    private Button mContainerSearchButton;
+    private Button mTestContainerRadioButton;
+    private Button mTestRadioButton;
+    private Label mTestLabel; 
+
+    // Android specific members
+    private Image mTabIcon = null;
+    private Combo mInstrumentationCombo;
+    private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+    private String[] mInstrumentations = null;
+    private ProjectChooserHelper mProjectChooserHelper;
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
+     */
+    public void createControl(Composite parent) {
+        mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
+
+        Composite comp = new Composite(parent, SWT.NONE);
+        setControl(comp);
+
+        GridLayout topLayout = new GridLayout();
+        topLayout.numColumns = 3;
+        comp.setLayout(topLayout);      
+        
+        createSingleTestSection(comp);
+        createTestContainerSelectionGroup(comp);
+        
+        createSpacer(comp);
+        
+        createInstrumentationGroup(comp);
+
+        createSpacer(comp);
+        
+        Dialog.applyDialogFont(comp);
+        // TODO: add help link here when available
+        //PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), 
+        //      IJUnitHelpContextIds.LAUNCH_CONFIGURATION_DIALOG_JUNIT_MAIN_TAB);
+        validatePage();
+    }
+
+
+    private void createSpacer(Composite comp) {
+        Label label = new Label(comp, SWT.NONE);
+        GridData gd = new GridData();
+        gd.horizontalSpan = 3;
+        label.setLayoutData(gd);
+    }
+    
+    private void createSingleTestSection(Composite comp) {
+        mTestRadioButton = new Button(comp, SWT.RADIO);
+        mTestRadioButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_oneTest); 
+        GridData gd = new GridData();
+        gd.horizontalSpan = 3;
+        mTestRadioButton.setLayoutData(gd); 
+        mTestRadioButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mTestRadioButton.getSelection()) {
+                    testModeChanged();
+                }    
+            }
+        });
+        
+        mProjLabel = new Label(comp, SWT.NONE);
+        mProjLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_project); 
+        gd = new GridData();
+        gd.horizontalIndent = 25;
+        mProjLabel.setLayoutData(gd);
+        
+        mProjText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+        mProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mProjText.addModifyListener(new ModifyListener() {
+            public void modifyText(ModifyEvent evt) {
+                validatePage();
+                updateLaunchConfigurationDialog();              
+                mSearchButton.setEnabled(mTestRadioButton.getSelection() && 
+                        mProjText.getText().length() > 0);
+            }
+        });
+            
+        mProjButton = new Button(comp, SWT.PUSH);
+        mProjButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_browse); 
+        mProjButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent evt) {
+                handleProjectButtonSelected();
+            }
+        });
+        setButtonGridData(mProjButton);
+        
+        mTestLabel = new Label(comp, SWT.NONE);
+        gd = new GridData();
+        gd.horizontalIndent = 25;
+        mTestLabel.setLayoutData(gd);
+        mTestLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_test); 
+        
+    
+        mTestText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+        mTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTestText.addModifyListener(new ModifyListener() {
+            public void modifyText(ModifyEvent evt) {
+                validatePage();
+                updateLaunchConfigurationDialog();
+            }
+        });
+        
+        mSearchButton = new Button(comp, SWT.PUSH);
+        mSearchButton.setEnabled(mProjText.getText().length() > 0);
+        mSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search); 
+        mSearchButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent evt) {
+                handleSearchButtonSelected();
+            }
+        });
+        setButtonGridData(mSearchButton);
+        
+        new Label(comp, SWT.NONE);
+        
+        mTestMethodLabel = new Label(comp, SWT.NONE);
+        mTestMethodLabel.setText("");  //$NON-NLS-1$
+        gd = new GridData();
+        gd.horizontalSpan = 2;
+        mTestMethodLabel.setLayoutData(gd);
+    }
+
+    private void createTestContainerSelectionGroup(Composite comp) {
+        mTestContainerRadioButton = new Button(comp, SWT.RADIO);
+        mTestContainerRadioButton.setText(
+                JUnitMessages.JUnitLaunchConfigurationTab_label_containerTest); 
+        GridData gd = new GridData();
+        gd.horizontalSpan = 3;
+        mTestContainerRadioButton.setLayoutData(gd);
+        mTestContainerRadioButton.addSelectionListener(new SelectionListener() {
+            public void widgetSelected(SelectionEvent e) {
+                if (mTestContainerRadioButton.getSelection()) {
+                    testModeChanged();
+                }    
+            }
+            public void widgetDefaultSelected(SelectionEvent e) {
+            }
+        });
+        
+        mContainerText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.READ_ONLY);
+        gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.horizontalIndent = 25;
+        gd.horizontalSpan = 2;
+        mContainerText.setLayoutData(gd);
+        mContainerText.addModifyListener(new ModifyListener() {
+            public void modifyText(ModifyEvent evt) {
+                updateLaunchConfigurationDialog();
+            }
+        });
+        
+        mContainerSearchButton = new Button(comp, SWT.PUSH);
+        mContainerSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search); 
+        mContainerSearchButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent evt) {
+                handleContainerSearchButtonSelected();
+            }
+        });
+        setButtonGridData(mContainerSearchButton);  
+    }
+    
+    private void createInstrumentationGroup(Composite comp) {
+        Label loaderLabel = new Label(comp, SWT.NONE);
+        loaderLabel.setText("Instrumentation runner:");
+        GridData gd = new GridData();
+        gd.horizontalIndent = 0;
+        loaderLabel.setLayoutData(gd);
+        
+        mInstrumentationCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+        gd = new GridData(GridData.FILL_HORIZONTAL);
+        mInstrumentationCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mInstrumentationCombo.clearSelection();
+        mInstrumentationCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                validatePage();
+                updateLaunchConfigurationDialog();
+            }
+        });
+    }
+
+    private void handleContainerSearchButtonSelected() {
+        IJavaElement javaElement = chooseContainer(mContainerElement);
+        if (javaElement != null) {
+            setContainerElement(javaElement);
+        }    
+    }
+
+    private void setContainerElement(IJavaElement javaElement) {
+        mContainerElement = javaElement;
+        mContainerText.setText(getPresentationName(javaElement));
+        validatePage();
+        updateLaunchConfigurationDialog();
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+     */
+    public void initializeFrom(ILaunchConfiguration config) {
+        String projectName = updateProjectFromConfig(config);
+        String containerHandle = EMPTY_STRING;
+        try {
+            containerHandle = config.getAttribute(
+                    JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+        } catch (CoreException ce) {
+            // ignore
+        }
+        
+        if (containerHandle.length() > 0) {
+            updateTestContainerFromConfig(config);
+        } else {
+            updateTestTypeFromConfig(config);
+        }    
+
+        IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+        loadInstrumentations(proj);
+        updateInstrumentationFromConfig(config);
+        
+        validatePage();
+    }
+
+    private void updateInstrumentationFromConfig(ILaunchConfiguration config) {
+        boolean found = false;
+        try {
+            String currentInstrumentation = config.getAttribute(
+                    AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, EMPTY_STRING);
+            if (mInstrumentations != null) {
+                // look for the name of the instrumentation in the combo.
+                for (int i = 0; i < mInstrumentations.length; i++) {
+                   if (currentInstrumentation.equals(mInstrumentations[i])) {
+                       found = true;
+                       mInstrumentationCombo.select(i);
+                       break;
+                    }
+                }
+            }    
+        } catch (CoreException ce) {
+            // ignore
+        }
+        if (!found) {
+            mInstrumentationCombo.clearSelection();
+        }    
+    }
+
+    private String updateProjectFromConfig(ILaunchConfiguration config) {
+        String projectName = EMPTY_STRING;
+        try {
+            projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+                    EMPTY_STRING);
+        } catch (CoreException ce) {
+            // ignore
+        }
+        mProjText.setText(projectName);
+        return projectName;
+    }
+
+    private void updateTestTypeFromConfig(ILaunchConfiguration config) {
+        String testTypeName = EMPTY_STRING;
+        mOriginalTestMethodName = EMPTY_STRING;
+        try {
+            testTypeName = config.getAttribute(
+                    IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
+            mOriginalTestMethodName = config.getAttribute(
+                    JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
+        } catch (CoreException ce) {
+            // ignore
+        }
+        mTestRadioButton.setSelection(true);
+        setEnableSingleTestGroup(true);
+        setEnableContainerTestGroup(false);     
+        mTestContainerRadioButton.setSelection(false);
+        mTestText.setText(testTypeName);
+        mContainerText.setText(EMPTY_STRING); 
+        setTestMethodLabel(mOriginalTestMethodName);
+    }
+
+    private void setTestMethodLabel(String testMethodName) {
+        if (!EMPTY_STRING.equals(testMethodName)) {
+            mTestMethodLabel.setText(
+                    JUnitMessages.JUnitLaunchConfigurationTab_label_method + 
+                    mOriginalTestMethodName); 
+        } else {
+            mTestMethodLabel.setText(EMPTY_STRING);
+        }
+    }
+
+    private void updateTestContainerFromConfig(ILaunchConfiguration config) {
+        String containerHandle = EMPTY_STRING;
+        IJavaElement containerElement = null;
+        try {
+            containerHandle = config.getAttribute(
+                    JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+            if (containerHandle.length() > 0) {
+                containerElement = JavaCore.create(containerHandle);
+            }
+        } catch (CoreException ce) {
+            // ignore
+        }
+        if (containerElement != null) {
+            mContainerElement = containerElement;
+        }    
+        mTestContainerRadioButton.setSelection(true);
+        setEnableSingleTestGroup(false);
+        setEnableContainerTestGroup(true);              
+        mTestRadioButton.setSelection(false);
+        if (mContainerElement != null) {
+            mContainerText.setText(getPresentationName(mContainerElement));
+        }    
+        mTestText.setText(EMPTY_STRING);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+     */
+    public void performApply(ILaunchConfigurationWorkingCopy config) {
+        if (mTestContainerRadioButton.getSelection() && mContainerElement != null) {
+            config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, 
+                    mContainerElement.getJavaProject().getElementName());
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, 
+                    mContainerElement.getHandleIdentifier());
+            config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+                    EMPTY_STRING);
+             //workaround for Eclipse bug 65399
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+                    EMPTY_STRING);
+        } else {
+            config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+                    mProjText.getText());
+            config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+                    mTestText.getText());
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+                    EMPTY_STRING);
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+                    mOriginalTestMethodName);
+        }
+        try {
+            mapResources(config);
+        } catch (CoreException e) {
+            // TODO: does the real error need to be extracted out of CoreException
+            AdtPlugin.log(e, "Error occurred saving configuration");
+        }
+        AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+        
+        config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+                getSelectedInstrumentation());
+    }
+
+    private void mapResources(ILaunchConfigurationWorkingCopy config)  throws CoreException {
+        JUnitMigrationDelegate.mapResources(config);
+    }   
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#dispose()
+     */
+    @Override
+    public void dispose() {
+        super.dispose();
+        if (mTabIcon != null) {
+            mTabIcon.dispose();
+            mTabIcon = null;
+        }
+        mJavaElementLabelProvider.dispose();
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getImage()
+     */
+    @Override
+    public Image getImage() {
+        // reuse icon from the Android App Launch config tab
+        if (mTabIcon == null) {
+            mTabIcon= AdtPlugin.getImageLoader().loadImage(MainLaunchConfigTab.LAUNCH_TAB_IMAGE,
+                    null);
+        }
+        return mTabIcon;
+    }
+
+    /**
+     * Show a dialog that lists all main types
+     */
+    private void handleSearchButtonSelected() {
+        Shell shell = getShell();
+
+        IJavaProject javaProject = getJavaProject();
+
+        IType[] types = new IType[0];
+        boolean[] radioSetting = new boolean[2];
+        try {
+            // fix for Eclipse bug 66922 Wrong radio behaviour when switching
+            // remember the selected radio button
+            radioSetting[0] = mTestRadioButton.getSelection();
+            radioSetting[1] = mTestContainerRadioButton.getSelection();
+            
+            types = TestSearchEngine.findTests(getLaunchConfigurationDialog(), javaProject,
+                    getTestKind()); 
+        } catch (InterruptedException e) {
+            setErrorMessage(e.getMessage());
+            return;
+        } catch (InvocationTargetException e) {
+            AdtPlugin.log(e.getTargetException(), "Error finding test types");
+            return;
+        } finally {
+            mTestRadioButton.setSelection(radioSetting[0]);
+            mTestContainerRadioButton.setSelection(radioSetting[1]);
+        }
+
+        SelectionDialog dialog = new TestSelectionDialog(shell, types);
+        dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_title); 
+        dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_message); 
+        if (dialog.open() == Window.CANCEL) {
+            return;
+        }
+
+        Object[] results = dialog.getResult();
+        if ((results == null) || (results.length < 1)) {
+            return;
+        }       
+        IType type = (IType) results[0];
+
+        if (type != null) {
+            mTestText.setText(type.getFullyQualifiedName('.'));
+            javaProject = type.getJavaProject();
+            mProjText.setText(javaProject.getElementName());
+        }
+    }
+
+    private ITestKind getTestKind() {
+        // harddcode this to JUnit 3
+        return TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID);
+    }
+
+    /**
+     * Show a dialog that lets the user select a Android project.  This in turn provides
+     * context for the main type, allowing the user to key a main type name, or
+     * constraining the search for main types to the specified project.
+     */
+    private void handleProjectButtonSelected() {
+        IJavaProject project = mProjectChooserHelper.chooseJavaProject(getProjectName());
+        if (project == null) {
+            return;
+        }
+
+        String projectName = project.getElementName();
+        mProjText.setText(projectName);
+        loadInstrumentations(project.getProject());   
+    }
+
+    /**
+     * Return the IJavaProject corresponding to the project name in the project name
+     * text field, or null if the text does not match a Android project name.
+     */
+    private IJavaProject getJavaProject() {
+        String projectName = getProjectName();
+        return getJavaModel().getJavaProject(projectName);      
+    }
+
+    /**
+     * Returns the name of the currently specified project. Null if no project is selected.
+     */
+    private String getProjectName() {
+        String projectName = mProjText.getText().trim();
+        if (projectName.length() < 1) {
+            return null;
+        }
+        return projectName;
+    }
+
+    /**
+     * Convenience method to get the workspace root.
+     */
+    private IWorkspaceRoot getWorkspaceRoot() {
+        return ResourcesPlugin.getWorkspace().getRoot();
+    }
+
+    /**
+     * Convenience method to get access to the java model.
+     */
+    private IJavaModel getJavaModel() {
+        return JavaCore.create(getWorkspaceRoot());
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#isValid(org.eclipse.debug.core.ILaunchConfiguration)
+     */
+    @Override
+    public boolean isValid(ILaunchConfiguration config) {
+        validatePage();
+        return getErrorMessage() == null;
+    }
+
+    private void testModeChanged() {
+        boolean isSingleTestMode = mTestRadioButton.getSelection();
+        setEnableSingleTestGroup(isSingleTestMode);
+        setEnableContainerTestGroup(!isSingleTestMode);
+        if (!isSingleTestMode && mContainerText.getText().length() == 0) {
+            String projText = mProjText.getText();
+            if (Path.EMPTY.isValidSegment(projText)) {
+                IJavaProject javaProject = getJavaModel().getJavaProject(projText);
+                if (javaProject != null && javaProject.exists()) {
+                    setContainerElement(javaProject);
+                }    
+            }
+        }
+        validatePage();
+        updateLaunchConfigurationDialog();
+    }
+
+    private void validatePage() {       
+        setErrorMessage(null);
+        setMessage(null);
+
+        if (mTestContainerRadioButton.getSelection()) {
+            if (mContainerElement == null) {
+                setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_noContainer);
+                return;
+            }
+            validateJavaProject(mContainerElement.getJavaProject());
+            return;
+        }
+
+        String projectName = mProjText.getText().trim();
+        if (projectName.length() == 0) {
+            setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotdefined);
+            return;
+        }
+
+        IStatus status = ResourcesPlugin.getWorkspace().validatePath(IPath.SEPARATOR + projectName,
+                IResource.PROJECT);
+        if (!status.isOK() || !Path.ROOT.isValidSegment(projectName)) {
+            setErrorMessage(Messages.format(
+                    JUnitMessages.JUnitLaunchConfigurationTab_error_invalidProjectName, 
+                    projectName));
+            return;
+        }
+
+        IProject project = getWorkspaceRoot().getProject(projectName);
+        if (!project.exists()) {
+            setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotexists);
+            return;
+        }
+        IJavaProject javaProject = JavaCore.create(project);
+        validateJavaProject(javaProject);
+
+        try {
+            if (!project.hasNature(AndroidConstants.NATURE)) {
+                setErrorMessage("Specified project is not an Android project");
+                return;
+            }
+            String className = mTestText.getText().trim();
+            if (className.length() == 0) {
+                setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
+                return;
+            }
+            if (javaProject.findType(className) == null) {
+                setErrorMessage(Messages.format(
+                        JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found,
+                        new String[] { className, projectName }));
+                return;
+            }          
+        } catch (CoreException e) {
+            AdtPlugin.log(e, "validatePage failed");
+        }
+
+        validateInstrumentation(javaProject);
+    }
+
+    private void validateJavaProject(IJavaProject javaProject) {
+        if (!TestSearchEngine.hasTestCaseType(javaProject)) {
+            setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testcasenotonpath); 
+            return;             
+        }
+    }
+
+    private void validateInstrumentation(IJavaProject javaProject) {
+        if (mInstrumentations == null || mInstrumentations.length < 1) {
+            setErrorMessage("Specified project has no defined instrumentations");
+        }
+        String instrumentation = getSelectedInstrumentation();
+        if (instrumentation == null) {
+            setErrorMessage("Instrumentation not specified");
+        }
+    }
+
+    private String getSelectedInstrumentation() {
+        int selectionIndex = mInstrumentationCombo.getSelectionIndex();
+        if (mInstrumentations != null && selectionIndex >= 0 && 
+                selectionIndex < mInstrumentations.length) {
+            return mInstrumentations[selectionIndex];
+        }
+        return null;
+    }
+
+    private void setEnableContainerTestGroup(boolean enabled) {
+        mContainerSearchButton.setEnabled(enabled);
+        mContainerText.setEnabled(enabled);
+    }
+
+    private void setEnableSingleTestGroup(boolean enabled) {
+        mProjLabel.setEnabled(enabled);
+        mProjText.setEnabled(enabled);
+        mProjButton.setEnabled(enabled);
+        mTestLabel.setEnabled(enabled);
+        mTestText.setEnabled(enabled);
+        mSearchButton.setEnabled(enabled && mProjText.getText().length() > 0);
+        mTestMethodLabel.setEnabled(enabled);
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+     */
+    public void setDefaults(ILaunchConfigurationWorkingCopy config) {
+        IJavaElement javaElement = getContext();
+        if (javaElement != null) {
+            initializeJavaProject(javaElement, config);
+        } else {
+            // We set empty attributes for project & main type so that when one config is
+            // compared to another, the existence of empty attributes doesn't cause an
+            // incorrect result (the performApply() method can result in empty values
+            // for these attributes being set on a config if there is nothing in the
+            // corresponding text boxes)
+            config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, EMPTY_STRING);
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+                    EMPTY_STRING);
+        }
+        initializeTestAttributes(javaElement, config);
+    }
+
+    private void initializeTestAttributes(IJavaElement javaElement,
+            ILaunchConfigurationWorkingCopy config) {
+        if (javaElement != null && javaElement.getElementType() < IJavaElement.COMPILATION_UNIT) { 
+            initializeTestContainer(javaElement, config);
+        } else {
+            initializeTestType(javaElement, config);
+        }    
+    }
+
+    private void initializeTestContainer(IJavaElement javaElement,
+            ILaunchConfigurationWorkingCopy config) {
+        config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+                javaElement.getHandleIdentifier());
+        initializeName(config, javaElement.getElementName());
+    }
+
+    private void initializeName(ILaunchConfigurationWorkingCopy config, String name) {
+        if (name == null) {
+            name = EMPTY_STRING;
+        }
+        if (name.length() > 0) {
+            int index = name.lastIndexOf('.');
+            if (index > 0) {
+                name = name.substring(index + 1);
+            }
+            name = getLaunchConfigurationDialog().generateName(name);
+            config.rename(name);
+        }
+    }
+
+    /**
+     * Sets the main type & name attributes on the working copy based on the IJavaElement
+     */
+    private void initializeTestType(IJavaElement javaElement,
+            ILaunchConfigurationWorkingCopy config) {
+        String name = EMPTY_STRING;
+        String testKindId = null;
+        try {
+            // only do a search for compilation units or class files or source references
+            if (javaElement instanceof ISourceReference) {
+                ITestKind testKind = TestKindRegistry.getContainerTestKind(javaElement);
+                testKindId = testKind.getId();
+
+                IType[] types = TestSearchEngine.findTests(getLaunchConfigurationDialog(),
+                        javaElement, testKind); 
+                if ((types == null) || (types.length < 1)) {
+                    return;
+                }
+                // Simply grab the first main type found in the searched element
+                name = types[0].getFullyQualifiedName('.');
+                
+            }   
+        } catch (InterruptedException ie) {
+            // ignore
+        } catch (InvocationTargetException ite) {
+            // ignore
+        }
+        config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, name);
+        if (testKindId != null) {
+            config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+                    testKindId);
+        }    
+        initializeName(config, name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+     */
+    public String getName() {
+        return JUnitMessages.JUnitLaunchConfigurationTab_tab_label; 
+    }
+
+    @SuppressWarnings("unchecked")
+    private IJavaElement chooseContainer(IJavaElement initElement) {
+        Class[] acceptedClasses = new Class[] { IPackageFragmentRoot.class, IJavaProject.class,
+                IPackageFragment.class };
+        TypedElementSelectionValidator validator = new TypedElementSelectionValidator(
+                acceptedClasses, false) {
+            @Override
+            public boolean isSelectedValid(Object element) {
+                return true;
+            }
+        };
+
+        acceptedClasses = new Class[] { IJavaModel.class, IPackageFragmentRoot.class,
+                IJavaProject.class, IPackageFragment.class };
+        ViewerFilter filter = new TypedViewerFilter(acceptedClasses) {
+            @Override
+            public boolean select(Viewer viewer, Object parent, Object element) {
+                if (element instanceof IPackageFragmentRoot && 
+                        ((IPackageFragmentRoot) element).isArchive()) {
+                    return false;
+                }    
+                try {
+                    if (element instanceof IPackageFragment &&
+                            !((IPackageFragment) element).hasChildren()) {
+                        return false;
+                    }
+                } catch (JavaModelException e) {
+                    return false;
+                }
+                return super.select(viewer, parent, element);
+            }
+        };      
+
+        StandardJavaElementContentProvider provider = new StandardJavaElementContentProvider();
+        ILabelProvider labelProvider = new JavaElementLabelProvider(
+                JavaElementLabelProvider.SHOW_DEFAULT); 
+        ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(), 
+                labelProvider, provider);
+        dialog.setValidator(validator);
+        dialog.setComparator(new JavaElementComparator());
+        dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_title);  
+        dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_message);  
+        dialog.addFilter(filter);
+        dialog.setInput(JavaCore.create(getWorkspaceRoot()));
+        dialog.setInitialSelection(initElement);
+        dialog.setAllowMultiple(false);
+
+        if (dialog.open() == Window.OK) {
+            Object element = dialog.getFirstResult();
+            return (IJavaElement) element;
+        }
+        return null;
+    }
+
+    private String getPresentationName(IJavaElement element) {
+        return mJavaElementLabelProvider.getText(element);
+    }
+
+    /**
+     * Returns the current Java element context from which to initialize
+     * default settings, or <code>null</code> if none.
+     * 
+     * @return Java element context.
+     */
+    private IJavaElement getContext() {
+        IWorkbenchWindow activeWorkbenchWindow =
+            PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+        if (activeWorkbenchWindow == null) {
+            return null;
+        }
+        IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
+        if (page != null) {
+            ISelection selection = page.getSelection();
+            if (selection instanceof IStructuredSelection) {
+                IStructuredSelection ss = (IStructuredSelection) selection;
+                if (!ss.isEmpty()) {
+                    Object obj = ss.getFirstElement();
+                    if (obj instanceof IJavaElement) {
+                        return (IJavaElement) obj;
+                    }
+                    if (obj instanceof IResource) {
+                        IJavaElement je = JavaCore.create((IResource) obj);
+                        if (je == null) {
+                            IProject pro = ((IResource) obj).getProject();
+                            je = JavaCore.create(pro);
+                        }
+                        if (je != null) {
+                            return je;
+                        }
+                    }
+                }
+            }
+            IEditorPart part = page.getActiveEditor();
+            if (part != null) {
+                IEditorInput input = part.getEditorInput();
+                return (IJavaElement) input.getAdapter(IJavaElement.class);
+            }
+        }
+        return null;
+    }
+
+    private void initializeJavaProject(IJavaElement javaElement,
+            ILaunchConfigurationWorkingCopy config) {
+        IJavaProject javaProject = javaElement.getJavaProject();
+        String name = null;
+        if (javaProject != null && javaProject.exists()) {
+            name = javaProject.getElementName();
+        }
+        config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+    }
+
+    private void setButtonGridData(Button button) {
+        GridData gridData = new GridData();
+        button.setLayoutData(gridData);
+        LayoutUtil.setButtonDimensionHint(button);
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getId()
+     */
+    @Override
+    public String getId() {
+        return "com.android.ide.eclipse.adt.launch.AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+    }
+
+    /**
+     * Loads the UI with the instrumentations of the specified project, and stores the
+     * activities in <code>mActivities</code>.
+     * <p/>
+     * First activity is selected by default if present.
+     * 
+     * @param project the {@link IProject} to load the instrumentations from.
+     */
+    private void loadInstrumentations(IProject project) {
+        mInstrumentations = AndroidJUnitLaunchConfigDelegate.getInstrumentationsForProject(project);
+        if (mInstrumentations != null) {
+            mInstrumentationCombo.removeAll();
+            for (String instrumentation : mInstrumentations) {
+                mInstrumentationCombo.add(instrumentation);
+            }
+            // the selection will be set when we update the ui from the current
+            // config object.
+            return;
+        }
+
+        // if we reach this point, either project is null, or we got an exception during
+        // the parsing. In either case, we empty the instrumentation list.
+        mInstrumentations = null;
+        mInstrumentationCombo.removeAll();
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java
new file mode 100755
index 0000000..e03f282
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
+
+/**
+ * Launch shortcut to launch debug/run Android JUnit configuration directly.
+ */
+public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
+
+    @Override
+    protected String getLaunchConfigurationTypeId() {
+        return "com.android.ide.eclipse.adt.junit.launchConfigurationType"; //$NON-NLS-1$
+    }
+
+    /**
+     * Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
+     * first instrumentation found in the AndroidManifest.  
+     */
+    @Override
+    protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
+            throws CoreException {
+        ILaunchConfigurationWorkingCopy config = super.createLaunchConfiguration(element);
+        IProject project = element.getResource().getProject();
+        String[] instrumentations = 
+            AndroidJUnitLaunchConfigDelegate.getInstrumentationsForProject(project);
+        if (instrumentations != null && instrumentations.length > 0) {
+            // just pick the first runner
+            config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, 
+                    instrumentations[0]);
+        }
+        AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+
+        return config;
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java
new file mode 100644
index 0000000..3c82f57
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit;
+
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+import com.android.ide.eclipse.adt.launch.EmulatorConfigTab;
+
+/**
+ * Tab group object for Android JUnit launch configuration type.
+ */
+public class AndroidJUnitTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+    /**
+     * Creates the UI tabs for the Android JUnit configuration
+     */
+    public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+        ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+                new AndroidJUnitLaunchConfigurationTab(),
+                new EmulatorConfigTab(),
+                new CommonTab()
+        };
+        setTabs(tabs);
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java
new file mode 100644
index 0000000..89cad97
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import org.eclipse.core.resources.IProject;
+
+import com.android.ddmlib.IDevice;
+
+/**
+ * Contains info about Android JUnit launch
+ */
+public class AndroidJUnitLaunchInfo {
+    private final IProject mProject;
+    private final String mTestPackage;
+    private final String mRunner;
+    private final boolean mDebugMode;
+    private final IDevice mDevice;
+    
+    public AndroidJUnitLaunchInfo(IProject project, String testPackage, String runner,
+            boolean debugMode, IDevice device) {
+        mProject = project;
+        mTestPackage = testPackage;
+        mRunner = runner;
+        mDebugMode = debugMode;
+        mDevice = device;
+    }
+    
+    public IProject getProject() {
+        return mProject;
+    }
+
+    public String getTestPackage() {
+        return mTestPackage;
+    }
+
+    public String getRunner() {
+        return mRunner;
+    }
+
+    public boolean isDebugMode() {
+        return mDebugMode;
+    }
+
+    public IDevice getDevice() {
+        return mDevice;
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java
new file mode 100644
index 0000000..9db3ef0
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.ITestIdentifier;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+
+/**
+ * Base implementation of the Eclipse {@link ITestReference} and {@link ITestIdentifier} interfaces
+ * for Android tests.
+ * <p/>
+ * Provides generic equality/hashcode services
+ */
+@SuppressWarnings("restriction")  //$NON-NLS-1$
+abstract class AndroidTestReference implements ITestReference, ITestIdentifier {
+
+    /**
+     * Gets the {@link ITestIdentifier} for this test reference.
+     */
+    public ITestIdentifier getIdentifier() {
+        // this class serves as its own test identifier
+        return this;
+    }
+
+    /**
+     * Not supported.
+     */
+    public void run(TestExecution execution) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Compares {@link ITestIdentifier} using names
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ITestIdentifier) {
+            ITestIdentifier testid = (ITestIdentifier) obj;
+            return getName().equals(testid.getName());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return getName().hashCode();
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java
new file mode 100755
index 0000000..6834c08
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
+
+/**
+ * Supports Eclipse JUnit execution of Android tests.
+ * <p/>
+ * Communicates back to a Eclipse JDT JUnit client via a socket connection.
+ * 
+ * @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
+ */
+@SuppressWarnings("restriction")
+public class RemoteADTTestRunner extends RemoteTestRunner {
+
+    private AndroidJUnitLaunchInfo mLaunchInfo;
+    private TestExecution mExecution;
+    
+    /**
+     * Initialize the JDT JUnit test runner parameters from the {@code args}.
+     * 
+     * @param args name-value pair of arguments to pass to parent JUnit runner. 
+     * @param launchInfo the Android specific test launch info
+     */
+    protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) {
+        defaultInit(args);
+        mLaunchInfo = launchInfo;
+    }   
+
+    /**
+     * Runs a set of tests, and reports back results using parent class.
+     * <p/>
+     * JDT Unit expects to be sent data in the following sequence:
+     * <ol>
+     *   <li>The total number of tests to be executed.</li>
+     *   <li>The test 'tree' data about the tests to be executed, which is composed of the set of
+     *   test class names, the number of tests in each class, and the names of each test in the
+     *   class.</li>
+     *   <li>The test execution result for each test method. Expects individual notifications of
+     *   the test execution start, any failures, and the end of the test execution.</li>
+     *   <li>The end of the test run, with its elapsed time.</li>
+     * </ol>  
+     * <p/>
+     * In order to satisfy this, this method performs two actual Android instrumentation runs.
+     * The first is a 'log only' run that will collect the test tree data, without actually
+     * executing the tests,  and send it back to JDT JUnit. The second is the actual test execution,
+     * whose results will be communicated back in real-time to JDT JUnit.
+     * 
+     * @param testClassNames array of fully qualified test class names to execute. Cannot be empty.
+     * @param testName test to execute. If null, will be ignored.
+     * @param execution used to report test progress
+     */
+    @Override
+    public void runTests(String[] testClassNames, String testName, TestExecution execution) {
+        // hold onto this execution reference so it can be used to report test progress
+        mExecution = execution;
+        
+        RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mLaunchInfo.getTestPackage(), 
+                mLaunchInfo.getRunner(), mLaunchInfo.getDevice()); 
+
+        if (testClassNames != null && testClassNames.length > 0) {
+            if (testName != null) {
+                runner.setMethodName(testClassNames[0], testName);
+            } else {
+                runner.setClassNames(testClassNames);
+            }
+        }
+        // set log only to first collect test case info, so Eclipse has correct test case count/
+        // tree info
+        runner.setLogOnly(true);
+        TestCollector collector = new TestCollector();        
+        runner.run(collector);
+        if (collector.getErrorMessage() != null) {
+            // error occurred during test collection.
+            reportError(collector.getErrorMessage());
+            // abort here
+        }
+        notifyTestRunStarted(collector.getTestCaseCount());
+        collector.sendTrees(this);
+        
+        // now do real execution
+        runner.setLogOnly(false);
+        if (mLaunchInfo.isDebugMode()) {
+            runner.setDebug(true);
+        }
+        runner.run(new TestRunListener());
+    }
+    
+    /**
+     * Main entry method to run tests
+     * 
+     * @param programArgs JDT JUnit program arguments to be processed by parent
+     * @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru
+     */
+    public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
+        init(programArgs, junitInfo);
+        run();
+    } 
+
+    /**
+     * Stop the current test run.
+     */
+    public void terminate() {
+        stop();
+    }
+
+    @Override
+    protected void stop() {
+        if (mExecution != null) {
+            mExecution.stop();
+        }    
+    }
+
+    private void notifyTestRunEnded(long elapsedTime) {
+        // copy from parent - not ideal, but method is private
+        sendMessage(MessageIds.TEST_RUN_END + elapsedTime);
+        flush();
+        //shutDown();
+    }
+
+    /**
+     * @param errorMessage
+     */
+    private void reportError(String errorMessage) {
+        AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), 
+                String.format("Test run failed: %s", errorMessage));
+        // is this needed?
+        //notifyTestRunStopped(-1);
+    }
+
+    /**
+     * TestRunListener that communicates results in real-time back to JDT JUnit 
+     */
+    private class TestRunListener implements ITestRunListener {
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
+         */
+        public void testEnded(TestIdentifier test) {
+            mExecution.getListener().notifyTestEnded(new TestCaseReference(test));
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+         */
+        public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+            String statusString;
+            if (status == TestFailure.ERROR) {
+                statusString = MessageIds.TEST_ERROR;
+            } else {
+                statusString = MessageIds.TEST_FAILED;
+            }
+            TestReferenceFailure failure = 
+                new TestReferenceFailure(new TestCaseReference(test), 
+                        statusString, trace, null);
+            mExecution.getListener().notifyTestFailed(failure);
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
+         */
+        public void testRunEnded(long elapsedTime) {
+            notifyTestRunEnded(elapsedTime);
+            AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run complete");
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+         */
+        public void testRunFailed(String errorMessage) {
+            reportError(errorMessage);
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+         */
+        public void testRunStarted(int testCount) {
+            // ignore
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+         */
+        public void testRunStopped(long elapsedTime) {
+            notifyTestRunStopped(elapsedTime);
+            AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run stopped");
+        }
+
+        /* (non-Javadoc)
+         * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+         */
+        public void testStarted(TestIdentifier test) {
+            TestCaseReference testId = new TestCaseReference(test);
+            mExecution.getListener().notifyTestStarted(testId);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java
new file mode 100644
index 0000000..1a0ee9a
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+
+import java.text.MessageFormat;
+
+/**
+ * Reference for a single Android test method.
+ */
+@SuppressWarnings("restriction")
+class TestCaseReference extends AndroidTestReference {
+
+    private final String mClassName;
+    private final String mTestName;
+    
+    /**
+     * Creates a TestCaseReference from a class and method name
+     */
+    TestCaseReference(String className, String testName) {
+        mClassName = className;
+        mTestName = testName;
+    }
+
+    /**
+     * Creates a TestCaseReference from a {@link TestIdentifier}
+     * @param test
+     */
+    TestCaseReference(TestIdentifier test) {
+        mClassName = test.getClassName();
+        mTestName = test.getTestName();
+    }
+
+    /**
+     * Returns a count of the number of test cases referenced. Is always one for this class.
+     */
+    public int countTestCases() {
+        return 1;
+    }
+
+    /**
+     * Sends test identifier and test count information for this test
+     * 
+     * @param notified the {@link IVisitsTestTrees} to send test info to
+     */
+    public void sendTree(IVisitsTestTrees notified) {
+        notified.visitTreeEntry(getIdentifier(), false, countTestCases());
+    }
+
+    /**
+     * Returns the identifier of this test, in a format expected by JDT JUnit
+     */
+    public String getName() {
+        return MessageFormat.format(MessageIds.TEST_IDENTIFIER_MESSAGE_FORMAT, 
+                new Object[] { mTestName, mClassName});
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java
new file mode 100644
index 0000000..2dc13a7
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collects info about tests to be executed by listening to the results of an Android test run.
+ */
+@SuppressWarnings("restriction")
+class TestCollector implements ITestRunListener {
+
+    private int mTotalTestCount;
+    /** test name to test suite reference map. */
+    private Map<String, TestSuiteReference> mTestTree;
+    private String mErrorMessage = null;
+
+    TestCollector() {
+        mTotalTestCount = 0; 
+        mTestTree = new HashMap<String, TestSuiteReference>();
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
+     */
+    public void testEnded(TestIdentifier test) {
+        // ignore
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+     */
+    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+        // ignore - should be impossible since this is only collecting test information
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
+     */
+    public void testRunEnded(long elapsedTime) {
+        // ignore
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+     */
+    public void testRunFailed(String errorMessage) {
+        mErrorMessage = errorMessage;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+     */
+    public void testRunStarted(int testCount) {
+        mTotalTestCount = testCount;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+     */
+    public void testRunStopped(long elapsedTime) {
+        // ignore
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+     */
+    public void testStarted(TestIdentifier test) {
+        TestSuiteReference suiteRef = mTestTree.get(test.getClassName());
+        if (suiteRef == null) {
+            // this test suite has not been seen before, create it
+            suiteRef = new TestSuiteReference(test.getClassName());
+            mTestTree.put(test.getClassName(), suiteRef);
+        }
+        suiteRef.addTest(new TestCaseReference(test));
+    }
+
+    /**
+     * Returns the total test count in the test run.
+     */
+    public int getTestCaseCount() {
+        return mTotalTestCount;
+    }
+
+    /**
+     * Sends info about the test tree to be executed (ie the suites and their enclosed tests) 
+     * 
+     * @param notified the {@link IVisitsTestTrees} to send test data to
+     */
+    public void sendTrees(IVisitsTestTrees notified) {
+        for (ITestReference ref : mTestTree.values()) {
+            ref.sendTree(notified);
+        }
+    }
+
+    /**
+     * Returns the error message that was reported when collecting test info. 
+     * Returns <code>null</code> if no error occurred.
+     */
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java
new file mode 100644
index 0000000..797f27b
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Reference for an Android test suite aka class.
+ */
+@SuppressWarnings("restriction")
+class TestSuiteReference extends AndroidTestReference {
+
+    private final String mClassName;
+    private List<TestCaseReference> mTests;
+
+    /**
+     * Creates a TestSuiteReference
+     * 
+     * @param className the fully qualified name of the test class
+     */
+    TestSuiteReference(String className) {
+         mClassName = className; 
+         mTests = new ArrayList<TestCaseReference>();
+    }
+
+    /**
+     * Returns a count of the number of test cases included in this suite. 
+     */
+    public int countTestCases() {
+        return mTests.size();
+    }
+
+    /**
+     * Sends test identifier and test count information for this test class, and all its included
+     * test methods.
+     * 
+     * @param notified the {@link IVisitsTestTrees} to send test info too
+     */
+    public void sendTree(IVisitsTestTrees notified) {
+        notified.visitTreeEntry(getIdentifier(), true, countTestCases());
+        for (TestCaseReference ref : mTests) {
+            ref.sendTree(notified);
+        }
+    }
+
+    /**
+     * Return the name of this test class.
+     */
+    public String getName() {
+        return mClassName;
+    }
+
+    /**
+     * Adds a test method to this suite.
+     * 
+     * @param testRef the {@link TestCaseReference} to add
+     */
+    void addTest(TestCaseReference testRef) {
+        mTests.add(testRef);
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
index 5aeb335..e9df77f 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
@@ -487,6 +487,15 @@
             IJavaProject javaProject = projects.get(i);
             IProject iProject = javaProject.getProject();
             
+            // check if the project is opened
+            if (iProject.isOpen() == false) {
+                // remove from the list
+                // we do not increment i in this case.
+                projects.remove(i);
+
+                continue;
+            }
+
             // get the target from the project and its paths
             IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
             if (target == null) {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
index 0dd88c0..7d3cad3 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
@@ -829,7 +829,7 @@
         
         String packageName = null;
         String activityName = null;
-        int minSdkVersion = 0; // 0 means no minSdkVersion provided in the manifest
+        int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
         try {
             packageName = manifestData.getPackage();
             minSdkVersion = manifestData.getApiLevelRequirement();
@@ -927,7 +927,7 @@
             }
         }
 
-        if (!foundTarget && minSdkVersion > 0) {
+        if (!foundTarget && minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK) {
             try {
                 for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
                     if (target.getApiVersionNumber() == minSdkVersion) {
@@ -954,7 +954,8 @@
         if (!foundTarget) {
             mInternalMinSdkVersionUpdate = true;
             mMinSdkVersionField.setText(
-                    minSdkVersion <= 0 ? "" : Integer.toString(minSdkVersion)); //$NON-NLS-1$
+                    minSdkVersion == AndroidManifestParser.INVALID_MIN_SDK ? "" :
+                        Integer.toString(minSdkVersion)); //$NON-NLS-1$
             mInternalMinSdkVersionUpdate = false;
         }
     }
@@ -1148,7 +1149,7 @@
             return MSG_NONE;
         }
 
-        int version = -1;
+        int version = AndroidManifestParser.INVALID_MIN_SDK;
         try {
             // If not empty, it must be a valid integer > 0
             version = Integer.parseInt(getMinSdkVersion());
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
index 0a45196..42c881b 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
@@ -72,6 +72,8 @@
     private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$
     private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$
     
+    public final static int INVALID_MIN_SDK = -1;
+    
     /**
      * XML error & data handler used when parsing the AndroidManifest.xml file.
      * <p/>
@@ -92,8 +94,9 @@
         private Set<String> mProcesses = null;
         /** debuggable attribute value. If null, the attribute is not present. */
         private Boolean mDebuggable = null;
-        /** API level requirement. if 0 the attribute was not present. */
-        private int mApiLevelRequirement = 0;
+        /** API level requirement. if {@link AndroidManifestParser#INVALID_MIN_SDK}
+         * the attribute was not present. */
+        private int mApiLevelRequirement = INVALID_MIN_SDK;
         /** List of all instrumentations declared by the manifest */
         private final ArrayList<String> mInstrumentations = new ArrayList<String>();
         /** List of all libraries in use declared by the manifest */
@@ -171,7 +174,8 @@
         }
         
         /**
-         * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set. 
+         * Returns the <code>minSdkVersion</code> attribute, or
+         * {@link AndroidManifestParser#INVALID_MIN_SDK} if it's not set. 
          */
         int getApiLevelRequirement() {
             return mApiLevelRequirement;
@@ -750,7 +754,8 @@
     }
     
     /**
-     * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set. 
+     * Returns the <code>minSdkVersion</code> attribute, or {@link #INVALID_MIN_SDK}
+     * if it's not set. 
      */
     public int getApiLevelRequirement() {
         return mApiLevelRequirement;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
index 0c43499..b6d4c9a 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
@@ -16,8 +16,7 @@
 
 package com.android.ide.eclipse.common.project;
 
-import com.android.ide.eclipse.common.project.BaseProjectHelper;
-
+import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IWorkspaceRoot;
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.jdt.core.IJavaModel;
@@ -82,7 +81,7 @@
 
         // open the dialog and return the object selected if OK was clicked, or null otherwise
         if (dialog.open() == Window.OK) {
-            return (IJavaProject)dialog.getFirstResult();
+            return (IJavaProject) dialog.getFirstResult();
         }
         return null;
     }
@@ -107,4 +106,24 @@
         
         return mAndroidProjects;
     }
+    
+    /**
+     * Helper method to get the Android project with the given name
+     * 
+     * @param projectName the name of the project to find
+     * @return the {@link IProject} for the Android project. <code>null</code> if not found.
+     */
+    public IProject getAndroidProject(String projectName) {
+        IProject iproject = null;
+        IJavaProject[] javaProjects = getAndroidProjects(null);
+        if (javaProjects != null) {
+            for (IJavaProject javaProject : javaProjects) {
+                if (javaProject.getElementName().equals(projectName)) {
+                    iproject = javaProject.getProject();
+                    break;
+                }
+            }
+        }    
+        return iproject;
+    }
 }
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
index 987ea92..5739153 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
@@ -461,5 +461,13 @@
     public void setHidden(boolean isHidden) throws CoreException {
         throw new NotImplementedException();
     }
+
+    public boolean isHidden(int options) {
+        throw new NotImplementedException();
+    }
+
+    public boolean isTeamPrivateMember(int options) {
+        throw new NotImplementedException();
+    }
 }
 
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
index 73a69aa..26bf53e 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
@@ -74,7 +74,8 @@
 
     // -------- UNIMPLEMENTED METHODS ----------------
 
-    public void create(boolean force, boolean local, IProgressMonitor monitor) throws CoreException {
+    public void create(boolean force, boolean local, IProgressMonitor monitor)
+            throws CoreException {
         throw new NotImplementedException();
     }
 
@@ -106,8 +107,8 @@
         throw new NotImplementedException();
     }
 
-    public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor)
-            throws CoreException {
+    public void move(IPath destination, boolean force, boolean keepHistory,
+            IProgressMonitor monitor) throws CoreException {
         throw new NotImplementedException();
     }
 
@@ -225,7 +226,8 @@
         throw new NotImplementedException();
     }
 
-    public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+    public void deleteMarkers(String type, boolean includeSubtypes, int depth)
+            throws CoreException {
         throw new NotImplementedException();
     }
 
@@ -428,24 +430,31 @@
         throw new NotImplementedException();
     }
 
-	public Map<?,?> getPersistentProperties() throws CoreException {
+    public Map<?,?> getPersistentProperties() throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
-	public Map<?,?> getSessionProperties() throws CoreException {
+    public Map<?,?> getSessionProperties() throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
-	public boolean isDerived(int options) {
+    public boolean isDerived(int options) {
         throw new NotImplementedException();
-	}
+    }
 
-	public boolean isHidden() {
+    public boolean isHidden() {
         throw new NotImplementedException();
-	}
+    }
 
-	public void setHidden(boolean isHidden) throws CoreException {
+    public void setHidden(boolean isHidden) throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
+    public boolean isHidden(int options) {
+        throw new NotImplementedException();
+    }
+
+    public boolean isTeamPrivateMember(int options) {
+        throw new NotImplementedException();
+    }
 }
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
index 0e6fde0..6c32d0f 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
@@ -42,6 +42,14 @@
 import java.net.URI;
 import java.util.Map;
 
+/**
+ * Mock implementation of {@link IProject}.
+ * <p/>Supported methods:
+ * <ul>
+ * <li>{@link #build(int kind, IProgressMonitor monitor)}</li>
+ * <li>{@link #members(int kind, String builderName, Map args, IProgressMonitor monitor)}</li>
+ * </ul>
+ */
 @SuppressWarnings("deprecation")
 public class ProjectMock implements IProject {
 
@@ -265,7 +273,8 @@
         throw new NotImplementedException();
     }
 
-    public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+    public void deleteMarkers(String type, boolean includeSubtypes, int depth)
+            throws CoreException {
         throw new NotImplementedException();
     }
 
@@ -473,29 +482,36 @@
         throw new NotImplementedException();
     }
 
-	public void create(IProjectDescription description, int updateFlags,
-			IProgressMonitor monitor) throws CoreException {
+    public void create(IProjectDescription description, int updateFlags,
+            IProgressMonitor monitor) throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
-	public Map<?,?> getPersistentProperties() throws CoreException {
+    public Map<?,?> getPersistentProperties() throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
-	public Map<?,?> getSessionProperties() throws CoreException {
+    public Map<?,?> getSessionProperties() throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
-	public boolean isDerived(int options) {
+    public boolean isDerived(int options) {
         throw new NotImplementedException();
-	}
+    }
 
-	public boolean isHidden() {
+    public boolean isHidden() {
         throw new NotImplementedException();
-	}
+    }
 
-	public void setHidden(boolean isHidden) throws CoreException {
+    public void setHidden(boolean isHidden) throws CoreException {
         throw new NotImplementedException();
-	}
+    }
 
+    public boolean isHidden(int options) {
+        throw new NotImplementedException();
+    }
+
+    public boolean isTeamPrivateMember(int options) {
+        throw new NotImplementedException();
+    }
 }
diff --git a/tools/scripts/divide_and_compress.py b/tools/scripts/divide_and_compress.py
index d369be4..2bcb0ab 100755
--- a/tools/scripts/divide_and_compress.py
+++ b/tools/scripts/divide_and_compress.py
@@ -36,89 +36,99 @@
 
 __author__ = 'jmatt@google.com (Justin Mattson)'
 
-from optparse import OptionParser
+import optparse
 import os
 import stat
 import sys
 import zipfile
-from zipfile import ZipFile
 import divide_and_compress_constants
 
 
-def Main(argv):
-  parser = CreateOptionsParser()
-  (options, args) = parser.parse_args()
-  VerifyArguments(options, parser)
-  zipper = DirectoryZipper(options.destination, 
-                           options.sourcefiles, 
-                           ParseSize(options.filesize),
-                           options.compress)
-  zipper.StartCompress()
-  
-
 def CreateOptionsParser():
-  rtn = OptionParser()
+  """Creates the parser for command line arguments.
+
+  Returns:
+    A configured optparse.OptionParser object.
+  """
+  rtn = optparse.OptionParser()
   rtn.add_option('-s', '--sourcefiles', dest='sourcefiles', default=None,
                  help='The directory containing the files to compress')
   rtn.add_option('-d', '--destination', dest='destination', default=None,
                  help=('Where to put the archive files, this should not be'
                        ' a child of where the source files exist.'))
   rtn.add_option('-f', '--filesize', dest='filesize', default='1M',
-                 help=('Maximum size of archive files. A number followed by' 
-                       'a magnitude indicator, eg. 1000000B == one million '
-                       'BYTES, 500K == five hundred KILOBYTES, 1.2M == one '
-                       'point two MEGABYTES. 1M == 1048576 BYTES'))
+                 help=('Maximum size of archive files. A number followed by '
+                       'a magnitude indicator either "B", "K", "M", or "G". '
+                       'Examples:\n  1000000B == one million BYTES\n'
+                       '  1.2M == one point two MEGABYTES\n'
+                       '  1M == 1048576 BYTES'))
   rtn.add_option('-n', '--nocompress', action='store_false', dest='compress',
-                 default=True, 
+                 default=True,
                  help=('Whether the archive files should be compressed, or '
                        'just a concatenation of the source files'))
   return rtn
 
 
 def VerifyArguments(options, parser):
+  """Runs simple checks on correctness of commandline arguments.
+
+  Args:
+    options: The command line options passed.
+    parser: The parser object used to parse the command string.
+  """
   try:
     if options.sourcefiles is None or options.destination is None:
       parser.print_help()
       sys.exit(-1)
-  except (AttributeError), err:
+  except AttributeError:
     parser.print_help()
     sys.exit(-1)
 
 
 def ParseSize(size_str):
+  """Parse the file size argument from a string to a number of bytes.
+
+  Args:
+    size_str: The string representation of the file size.
+
+  Returns:
+    The file size in bytes.
+
+  Raises:
+    ValueError: Raises an error if the numeric or qualifier portions of the
+      file size argument is invalid.
+  """
   if len(size_str) < 2:
     raise ValueError(('filesize argument not understood, please include'
                       ' a numeric value and magnitude indicator'))
-  magnitude = size_str[len(size_str)-1:]
-  if not magnitude in ('K', 'B', 'M'):
-    raise ValueError(('filesize magnitude indicator not valid, must be \'K\','
-                      '\'B\', or \'M\''))
-  numeral = float(size_str[0:len(size_str)-1])
+  magnitude = size_str[-1]
+  if not magnitude in ('B', 'K', 'M', 'G'):
+    raise ValueError(('filesize magnitude indicator not valid, must be "B",'
+                      '"K","M", or "G"'))
+  numeral = float(size_str[:-1])
   if magnitude == 'K':
     numeral *= 1024
   elif magnitude == 'M':
     numeral *= 1048576
+  elif magnitude == 'G':
+    numeral *= 1073741824
   return int(numeral)
 
 
 class DirectoryZipper(object):
-  """Class to compress a directory and all its sub-directories."""  
-  current_archive = None
-  output_dir = None
-  base_path = None
-  max_size = None
-  compress = None
-  index_fp = None
+  """Class to compress a directory and all its sub-directories."""
 
   def __init__(self, output_path, base_dir, archive_size, enable_compression):
     """DirectoryZipper constructor.
 
     Args:
-      output_path: the path to write the archives and index file to
-      base_dir: the directory to compress
-      archive_size: the maximum size, in bytes, of a single archive file
-      enable_compression: whether or not compression should be enabled, if
-        disabled, the files will be written into an uncompresed zip
+      output_path: A string, the path to write the archives and index file to.
+      base_dir: A string, the directory to compress.
+      archive_size: An number, the maximum size, in bytes, of a single
+        archive file.
+      enable_compression: A boolean, whether or not compression should be
+        enabled, if disabled, the files will be written into an uncompresed
+        zip.
     """
     self.output_dir = output_path
     self.current_archive = '0.zip'
@@ -126,6 +136,9 @@
     self.max_size = archive_size
     self.compress = enable_compression
 
+    # Set index_fp to None, because we don't know what it will be yet.
+    self.index_fp = None
+
   def StartCompress(self):
     """Start compress of the directory.
 
@@ -133,7 +146,7 @@
     specified output directory. It will also produce an 'index.txt' file in the
     output directory that maps from file to archive.
     """
-    self.index_fp = open(''.join([self.output_dir, 'main.py']), 'w')
+    self.index_fp = open(os.path.join(self.output_dir, 'main.py'), 'w')
     self.index_fp.write(divide_and_compress_constants.file_preamble)
     os.path.walk(self.base_path, self.CompressDirectory, 1)
     self.index_fp.write(divide_and_compress_constants.file_endpiece)
@@ -149,37 +162,32 @@
     Args:
       archive_path: Path to the archive to modify. This archive should not be
         open elsewhere, since it will need to be deleted.
-    Return:
-      A new ZipFile object that points to the modified archive file
+
+    Returns:
+      A new ZipFile object that points to the modified archive file.
     """
     if archive_path is None:
-      archive_path = ''.join([self.output_dir, self.current_archive])
+      archive_path = os.path.join(self.output_dir, self.current_archive)
 
-    # Move the old file and create a new one at its old location
-    ext_offset = archive_path.rfind('.')
-    old_archive = ''.join([archive_path[0:ext_offset], '-old',
-                           archive_path[ext_offset:]])
+    # Move the old file and create a new one at its old location.
+    root, ext = os.path.splitext(archive_path)
+    old_archive = ''.join([root, '-old', ext])
     os.rename(archive_path, old_archive)
     old_fp = self.OpenZipFileAtPath(old_archive, mode='r')
 
+    # By default, store uncompressed.
+    compress_bit = zipfile.ZIP_STORED
     if self.compress:
-      new_fp = self.OpenZipFileAtPath(archive_path,
-                                      mode='w',
-                                      compress=zipfile.ZIP_DEFLATED)
-    else:
-      new_fp = self.OpenZipFileAtPath(archive_path,
-                                      mode='w',
-                                      compress=zipfile.ZIP_STORED)
-    
-    # Read the old archive in a new archive, except the last one
-    zip_members = enumerate(old_fp.infolist())
-    num_members = len(old_fp.infolist())
-    while num_members > 1:
-      this_member = zip_members.next()[1]
-      new_fp.writestr(this_member.filename, old_fp.read(this_member.filename))
-      num_members -= 1
+      compress_bit = zipfile.ZIP_DEFLATED
+    new_fp = self.OpenZipFileAtPath(archive_path,
+                                    mode='w',
+                                    compress=compress_bit)
 
-    # Close files and delete the old one
+    # Read the old archive in a new archive, except the last one.
+    for zip_member in old_fp.infolist()[:-1]:
+      new_fp.writestr(zip_member, old_fp.read(zip_member.filename))
+
+    # Close files and delete the old one.
     old_fp.close()
     new_fp.close()
     os.unlink(old_archive)
@@ -193,11 +201,11 @@
         mode = 'w'
 
     if mode == 'r':
-      return ZipFile(path, mode)
+      return zipfile.ZipFile(path, mode)
     else:
-      return ZipFile(path, mode, compress)
+      return zipfile.ZipFile(path, mode, compress)
 
-  def CompressDirectory(self, irrelevant, dir_path, dir_contents):
+  def CompressDirectory(self, unused_id, dir_path, dir_contents):
     """Method to compress the given directory.
 
     This method compresses the directory 'dir_path'. It will add to an existing
@@ -206,40 +214,35 @@
     mapping of files to archives to the self.index_fp file descriptor
 
     Args:
-      irrelevant: a numeric identifier passed by the os.path.walk method, this
-        is not used by this method
-      dir_path: the path to the directory to compress
-      dir_contents: a list of directory contents to be compressed
+      unused_id: A numeric identifier passed by the os.path.walk method, this
+        is not used by this method.
+      dir_path: A string, the path to the directory to compress.
+      dir_contents: A list of directory contents to be compressed.
     """
-    
-    # construct the queue of files to be added that this method will use
+    # Construct the queue of files to be added that this method will use
     # it seems that dir_contents is given in reverse alphabetical order,
-    # so put them in alphabetical order by inserting to front of the list
+    # so put them in alphabetical order by inserting to front of the list.
     dir_contents.sort()
     zip_queue = []
-    if dir_path[len(dir_path) - 1:] == os.sep:
-      for filename in dir_contents:
-        zip_queue.append(''.join([dir_path, filename]))
-    else:
-      for filename in dir_contents:
-        zip_queue.append(''.join([dir_path, os.sep, filename]))
+    for filename in dir_contents:
+      zip_queue.append(os.path.join(dir_path, filename))
     compress_bit = zipfile.ZIP_DEFLATED
     if not self.compress:
       compress_bit = zipfile.ZIP_STORED
 
-    # zip all files in this directory, adding to existing archives and creating
-    # as necessary
-    while len(zip_queue) > 0:
+    # Zip all files in this directory, adding to existing archives and creating
+    # as necessary.
+    while zip_queue:
       target_file = zip_queue[0]
       if os.path.isfile(target_file):
         self.AddFileToArchive(target_file, compress_bit)
-        
-        # see if adding the new file made our archive too large
+
+        # See if adding the new file made our archive too large.
         if not self.ArchiveIsValid():
-          
+
           # IF fixing fails, the last added file was to large, skip it
           # ELSE the current archive filled normally, make a new one and try
-          #  adding the file again
+          #  adding the file again.
           if not self.FixArchive('SIZE'):
             zip_queue.pop(0)
           else:
@@ -248,7 +251,7 @@
                     0:self.current_archive.rfind('.zip')]) + 1)
         else:
 
-          # if this the first file in the archive, write an index record
+          # Write an index record if necessary.
           self.WriteIndexRecord()
           zip_queue.pop(0)
       else:
@@ -260,10 +263,10 @@
     Only write an index record if this is the first file to go into archive
 
     Returns:
-      True if an archive record is written, False if it isn't
+      True if an archive record is written, False if it isn't.
     """
     archive = self.OpenZipFileAtPath(
-        ''.join([self.output_dir, self.current_archive]), 'r')
+        os.path.join(self.output_dir, self.current_archive), 'r')
     archive_index = archive.infolist()
     if len(archive_index) == 1:
       self.index_fp.write(
@@ -279,54 +282,56 @@
     """Make the archive compliant.
 
     Args:
-      problem: the reason the archive is invalid
+      problem: An enum, the reason the archive is invalid.
 
     Returns:
       Whether the file(s) removed to fix the archive could conceivably be
       in an archive, but for some reason can't be added to this one.
     """
-    archive_path = ''.join([self.output_dir, self.current_archive])
-    rtn_value = None
-    
+    archive_path = os.path.join(self.output_dir, self.current_archive)
+    return_value = None
+
     if problem == 'SIZE':
       archive_obj = self.OpenZipFileAtPath(archive_path, mode='r')
       num_archive_files = len(archive_obj.infolist())
-      
+
       # IF there is a single file, that means its too large to compress,
       # delete the created archive
-      # ELSE do normal finalization
+      # ELSE do normal finalization.
       if num_archive_files == 1:
         print ('WARNING: %s%s is too large to store.' % (
             self.base_path, archive_obj.infolist()[0].filename))
         archive_obj.close()
         os.unlink(archive_path)
-        rtn_value = False
+        return_value = False
       else:
-        self.RemoveLastFile(''.join([self.output_dir, self.current_archive]))
         archive_obj.close()
+        self.RemoveLastFile(
+          os.path.join(self.output_dir, self.current_archive))
         print 'Final archive size for %s is %i' % (
-            self.current_archive, os.stat(archive_path)[stat.ST_SIZE])
-        rtn_value = True
-    return rtn_value
+            self.current_archive, os.path.getsize(archive_path))
+        return_value = True
+    return return_value
 
   def AddFileToArchive(self, filepath, compress_bit):
     """Add the file at filepath to the current archive.
 
     Args:
-      filepath: the path of the file to add
-      compress_bit: whether or not this fiel should be compressed when added
+      filepath: A string, the path of the file to add.
+      compress_bit: A boolean, whether or not this file should be compressed
+        when added.
 
     Returns:
       True if the file could be added (typically because this is a file) or
-      False if it couldn't be added (typically because its a directory)
+      False if it couldn't be added (typically because its a directory).
     """
-    curr_archive_path = ''.join([self.output_dir, self.current_archive])
-    if os.path.isfile(filepath):
-      if os.stat(filepath)[stat.ST_SIZE] > 1048576:
+    curr_archive_path = os.path.join(self.output_dir, self.current_archive)
+    if os.path.isfile(filepath) and not os.path.islink(filepath):
+      if os.path.getsize(filepath) > 1048576:
         print 'Warning: %s is potentially too large to serve on GAE' % filepath
       archive = self.OpenZipFileAtPath(curr_archive_path,
                                        compress=compress_bit)
-      # add the file to the archive
+      # Add the file to the archive.
       archive.write(filepath, filepath[len(self.base_path):])
       archive.close()
       return True
@@ -340,13 +345,22 @@
     The thought is that eventually this will do additional validation
 
     Returns:
-      True if the archive is valid, False if its not
+      True if the archive is valid, False if its not.
     """
-    archive_path = ''.join([self.output_dir, self.current_archive])
-    if os.stat(archive_path)[stat.ST_SIZE] > self.max_size:
-      return False
-    else:
-      return True
+    archive_path = os.path.join(self.output_dir, self.current_archive)
+    return os.path.getsize(archive_path) <= self.max_size
+
+
+def main(argv):
+  parser = CreateOptionsParser()
+  (options, unused_args) = parser.parse_args(args=argv[1:])
+  VerifyArguments(options, parser)
+  zipper = DirectoryZipper(options.destination,
+                           options.sourcefiles,
+                           ParseSize(options.filesize),
+                           options.compress)
+  zipper.StartCompress()
+
 
 if __name__ == '__main__':
-  Main(sys.argv)
+  main(sys.argv)
diff --git a/tools/scripts/divide_and_compress_constants.py b/tools/scripts/divide_and_compress_constants.py
index 4e11b6f..15162b7 100644
--- a/tools/scripts/divide_and_compress_constants.py
+++ b/tools/scripts/divide_and_compress_constants.py
@@ -19,42 +19,40 @@
 
 __author__ = 'jmatt@google.com (Justin Mattson)'
 
-file_preamble = ('#!/usr/bin/env python\n'
-                 '#\n'
-                 '# Copyright 2008 Google Inc.\n'
-                 '#\n'                                                       
-                 '# Licensed under the Apache License, Version 2.0 (the' 
-                 '\"License");\n'                               
-                 '# you may not use this file except in compliance with the '
-                 'License.\n'                                                 
-                 '# You may obtain a copy of the License at\n'           
-                 '#\n'
-                 '#     http://www.apache.org/licenses/LICENSE-2.0\n'
-                 '#\n'
-                 '# Unless required by applicable law or agreed to in writing,'
-                 ' software\n'                                              
-                 '# distributed under the License is distributed on an \"AS' 
-                 'IS\" BASIS,\n'
-                 '# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either '
-                 'express or implied.\n'
-                 '# See the License for the specific language governing' 
-                 ' permissions and\n'
-                 '# limitations under the License.\n'
-                 '#\n\n'
-                 'import wsgiref.handlers\n'
-                 'from google.appengine.ext import zipserve\n'
-                 'from google.appengine.ext import webapp\n'
-                 'import memcache_zipserve\n\n\n'
-                 'class MainHandler(webapp.RequestHandler):\n\n'
-                 '  def get(self):\n'
-                 '    self.response.out.write(\'Hello world!\')\n\n'
-                 'def main():\n'
-                 '  application = webapp.WSGIApplication([(\'/(.*)\','
-                 ' memcache_zipserve.create_handler([')
+file_preamble = """#!/usr/bin/env python
+#
+# Copyright 2008 Google Inc.
+#
+# 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.
+#
 
-file_endpiece = ('])),\n'
-                 '],\n'
-                 'debug=False)\n'
-                 '  wsgiref.handlers.CGIHandler().run(application)\n\n'
-                 'if __name__ == \'__main__\':\n'
-                 '  main()')
+import wsgiref.handlers\n'
+from google.appengine.ext import zipserve\n'
+from google.appengine.ext import webapp\n'
+import memcache_zipserve\n\n\n'
+class MainHandler(webapp.RequestHandler):
+
+  def get(self):
+    self.response.out.write('Hello world!')
+
+def main():
+  application = webapp.WSGIApplication(['/(.*)',
+    memcache_zipserve.create_handler(["""
+
+file_endpiece = """])),
+],
+debug=False)
+  wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == __main__:
+  main()"""
diff --git a/tools/scripts/test_divide_and_compress.py b/tools/scripts/divide_and_compress_test.py
similarity index 97%
rename from tools/scripts/test_divide_and_compress.py
rename to tools/scripts/divide_and_compress_test.py
index d0d27b3..426449a 100755
--- a/tools/scripts/test_divide_and_compress.py
+++ b/tools/scripts/divide_and_compress_test.py
@@ -17,7 +17,7 @@
 
 """Tests for divide_and_compress.py.
 
-TODO: Add tests for module methods.
+TODO(jmatt): Add tests for module methods.
 """
 
 __author__ = 'jmatt@google.com (Justin Mattson)'
@@ -26,10 +26,9 @@
 import stat
 import unittest
 import zipfile
-from zipfile import ZipFile
 
 import divide_and_compress
-from mox import mox
+import mox
 
 
 class BagOfParts(object):
@@ -58,6 +57,10 @@
                       'sdjfljkgsc n;iself')
     self.files = {'file1': file1, 'file2': file2}
 
+  def tearDown(self):
+    """Remove any stubs we've created."""
+    self.my_mox.UnsetStubs()
+
   def testArchiveIsValid(self):
     """Test the DirectoryZipper.ArchiveIsValid method.
 
@@ -119,7 +122,7 @@
       A configured mocked
     """
     
-    source_zip = self.my_mox.CreateMock(ZipFile)
+    source_zip = self.my_mox.CreateMock(zipfile.ZipFile)
     source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
     source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
     source_zip.read(self.files['file1'].filename).AndReturn(
@@ -137,16 +140,12 @@
       A configured mocked
     """
     
-    dest_zip = mox.MockObject(ZipFile)
+    dest_zip = mox.MockObject(zipfile.ZipFile)
     dest_zip.writestr(self.files['file1'].filename,
                       self.files['file1'].contents)
     dest_zip.close()
     return dest_zip
 
-  def tearDown(self):
-    """Remove any stubs we've created."""
-    self.my_mox.UnsetStubs()
-
 
 class FixArchiveTests(unittest.TestCase):
   """Tests for the DirectoryZipper.FixArchive method."""
@@ -158,6 +157,10 @@
     self.file1.filename = 'file1.txt'
     self.file1.contents = 'This is a test file'
 
+  def tearDown(self):
+    """Unset any mocks that we've created."""
+    self.my_mox.UnsetStubs()
+
   def _InitMultiFileData(self):
     """Create an array of mock file objects.
 
@@ -211,7 +214,7 @@
     Returns:
       A configured mock object
     """
-    mock_zip = self.my_mox.CreateMock(ZipFile)
+    mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
     mock_zip.infolist().AndReturn([self.file1])
     mock_zip.infolist().AndReturn([self.file1])
     mock_zip.close()
@@ -250,15 +253,11 @@
       A configured mock object
     """
     self._InitMultiFileData()
-    mock_zip = self.my_mox.CreateMock(ZipFile)
+    mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
     mock_zip.infolist().AndReturn(self.multi_file_dir)
     mock_zip.close()
     return mock_zip
 
-  def tearDown(self):
-    """Unset any mocks that we've created."""
-    self.my_mox.UnsetStubs()
-
 
 class AddFileToArchiveTest(unittest.TestCase):
   """Test behavior of method to add a file to an archive."""
@@ -270,6 +269,9 @@
     self.file_to_add = 'file.txt'
     self.input_dir = '/foo/bar/baz/'
 
+  def tearDown(self):
+    self.my_mox.UnsetStubs()
+
   def testAddFileToArchive(self):
     """Test the DirectoryZipper.AddFileToArchive method.
 
@@ -312,15 +314,12 @@
     Returns:
       A configured mock object
     """
-    archive_mock = self.my_mox.CreateMock(ZipFile)
+    archive_mock = self.my_mox.CreateMock(zipfile.ZipFile)
     archive_mock.write(''.join([self.input_dir, self.file_to_add]),
                        self.file_to_add)
     archive_mock.close()
     return archive_mock
 
-  def tearDown(self):
-    self.my_mox.UnsetStubs()
-
 
 class CompressDirectoryTest(unittest.TestCase):
   """Test the master method of the class.
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
index a3da70e..5efd553 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
@@ -28,7 +28,7 @@
     /** String used to get a hash to the platform target */
     private final static String PLATFORM_HASH = "android-%d";
     
-    private final static String PLATFORM_VENDOR = "Android";
+    private final static String PLATFORM_VENDOR = "Android Open Source Project";
     private final static String PLATFORM_NAME = "Android %s";
 
     private final String mLocation;