Remote TF refactor part one.

Clean up current remote manager protocol to use JSON encoded payloads, and
set the stage for splitting out client into a separate library.

Bug: 10919026

Change-Id: Ie6ecc8f6fa42c133a7e2b93861d74115ae8e4a64
diff --git a/.classpath b/.classpath
index b56c2ca..ae5e1f9 100644
--- a/.classpath
+++ b/.classpath
@@ -7,6 +7,7 @@
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jline-1.0_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/jline/src"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/junit/src"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/Android.mk b/Android.mk
index 51c4370..eedf707 100644
--- a/Android.mk
+++ b/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE := tradefed
 
 LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 guavalib jline-1.0
+LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 guavalib jline-1.0 json-prebuilt
 # emmalib is only a runtime dependency if generating code coverage reporters,
 # not a compile time dependency
 LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt emmalib tools-common-prebuilt
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 5dffe88..0f80fec 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -19,6 +19,8 @@
 import com.android.ddmlib.DdmPreferences;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.command.remote.RemoteClient;
+import com.android.tradefed.command.remote.RemoteManager;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
 import com.android.tradefed.config.GlobalConfiguration;
@@ -830,7 +832,7 @@
             CLog.d("connected to remote manager at %d", handoverPort);
             // inform remote manager of the devices we are still using
             for (String deviceInUse : getDeviceManager().getAllocatedDevices()) {
-                if (!mRemoteClient.sendFilterDevice(deviceInUse)) {
+                if (!mRemoteClient.sendAllocateDevice(deviceInUse)) {
                     CLog.e("Failed to send command to remote manager");
                     return false;
                 }
@@ -874,7 +876,7 @@
         // TODO: send freed device state too
         if (mRemoteClient != null) {
             try {
-                mRemoteClient.sendUnfilterDevice(device.getSerialNumber());
+                mRemoteClient.sendFreeDevice(device.getSerialNumber());
             } catch (IOException e) {
                 CLog.e("Failed to send unfilter device %s to remote manager",
                         device.getSerialNumber());
diff --git a/src/com/android/tradefed/command/RemoteClient.java b/src/com/android/tradefed/command/RemoteClient.java
deleted file mode 100644
index d81390c..0000000
--- a/src/com/android/tradefed/command/RemoteClient.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.command;
-
-import com.android.tradefed.util.ArrayUtil;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-
-/**
- * Class for sending remote commands to another TF process via sockets.
- */
-public class RemoteClient {
-
-    private final Socket mSocket;
-    private final PrintWriter mWriter;
-    private final BufferedReader mReader;
-
-    /**
-     * @param port
-     * @throws IOException
-     * @throws UnknownHostException
-     */
-    RemoteClient(int port) throws UnknownHostException, IOException {
-        String hostName = InetAddress.getLocalHost().getHostName();
-        mSocket = new Socket(hostName, port);
-        mWriter = new PrintWriter(mSocket.getOutputStream(), true);
-        mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
-    }
-
-    private synchronized boolean sendCommand(String... cmd) throws IOException {
-        // TODO: use a more standard data protocol - such as Json
-        mWriter.println(ArrayUtil.join(RemoteManager.DELIM, (Object[])cmd));
-        String response = mReader.readLine();
-        return response != null && Boolean.parseBoolean(response);
-    }
-
-    public static RemoteClient connect(int port) throws UnknownHostException, IOException {
-        return new RemoteClient(port);
-    }
-
-    /**
-     * Send a 'add this device to global ignore filter' command
-     * @param serial
-     * @throws IOException
-     */
-    public boolean sendFilterDevice(String serial) throws IOException {
-        return sendCommand(RemoteManager.FILTER, serial);
-    }
-
-    /**
-     * Send a 'remove this device from global ignore filter' command
-     * @param serial
-     * @throws IOException
-     */
-    public boolean sendUnfilterDevice(String serial) throws IOException {
-        return sendCommand(RemoteManager.UNFILTER, serial);
-    }
-
-    /**
-     * Send a 'add command' command.
-     *
-     * @param commandArgs
-     */
-    public boolean sendAddCommand(long totalTime, String... commandArgs) throws IOException {
-        String[] fullList = ArrayUtil.buildArray(new String[] {RemoteManager.ADD_COMMAND,
-                Long.toString(totalTime)}, commandArgs);
-        return sendCommand(fullList);
-    }
-
-    /**
-     * Send a 'close connection' command
-     *
-     * @throws IOException
-     */
-    public boolean sendClose() throws IOException {
-        return sendCommand(RemoteManager.CLOSE);
-    }
-
-    /**
-     * Close the connection to the {@link RemoteManager}.
-     */
-    public synchronized void close() {
-        if (mSocket != null) {
-             try {
-                mSocket.close();
-            } catch (IOException e) {
-                // ignore
-            }
-        }
-        if (mWriter != null) {
-            mWriter.close();
-        }
-    }
-}
-
diff --git a/src/com/android/tradefed/command/remote/AddCommandOp.java b/src/com/android/tradefed/command/remote/AddCommandOp.java
new file mode 100644
index 0000000..268b115
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/AddCommandOp.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation for adding a command to the local tradefed scheduler.
+ */
+class AddCommandOp extends RemoteOperation {
+
+    private static final String COMMAND_ARGS = "commandArgs";
+    private static final String TIME = "time";
+    long mTotalTime;
+    String[] mCommandArgs;
+
+    AddCommandOp() {
+        this(0, new String[]{});
+    }
+
+    AddCommandOp(long totalTime, String... commandArgs) {
+        mTotalTime = totalTime;
+        mCommandArgs = commandArgs;
+    }
+
+    @Override
+    protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+        mTotalTime = json.getLong(TIME);
+        JSONArray jsonArgs = json.getJSONArray(COMMAND_ARGS);
+        mCommandArgs = new String[jsonArgs.length()];
+        for (int i=0; i < mCommandArgs.length; i++) {
+            mCommandArgs[i] = jsonArgs.getString(i);
+        }
+    }
+
+
+    @Override
+    protected OperationType getType() {
+        return OperationType.ADD_COMMAND;
+    }
+
+    @Override
+    protected void packIntoJson(JSONObject j) throws JSONException {
+        j.put(TIME, mTotalTime);
+        JSONArray jsonArgs = new JSONArray();
+        for (String arg : mCommandArgs) {
+            jsonArgs.put(arg);
+        }
+        j.put(COMMAND_ARGS, jsonArgs);
+    }
+}
diff --git a/src/com/android/tradefed/command/remote/AllocateDeviceOp.java b/src/com/android/tradefed/command/remote/AllocateDeviceOp.java
new file mode 100644
index 0000000..8f1a6e2
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/AllocateDeviceOp.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation for allocating a device.
+ * <p/>
+ * Currently used to mark the device as in use by another tradefed process in handover situation.
+ */
+class AllocateDeviceOp extends RemoteOperation {
+
+    private static final String SERIAL = "serial";
+    String mDeviceSerial;
+
+    AllocateDeviceOp(String serial) {
+        mDeviceSerial = serial;
+    }
+
+    AllocateDeviceOp() {
+        this(null);
+    }
+
+    @Override
+    protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+        mDeviceSerial = json.getString(SERIAL);
+    }
+
+
+    @Override
+    protected OperationType getType() {
+        return OperationType.ALLOCATE_DEVICE;
+    }
+
+    @Override
+    protected void packIntoJson(JSONObject j) throws JSONException {
+        j.put(SERIAL, mDeviceSerial);
+    }
+
+}
diff --git a/src/com/android/tradefed/command/remote/CloseOp.java b/src/com/android/tradefed/command/remote/CloseOp.java
new file mode 100644
index 0000000..4011388
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/CloseOp.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation that instructs the remote manager to shut down. Currently used when
+ * handover process is complete.
+ */
+class CloseOp extends RemoteOperation {
+
+    @Override
+    protected void unpackFromJson(JSONObject json) throws RemoteException {
+        // nothing to do
+    }
+
+    @Override
+    protected OperationType getType() {
+        return OperationType.CLOSE;
+    }
+
+    @Override
+    protected void packIntoJson(JSONObject j) throws JSONException {
+        // nothing to do
+    }
+}
diff --git a/src/com/android/tradefed/command/remote/DeviceTracker.java b/src/com/android/tradefed/command/remote/DeviceTracker.java
new file mode 100644
index 0000000..1702e40
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/DeviceTracker.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * Singleton class that tracks devices that have been remotely allocated.
+ */
+class DeviceTracker {
+
+    /**
+    * Use on demand holder idiom
+    * @see http://en.wikipedia.org/wiki/Singleton_pattern#The_solution_of_Bill_Pugh
+    */
+    private static class SingletonHolder {
+        public static final DeviceTracker cInstance = new DeviceTracker();
+    }
+
+    public static DeviceTracker getInstance() {
+        return SingletonHolder.cInstance;
+    }
+
+    // private constructor - don't allow instantiation
+    private DeviceTracker() {
+    }
+
+    // use Hashtable since its thread-safe
+    private Map<String, ITestDevice> mAllocatedDeviceMap = new Hashtable<String, ITestDevice>();
+
+    /**
+     * Mark given device as remotely allocated.
+     */
+    public void allocateDevice(ITestDevice d) {
+        mAllocatedDeviceMap.put(d.getSerialNumber(), d);
+    }
+
+    /**
+     * Mark given device serial as freed.
+     *
+     * @return the corresponding {@link ITestDevice} or <code>null</code> if device with given
+     *         serial cannot be found
+     */
+    public ITestDevice freeDevice(String serial) {
+        return mAllocatedDeviceMap.remove(serial);
+    }
+
+    /**
+     * Mark all remotely allocated devices as freed.
+     *
+     * @return a {@link Collection} of all remotely allocated devices
+     */
+    public Collection<ITestDevice> freeAll() {
+        Collection<ITestDevice> devices = new ArrayList<ITestDevice>(mAllocatedDeviceMap.values());
+        mAllocatedDeviceMap.clear();
+        return devices;
+    }
+}
diff --git a/src/com/android/tradefed/command/remote/FreeDeviceOp.java b/src/com/android/tradefed/command/remote/FreeDeviceOp.java
new file mode 100644
index 0000000..c2f7054
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/FreeDeviceOp.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A remote operation for freeing a previously remotely allocated device.
+ */
+class FreeDeviceOp extends RemoteOperation {
+
+    private static final String SERIAL = "serial";
+
+    static final String ALL_DEVICES = "*";
+
+    String mDeviceSerial;
+
+    FreeDeviceOp(String serial) {
+        mDeviceSerial = serial;
+    }
+
+    FreeDeviceOp() {
+        this(null);
+    }
+
+    @Override
+    protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+        mDeviceSerial = json.getString(SERIAL);
+    }
+
+    @Override
+    protected OperationType getType() {
+        return OperationType.FREE_DEVICE;
+    }
+
+    @Override
+    protected void packIntoJson(JSONObject j) throws JSONException {
+        j.put(SERIAL, mDeviceSerial);
+    }
+}
diff --git a/src/com/android/tradefed/command/remote/RemoteClient.java b/src/com/android/tradefed/command/remote/RemoteClient.java
new file mode 100644
index 0000000..62fdab1
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/RemoteClient.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import com.android.tradefed.command.remote.RemoteOperation.RemoteException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * Class for sending remote commands to another TF process.
+ * <p/>
+ * Currently uses JSON-encoded data sent via sockets.
+ */
+public class RemoteClient {
+
+    private final Socket mSocket;
+    private final PrintWriter mWriter;
+    private final BufferedReader mReader;
+
+    /**
+     * Initialize the {@RemoteClient}, and instruct it to connect to the given port on
+     * localhost.
+     *
+     * @param port the tcp/ip port number
+     * @throws IOException
+     * @throws UnknownHostException
+     */
+    RemoteClient(int port) throws UnknownHostException, IOException {
+        String hostName = InetAddress.getLocalHost().getHostName();
+        mSocket = new Socket(hostName, port);
+        mWriter = new PrintWriter(mSocket.getOutputStream(), true);
+        mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
+    }
+
+    /**
+     * Send the given command to the remote TF.
+     *
+     * @param cmd the {@link RemoteOperation} to send
+     * @return true if command was sent and processed successfully by remote TF
+     */
+    private synchronized boolean sendCommand(RemoteOperation cmd) {
+       try {
+           mWriter.println(cmd.pack());
+           String response = mReader.readLine();
+           return response != null && Boolean.parseBoolean(response);
+       } catch (RemoteException e) {
+           CLog.e("Failed to send remote commmand", e);
+       } catch (IOException e) {
+           CLog.e("Failed to send remote commmand", e);
+       }
+       return false;
+    }
+
+    /**
+     * Helper method to create a {@link RemoteClient} connected to given port
+     *
+     * @param port the tcp/ip port
+     * @return the {@link RemoteClient}
+     * @throws UnknownHostException
+     * @throws IOException
+     */
+    public static RemoteClient connect(int port) throws UnknownHostException, IOException {
+        return new RemoteClient(port);
+    }
+
+    /**
+     * Send a 'allocate device' command
+     *
+     * @param serial
+     * @throws IOException
+     */
+    public boolean sendAllocateDevice(String serial) throws IOException {
+        return sendCommand(new AllocateDeviceOp(serial));
+    }
+
+    /**
+     * Send a 'free previously allocated device' command
+     * @param serial
+     * @throws IOException
+     */
+    public boolean sendFreeDevice(String serial) throws IOException {
+        return sendCommand(new FreeDeviceOp(serial));
+    }
+
+    /**
+     * Send a 'add command' command.
+     *
+     * @param commandArgs
+     */
+    public boolean sendAddCommand(long totalTime, String... commandArgs) throws IOException {
+        return sendCommand(new AddCommandOp(totalTime, commandArgs));
+    }
+
+    /**
+     * Send a 'close connection' command
+     *
+     * @throws IOException
+     */
+    public boolean sendClose() throws IOException {
+        return sendCommand(new CloseOp());
+    }
+
+    /**
+     * Close the connection to the {@link RemoteManager}.
+     */
+    public synchronized void close() {
+        if (mSocket != null) {
+             try {
+                mSocket.close();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        StreamUtil.close(mWriter);
+    }
+}
+
diff --git a/src/com/android/tradefed/command/RemoteManager.java b/src/com/android/tradefed/command/remote/RemoteManager.java
similarity index 61%
rename from src/com/android/tradefed/command/RemoteManager.java
rename to src/com/android/tradefed/command/remote/RemoteManager.java
index a9363f4..9ca8b40 100644
--- a/src/com/android/tradefed/command/RemoteManager.java
+++ b/src/com/android/tradefed/command/remote/RemoteManager.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tradefed.command;
+package com.android.tradefed.command.remote;
 
-import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.command.ICommandScheduler;
+import com.android.tradefed.command.remote.RemoteOperation.RemoteException;
 import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
 import com.android.tradefed.device.ITestDevice;
@@ -29,12 +30,9 @@
 import java.io.PrintWriter;
 import java.net.ServerSocket;
 import java.net.Socket;
-import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.Map;
 
 /**
- * Class that receives remote commands to add and remove devices from use via a socket.
+ * Class that receives {@link RemoteOperation}s via a socket.
  * <p/>
  * Currently accepts only one remote connection at one time, and processes incoming commands
  * serially.
@@ -49,19 +47,10 @@
  */
 public class RemoteManager extends Thread {
 
-    // constants that define wire protocol between RemoteClient and RemoteManager
-    static final String DELIM = ";";
-    static final String FILTER = "filter";
-    static final String UNFILTER = "unfilter";
-    static final String ALL_DEVICES = "*";
-    static final String CLOSE = "close";
-    static final String ADD_COMMAND = "add_command";
-
     private ServerSocket mServerSocket = null;
     private boolean mCancel = false;
     private final IDeviceManager mDeviceManager;
     private final ICommandScheduler mScheduler;
-    private Map<String, ITestDevice> mFilteredDeviceMap = new Hashtable<String, ITestDevice>();
 
     /**
      * Creates a {@link RemoteManager}.
@@ -130,7 +119,7 @@
                 clientSocket = serverSocket.accept();
                 in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                 out = new PrintWriter(clientSocket.getOutputStream(), true);
-                processClientCommands(in, out);
+                processClientOperations(in, out);
             } catch (IOException e) {
                 CLog.e("Failed to accept connection: %s", e);
             } finally {
@@ -141,91 +130,84 @@
         }
     }
 
-    private void processClientCommands(BufferedReader in, PrintWriter out) throws IOException {
+    private void processClientOperations(BufferedReader in, PrintWriter out) throws IOException {
         String line = null;
         while ((line = in.readLine()) != null && !mCancel) {
             boolean result = false;
-            String[] commandSegments = line.split(DELIM);
-            String cmdType = commandSegments[0];
-            if (FILTER.equals(cmdType)) {
-                result = processFilterCommand(commandSegments);
-            } else if (UNFILTER.equals(cmdType)) {
-                result = processUnfilterCommand(commandSegments);
-            } else if (CLOSE.equals(cmdType)) {
-                cancel();
-                result = true;
-            } else if (ADD_COMMAND.equals(cmdType)) {
-                result = processAddCommand(commandSegments);
+            RemoteOperation rc;
+            try {
+                rc = RemoteOperation.createRemoteOpFromString(line);
+                switch (rc.getType()) {
+                    case ADD_COMMAND:
+                        result = processAdd((AddCommandOp)rc);
+                        break;
+                    case CLOSE:
+                        result = processClose((CloseOp)rc);
+                        break;
+                    case ALLOCATE_DEVICE:
+                        result = processAllocate((AllocateDeviceOp)rc);
+                        break;
+                    case FREE_DEVICE:
+                        result = processFree((FreeDeviceOp)rc);
+                        break;
+                }
+            } catch (RemoteException e) {
+                CLog.e("Failed to handle remote command", e);
             }
             sendAck(result, out);
         }
     }
 
-    private boolean processFilterCommand(final String[] commandSegments) {
-        if (commandSegments.length < 2) {
-            CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
-            return false;
-        }
-        final String serial = commandSegments[1];
-        ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(serial);
+    private boolean processAllocate(AllocateDeviceOp c) {
+        ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(c.mDeviceSerial);
         if (allocatedDevice != null) {
-            Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
-                    String.format("Allocating device %s that is still in use by remote tradefed",
-                            serial));
-            mFilteredDeviceMap.put(serial, allocatedDevice);
+            CLog.logAndDisplay(LogLevel.INFO,
+                    "Allocating device %s that is still in use by remote tradefed",
+                            c.mDeviceSerial);
+            DeviceTracker.getInstance().allocateDevice(allocatedDevice);
             return true;
-        } else {
-            CLog.e("Failed to allocate remote device %s", serial);
-            return false;
         }
+        CLog.e("Failed to allocate device %s", c.mDeviceSerial);
+        return false;
     }
 
-    private boolean processUnfilterCommand(final String[] commandSegments) {
-        if (commandSegments.length < 2) {
-            CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
-            return false;
-        }
-        // TODO: consider making this synchronous, and sending ack back to client once allocated
-        final String serial = commandSegments[1];
-        if (ALL_DEVICES.equals(serial)) {
+    private boolean processFree(FreeDeviceOp c) {
+        if (FreeDeviceOp.ALL_DEVICES.equals(c.mDeviceSerial)) {
             freeAllDevices();
             return true;
         } else {
-            ITestDevice d = mFilteredDeviceMap.remove(serial);
+            ITestDevice d = DeviceTracker.getInstance().freeDevice(c.mDeviceSerial);
             if (d != null) {
-                Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
-                        String.format("Freeing device %s no longer in use by remote tradefed",
-                                serial));
+                CLog.logAndDisplay(LogLevel.INFO,
+                        "Freeing device %s no longer in use by remote tradefed",
+                                c.mDeviceSerial);
                 mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
                 return true;
             } else {
-                CLog.w("Could not find device to free %s", serial);
+                CLog.w("Could not find device to free %s", c.mDeviceSerial);
             }
         }
         return false;
     }
 
-    private boolean processAddCommand(final String[] commandSegments) {
-        if (commandSegments.length < 3) {
-            CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
-            return false;
-        }
-        long totalTime = Long.parseLong(commandSegments[1]);
-        String[] cmdArgs = Arrays.copyOfRange(commandSegments, 2, commandSegments.length);
-        Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
-                String.format("Adding command '%s'", ArrayUtil.join(" ", (Object[])cmdArgs)));
-        return mScheduler.addCommand(cmdArgs, totalTime);
+    boolean processAdd(AddCommandOp c) {
+        CLog.logAndDisplay(LogLevel.INFO, "Adding command '%s'", ArrayUtil.join(" ",
+                c.mCommandArgs));
+        return mScheduler.addCommand(c.mCommandArgs, c.mTotalTime);
+    }
+
+    private boolean processClose(CloseOp rc) {
+        cancel();
+        return true;
     }
 
     private void freeAllDevices() {
-        for (ITestDevice d : mFilteredDeviceMap.values()) {
-            Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
-                    String.format("Freeing device %s no longer in use by remote tradefed",
-                            d.getSerialNumber()));
-
+        for (ITestDevice d : DeviceTracker.getInstance().freeAll()) {
+            CLog.logAndDisplay(LogLevel.INFO,
+                    "Freeing device %s no longer in use by remote tradefed",
+                            d.getSerialNumber());
             mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
         }
-        mFilteredDeviceMap.clear();
     }
 
     private void sendAck(boolean result, PrintWriter out) {
@@ -238,7 +220,7 @@
     public synchronized void cancel() {
         if (!mCancel) {
             mCancel  = true;
-            Log.logAndDisplay(LogLevel.INFO, "RemoteManager", "Closing remote manager");
+            CLog.logAndDisplay(LogLevel.INFO, "Closing remote manager");
         }
     }
 
diff --git a/src/com/android/tradefed/command/remote/RemoteOperation.java b/src/com/android/tradefed/command/remote/RemoteOperation.java
new file mode 100644
index 0000000..91f70f9
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/RemoteOperation.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates data for a remote operation sent over the wire.
+ */
+abstract class RemoteOperation {
+    private static final String TYPE = "type";
+    private static final String VERSION = "version";
+    static final int CURRENT_PROTOCOL_VERSION = 1;
+
+    @SuppressWarnings("serial")
+    static class RemoteException extends Exception {
+        RemoteException(Throwable t) {
+            super(t);
+        }
+
+        RemoteException(String msg) {
+            super(msg);
+        }
+    }
+
+    /**
+     * Represents all types of remote operations that can be performed
+     */
+    enum OperationType {
+        ALLOCATE_DEVICE, FREE_DEVICE, CLOSE, ADD_COMMAND
+    }
+
+    /**
+     * Create and populate a {@link RemoteOperation} from given data.
+     *
+     * @param data the data to parse
+     * @throws RemoteException
+     */
+    final static RemoteOperation createRemoteOpFromString(String data) throws RemoteException {
+        try {
+            JSONObject jsonData = new JSONObject(data);
+            int protocolVersion = jsonData.getInt(VERSION);
+            // to keep things simple for now, just barf when protocol version is unknown
+            if (protocolVersion != CURRENT_PROTOCOL_VERSION) {
+                throw new RemoteException(String.format(
+                        "Remote operation has unknown version '%d'. Expected '%d'",
+                        protocolVersion, CURRENT_PROTOCOL_VERSION));
+            }
+            OperationType op = OperationType.valueOf(jsonData.getString(TYPE));
+            RemoteOperation rc = null;
+            switch (op) {
+                case ALLOCATE_DEVICE:
+                    rc = new AllocateDeviceOp();
+                    break;
+                case FREE_DEVICE:
+                    rc = new FreeDeviceOp();
+                    break;
+                case CLOSE:
+                    rc = new CloseOp();
+                    break;
+                case ADD_COMMAND:
+                    rc = new AddCommandOp();
+                    break;
+                default:
+                    throw new RemoteException(String.format("unknown remote command '%s'", data));
+
+            }
+            rc.unpackFromJson(jsonData);
+            return rc;
+        } catch (JSONException e) {
+            throw new RemoteException(e);
+        }
+    }
+
+    protected abstract OperationType getType();
+
+    /**
+     * Abstract method to allow sub-classes to parse additional data from payload.
+     *
+     * @param json
+     * @throws RemoteException, JSONException
+     */
+    protected abstract void unpackFromJson(JSONObject json) throws RemoteException, JSONException;
+
+    /**
+     * Return the RemoteCommand data in its wire protocol format
+     * @return
+     */
+     String pack() throws RemoteException {
+         JSONObject j = new JSONObject();
+         try {
+             j.put(VERSION, CURRENT_PROTOCOL_VERSION);
+             j.put(TYPE, getType().toString());
+             packIntoJson(j);
+         } catch (JSONException e) {
+             CLog.e("Failed to serialize RemoteOperation", e);
+         }
+         return j.toString();
+     }
+
+     /**
+      * Callback to add subclass specific data to the JSON object
+      * @param j
+      * @throws JSONException
+      */
+    protected abstract void packIntoJson(JSONObject j) throws JSONException;
+
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 60cdf55..d9476c1 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,7 +26,7 @@
 import com.android.tradefed.command.CommandFileParserTest;
 import com.android.tradefed.command.CommandSchedulerTest;
 import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.RemoteManagerTest;
+import com.android.tradefed.command.remote.RemoteManagerTest;
 import com.android.tradefed.config.ArgsOptionParserTest;
 import com.android.tradefed.config.ConfigurationDefTest;
 import com.android.tradefed.config.ConfigurationFactoryTest;
diff --git a/tests/src/com/android/tradefed/command/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/RemoteManagerTest.java
deleted file mode 100644
index ae3dfd4..0000000
--- a/tests/src/com/android/tradefed/command/RemoteManagerTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.command;
-
-import com.android.tradefed.device.IDeviceManager;
-import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
-import com.android.tradefed.device.ITestDevice;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-/**
- * Unit tests for {@link RemoteManager}.
- */
-public class RemoteManagerTest extends TestCase {
-
-    private IDeviceManager mMockDeviceManager;
-    private RemoteManager mRemoteMgr;
-    private RemoteClient mRemoteClient;
-    private ICommandScheduler mMockScheduler;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mMockDeviceManager = EasyMock.createMock(IDeviceManager.class);
-        mMockScheduler = EasyMock.createMock(ICommandScheduler.class);
-        mRemoteMgr = new RemoteManager(mMockDeviceManager, mMockScheduler);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        if (mRemoteClient != null) {
-            mRemoteClient.close();
-        }
-        if (mRemoteMgr != null) {
-            mRemoteMgr.cancel();
-        }
-        super.tearDown();
-    }
-
-    /**
-     * An integration test for client-manager interaction, that will filter, then unfilter a device.
-     */
-    public void testFilterUnfilter() throws Exception {
-        ITestDevice device = EasyMock.createMock(ITestDevice.class);
-        EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
-        mMockDeviceManager.freeDevice(EasyMock.eq(device),
-                EasyMock.eq(FreeDeviceState.AVAILABLE));
-
-        EasyMock.replay(mMockDeviceManager, device);
-        mRemoteMgr.start();
-        int port = mRemoteMgr.getPort();
-        assertTrue(port != -1);
-        mRemoteClient = RemoteClient.connect(port);
-        assertTrue(mRemoteClient.sendFilterDevice("serial"));
-        assertTrue(mRemoteClient.sendUnfilterDevice("serial"));
-        EasyMock.verify(mMockDeviceManager);
-    }
-}
diff --git a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
new file mode 100644
index 0000000..32bce6f
--- /dev/null
+++ b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command.remote;
+
+import com.android.tradefed.command.ICommandScheduler;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
+import com.android.tradefed.device.ITestDevice;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
+/**
+ * Unit tests for {@link RemoteManager}.
+ */
+public class RemoteManagerTest extends TestCase {
+
+    private IDeviceManager mMockDeviceManager;
+    private RemoteManager mRemoteMgr;
+    private RemoteClient mRemoteClient;
+    private ICommandScheduler mMockScheduler;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mMockDeviceManager = EasyMock.createMock(IDeviceManager.class);
+        mMockScheduler = EasyMock.createMock(ICommandScheduler.class);
+        mRemoteMgr = new RemoteManager(mMockDeviceManager, mMockScheduler);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mRemoteClient != null) {
+            mRemoteClient.close();
+        }
+        if (mRemoteMgr != null) {
+            mRemoteMgr.cancel();
+        }
+        super.tearDown();
+    }
+
+    /**
+     * An integration test for client-manager interaction, that will allocate, then free a device.
+     */
+    public void testAllocateFree() throws Exception {
+        ITestDevice device = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+        mMockDeviceManager.freeDevice(EasyMock.eq(device),
+                EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+        EasyMock.replay(mMockDeviceManager, device);
+        mRemoteMgr.start();
+        int port = mRemoteMgr.getPort();
+        assertTrue(port != -1);
+        mRemoteClient = RemoteClient.connect(port);
+        assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+        assertTrue(mRemoteClient.sendFreeDevice("serial"));
+        EasyMock.verify(mMockDeviceManager);
+    }
+
+    /**
+     * An integration test for client-manager interaction, that will add a command
+     */
+    public void testAddCommand() throws Exception {
+        EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(new String[] {
+                "arg1", "arg2"
+        }), EasyMock.anyInt())).andReturn(true);
+
+        EasyMock.replay(mMockScheduler);
+        mRemoteMgr.start();
+        int port = mRemoteMgr.getPort();
+        assertTrue(port != -1);
+        mRemoteClient = RemoteClient.connect(port);
+        assertTrue(mRemoteClient.sendAddCommand(3, "arg1", "arg2"));
+        EasyMock.verify(mMockScheduler);
+    }
+
+    /**
+     * An integration test for client-manager interaction, that will allocate, then close the
+     * connection. Verifies that closing frees all devices.
+     */
+    public void testAllocateClose() throws Exception {
+        ITestDevice device = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+        mMockDeviceManager.freeDevice(EasyMock.eq(device),
+                EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+        EasyMock.replay(mMockDeviceManager, device);
+        mRemoteMgr.start();
+        int port = mRemoteMgr.getPort();
+        assertTrue(port != -1);
+        mRemoteClient = RemoteClient.connect(port);
+        assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+        assertTrue(mRemoteClient.sendClose());
+        mRemoteClient.close();
+        mRemoteMgr.join();
+        EasyMock.verify(mMockDeviceManager);
+    }
+
+    /**
+     * An integration test for client-manager interaction, that will allocate, then frees all
+     * devices.
+     */
+    public void testAllocateFreeAll() throws Exception {
+        ITestDevice device = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+        mMockDeviceManager.freeDevice(EasyMock.eq(device),
+                EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+        EasyMock.replay(mMockDeviceManager, device);
+        mRemoteMgr.start();
+        int port = mRemoteMgr.getPort();
+        assertTrue(port != -1);
+        mRemoteClient = RemoteClient.connect(port);
+        assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+        assertTrue(mRemoteClient.sendFreeDevice("*"));
+        EasyMock.verify(mMockDeviceManager);
+    }
+
+}