am 45470559: (-s ours) am 12b7da69: am 6df477be: Merge "Rename (IF_)LOGE(_IF) to (IF_)ALOGE(_IF)"

* commit '45470559cd4a499fd5d38cf708f0b531dc081fed':
  Rename (IF_)LOGE(_IF) to (IF_)ALOGE(_IF)
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index aa6400b..1a74abf 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -45,7 +45,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(mType, mSubType, mSubscriberId);
+        return Objects.hashCode(mType, mSubType, mSubscriberId, mRoaming);
     }
 
     @Override
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index f6e627c..e8f60b4 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -16,8 +16,6 @@
 
 package android.net;
 
-import static com.android.internal.util.Preconditions.checkNotNull;
-
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
@@ -40,8 +38,6 @@
  * @hide
  */
 public class NetworkStats implements Parcelable {
-    private static final String TAG = "NetworkStats";
-
     /** {@link #iface} value when interface details unavailable. */
     public static final String IFACE_ALL = null;
     /** {@link #uid} value when UID details unavailable. */
@@ -463,62 +459,64 @@
      * between two snapshots in time. Assumes that statistics rows collect over
      * time, and that none of them have disappeared.
      */
-    public NetworkStats subtract(NetworkStats value) throws NonMonotonicException {
-        return subtract(value, false);
+    public NetworkStats subtract(NetworkStats right) {
+        return subtract(this, right, null);
     }
 
     /**
-     * Subtract the given {@link NetworkStats}, effectively leaving the delta
+     * Subtract the two given {@link NetworkStats} objects, returning the delta
      * between two snapshots in time. Assumes that statistics rows collect over
      * time, and that none of them have disappeared.
-     *
-     * @param clampNonMonotonic When non-monotonic stats are found, just clamp
-     *            to 0 instead of throwing {@link NonMonotonicException}.
+     * <p>
+     * If counters have rolled backwards, they are clamped to {@code 0} and
+     * reported to the given {@link NonMonotonicObserver}.
      */
-    public NetworkStats subtract(NetworkStats value, boolean clampNonMonotonic)
-            throws NonMonotonicException {
-        final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime;
+    public static NetworkStats subtract(
+            NetworkStats left, NetworkStats right, NonMonotonicObserver observer) {
+        long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime;
         if (deltaRealtime < 0) {
-            throw new NonMonotonicException(this, value);
+            if (observer != null) {
+                observer.foundNonMonotonic(left, -1, right, -1);
+            }
+            deltaRealtime = 0;
         }
 
         // result will have our rows, and elapsed time between snapshots
         final Entry entry = new Entry();
-        final NetworkStats result = new NetworkStats(deltaRealtime, size);
-        for (int i = 0; i < size; i++) {
-            entry.iface = iface[i];
-            entry.uid = uid[i];
-            entry.set = set[i];
-            entry.tag = tag[i];
+        final NetworkStats result = new NetworkStats(deltaRealtime, left.size);
+        for (int i = 0; i < left.size; i++) {
+            entry.iface = left.iface[i];
+            entry.uid = left.uid[i];
+            entry.set = left.set[i];
+            entry.tag = left.tag[i];
 
             // find remote row that matches, and subtract
-            final int j = value.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i);
+            final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i);
             if (j == -1) {
                 // newly appearing row, return entire value
-                entry.rxBytes = rxBytes[i];
-                entry.rxPackets = rxPackets[i];
-                entry.txBytes = txBytes[i];
-                entry.txPackets = txPackets[i];
-                entry.operations = operations[i];
+                entry.rxBytes = left.rxBytes[i];
+                entry.rxPackets = left.rxPackets[i];
+                entry.txBytes = left.txBytes[i];
+                entry.txPackets = left.txPackets[i];
+                entry.operations = left.operations[i];
             } else {
                 // existing row, subtract remote value
-                entry.rxBytes = rxBytes[i] - value.rxBytes[j];
-                entry.rxPackets = rxPackets[i] - value.rxPackets[j];
-                entry.txBytes = txBytes[i] - value.txBytes[j];
-                entry.txPackets = txPackets[i] - value.txPackets[j];
-                entry.operations = operations[i] - value.operations[j];
+                entry.rxBytes = left.rxBytes[i] - right.rxBytes[j];
+                entry.rxPackets = left.rxPackets[i] - right.rxPackets[j];
+                entry.txBytes = left.txBytes[i] - right.txBytes[j];
+                entry.txPackets = left.txPackets[i] - right.txPackets[j];
+                entry.operations = left.operations[i] - right.operations[j];
 
                 if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0
                         || entry.txPackets < 0 || entry.operations < 0) {
-                    if (clampNonMonotonic) {
-                        entry.rxBytes = Math.max(entry.rxBytes, 0);
-                        entry.rxPackets = Math.max(entry.rxPackets, 0);
-                        entry.txBytes = Math.max(entry.txBytes, 0);
-                        entry.txPackets = Math.max(entry.txPackets, 0);
-                        entry.operations = Math.max(entry.operations, 0);
-                    } else {
-                        throw new NonMonotonicException(this, i, value, j);
+                    if (observer != null) {
+                        observer.foundNonMonotonic(left, i, right, j);
                     }
+                    entry.rxBytes = Math.max(entry.rxBytes, 0);
+                    entry.rxPackets = Math.max(entry.rxPackets, 0);
+                    entry.txBytes = Math.max(entry.txBytes, 0);
+                    entry.txPackets = Math.max(entry.txPackets, 0);
+                    entry.operations = Math.max(entry.operations, 0);
                 }
             }
 
@@ -665,22 +663,8 @@
         }
     };
 
-    public static class NonMonotonicException extends Exception {
-        public final NetworkStats left;
-        public final NetworkStats right;
-        public final int leftIndex;
-        public final int rightIndex;
-
-        public NonMonotonicException(NetworkStats left, NetworkStats right) {
-            this(left, -1, right, -1);
-        }
-
-        public NonMonotonicException(
-                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) {
-            this.left = checkNotNull(left, "missing left");
-            this.right = checkNotNull(right, "missing right");
-            this.leftIndex = leftIndex;
-            this.rightIndex = rightIndex;
-        }
+    public interface NonMonotonicObserver {
+        public void foundNonMonotonic(
+                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex);
     }
 }
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index cd585b2..8bdb669 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -20,7 +20,6 @@
 import android.app.backup.BackupManager;
 import android.content.Context;
 import android.media.MediaPlayer;
-import android.net.NetworkStats.NonMonotonicException;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 
@@ -193,15 +192,12 @@
                 throw new IllegalStateException("not profiling data");
             }
 
-            try {
-                // subtract starting values and return delta
-                final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
-                final NetworkStats profilingDelta = profilingStop.subtract(sActiveProfilingStart);
-                sActiveProfilingStart = null;
-                return profilingDelta;
-            } catch (NonMonotonicException e) {
-                throw new RuntimeException(e);
-            }
+            // subtract starting values and return delta
+            final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
+            final NetworkStats profilingDelta = NetworkStats.subtract(
+                    profilingStop, sActiveProfilingStart, null);
+            sActiveProfilingStart = null;
+            return profilingDelta;
         }
     }
 
diff --git a/services/java/com/android/server/NativeDaemonConnector.java b/services/java/com/android/server/NativeDaemonConnector.java
index 28013bd..f475dd6 100644
--- a/services/java/com/android/server/NativeDaemonConnector.java
+++ b/services/java/com/android/server/NativeDaemonConnector.java
@@ -22,59 +22,51 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.os.SystemClock;
+import android.util.LocalLog;
 import android.util.Slog;
 
+import com.google.android.collect.Lists;
+
+import java.nio.charset.Charsets;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
 /**
- * Generic connector class for interfacing with a native
- * daemon which uses the libsysutils FrameworkListener
- * protocol.
+ * Generic connector class for interfacing with a native daemon which uses the
+ * {@code libsysutils} FrameworkListener protocol.
  */
 final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {
-    private static final boolean LOCAL_LOGD = false;
+    private static final boolean LOGD = false;
 
-    private BlockingQueue<String> mResponseQueue;
-    private OutputStream          mOutputStream;
-    private String                TAG = "NativeDaemonConnector";
-    private String                mSocket;
+    private final String TAG;
+
+    private String mSocket;
+    private OutputStream mOutputStream;
+    private LocalLog mLocalLog;
+
+    private final BlockingQueue<NativeDaemonEvent> mResponseQueue;
+
     private INativeDaemonConnectorCallbacks mCallbacks;
-    private Handler               mCallbackHandler;
+    private Handler mCallbackHandler;
 
     /** Lock held whenever communicating with native daemon. */
-    private Object mDaemonLock = new Object();
+    private final Object mDaemonLock = new Object();
 
     private final int BUFFER_SIZE = 4096;
 
-    class ResponseCode {
-        public static final int ActionInitiated                = 100;
-
-        public static final int CommandOkay                    = 200;
-
-        // The range of 400 -> 599 is reserved for cmd failures
-        public static final int OperationFailed                = 400;
-        public static final int CommandSyntaxError             = 500;
-        public static final int CommandParameterError          = 501;
-
-        public static final int UnsolicitedInformational       = 600;
-
-        //
-        public static final int FailedRangeStart               = 400;
-        public static final int FailedRangeEnd                 = 599;
-    }
-
-    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks,
-                          String socket, int responseQueueSize, String logTag) {
+    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
+            int responseQueueSize, String logTag, int maxLogSize) {
         mCallbacks = callbacks;
-        if (logTag != null)
-            TAG = logTag;
         mSocket = socket;
-        mResponseQueue = new LinkedBlockingQueue<String>(responseQueueSize);
+        mResponseQueue = new LinkedBlockingQueue<NativeDaemonEvent>(responseQueueSize);
+        TAG = logTag != null ? logTag : "NativeDaemonConnector";
+        mLocalLog = new LocalLog(maxLogSize);
     }
 
     @Override
@@ -136,26 +128,28 @@
 
                 for (int i = 0; i < count; i++) {
                     if (buffer[i] == 0) {
-                        String event = new String(buffer, start, i - start);
-                        if (LOCAL_LOGD) Slog.d(TAG, String.format("RCV <- {%s}", event));
+                        final String rawEvent = new String(
+                                buffer, start, i - start, Charsets.UTF_8);
+                        log("RCV <- {" + rawEvent + "}");
 
-                        String[] tokens = event.split(" ", 2);
                         try {
-                            int code = Integer.parseInt(tokens[0]);
-
-                            if (code >= ResponseCode.UnsolicitedInformational) {
-                                mCallbackHandler.sendMessage(
-                                        mCallbackHandler.obtainMessage(code, event));
+                            final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
+                                    rawEvent);
+                            if (event.isClassUnsolicited()) {
+                                // TODO: migrate to sending NativeDaemonEvent instances
+                                mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
+                                        event.getCode(), event.getRawEvent()));
                             } else {
                                 try {
                                     mResponseQueue.put(event);
                                 } catch (InterruptedException ex) {
-                                    Slog.e(TAG, "Failed to put response onto queue", ex);
+                                    Slog.e(TAG, "Failed to put response onto queue: " + ex);
                                 }
                             }
-                        } catch (NumberFormatException nfe) {
-                            Slog.w(TAG, String.format("Bad msg (%s)", event));
+                        } catch (IllegalArgumentException e) {
+                            Slog.w(TAG, "Problem parsing message: " + rawEvent, e);
                         }
+
                         start = i + 1;
                     }
                 }
@@ -195,137 +189,261 @@
         }
     }
 
-    private void sendCommandLocked(String command) throws NativeDaemonConnectorException {
-        sendCommandLocked(command, null);
-    }
-
     /**
-     * Sends a command to the daemon with a single argument
+     * Send command to daemon, escaping arguments as needed.
      *
-     * @param command  The command to send to the daemon
-     * @param argument The argument to send with the command (or null)
+     * @return the final command issued.
      */
-    private void sendCommandLocked(String command, String argument)
+    private String sendCommandLocked(String cmd, Object... args)
             throws NativeDaemonConnectorException {
-        if (command != null && command.indexOf('\0') >= 0) {
-            throw new IllegalArgumentException("unexpected command: " + command);
-        }
-        if (argument != null && argument.indexOf('\0') >= 0) {
-            throw new IllegalArgumentException("unexpected argument: " + argument);
+        // TODO: eventually enforce that cmd doesn't contain arguments
+        if (cmd.indexOf('\0') >= 0) {
+            throw new IllegalArgumentException("unexpected command: " + cmd);
         }
 
-        if (LOCAL_LOGD) Slog.d(TAG, String.format("SND -> {%s} {%s}", command, argument));
+        final StringBuilder builder = new StringBuilder(cmd);
+        for (Object arg : args) {
+            final String argString = String.valueOf(arg);
+            if (argString.indexOf('\0') >= 0) {
+                throw new IllegalArgumentException("unexpected argument: " + arg);
+            }
+
+            builder.append(' ');
+            appendEscaped(builder, argString);
+        }
+
+        final String unterminated = builder.toString();
+        log("SND -> {" + unterminated + "}");
+
+        builder.append('\0');
+
         if (mOutputStream == null) {
-            Slog.e(TAG, "No connection to daemon", new IllegalStateException());
-            throw new NativeDaemonConnectorException("No output stream!");
+            throw new NativeDaemonConnectorException("missing output stream");
         } else {
-            StringBuilder builder = new StringBuilder(command);
-            if (argument != null) {
-                builder.append(argument);
-            }
-            builder.append('\0');
-
             try {
-                mOutputStream.write(builder.toString().getBytes());
-            } catch (IOException ex) {
-                Slog.e(TAG, "IOException in sendCommand", ex);
+                mOutputStream.write(builder.toString().getBytes(Charsets.UTF_8));
+            } catch (IOException e) {
+                throw new NativeDaemonConnectorException("problem sending command", e);
             }
         }
+
+        return unterminated;
     }
 
     /**
-     * Issue a command to the native daemon and return the responses
+     * Issue the given command to the native daemon and return a single expected
+     * response.
+     *
+     * @throws NativeDaemonConnectorException when problem communicating with
+     *             native daemon, or if the response matches
+     *             {@link NativeDaemonEvent#isClassClientError()} or
+     *             {@link NativeDaemonEvent#isClassServerError()}.
      */
-    public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException {
-        synchronized (mDaemonLock) {
-            return doCommandLocked(cmd);
-        }
-    }
-
-    private ArrayList<String> doCommandLocked(String cmd) throws NativeDaemonConnectorException {
-        mResponseQueue.clear();
-        sendCommandLocked(cmd);
-
-        ArrayList<String> response = new ArrayList<String>();
-        boolean complete = false;
-        int code = -1;
-
-        while (!complete) {
-            try {
-                // TODO - this should not block forever
-                String line = mResponseQueue.take();
-                if (LOCAL_LOGD) Slog.d(TAG, String.format("RSP <- {%s}", line));
-                String[] tokens = line.split(" ");
-                try {
-                    code = Integer.parseInt(tokens[0]);
-                } catch (NumberFormatException nfe) {
-                    throw new NativeDaemonConnectorException(
-                            String.format("Invalid response from daemon (%s)", line));
-                }
-
-                if ((code >= 200) && (code < 600)) {
-                    complete = true;
-                }
-                response.add(line);
-            } catch (InterruptedException ex) {
-                Slog.e(TAG, "Failed to process response", ex);
-            }
-        }
-
-        if (code >= ResponseCode.FailedRangeStart &&
-                code <= ResponseCode.FailedRangeEnd) {
-            /*
-             * Note: The format of the last response in this case is
-             *        "NNN <errmsg>"
-             */
-            throw new NativeDaemonConnectorException(
-                    code, cmd, response.get(response.size()-1).substring(4));
-        }
-        return response;
+    public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
+        return execute(cmd.mCmd, cmd.mArguments.toArray());
     }
 
     /**
-     * Issues a list command and returns the cooked list
+     * Issue the given command to the native daemon and return a single expected
+     * response.
+     *
+     * @throws NativeDaemonConnectorException when problem communicating with
+     *             native daemon, or if the response matches
+     *             {@link NativeDaemonEvent#isClassClientError()} or
+     *             {@link NativeDaemonEvent#isClassServerError()}.
      */
-    public String[] doListCommand(String cmd, int expectedResponseCode)
+    public NativeDaemonEvent execute(String cmd, Object... args)
             throws NativeDaemonConnectorException {
+        final NativeDaemonEvent[] events = executeForList(cmd, args);
+        if (events.length != 1) {
+            throw new NativeDaemonConnectorException(
+                    "Expected exactly one response, but received " + events.length);
+        }
+        return events[0];
+    }
 
-        ArrayList<String> rsp = doCommand(cmd);
-        String[] rdata = new String[rsp.size()-1];
-        int idx = 0;
+    /**
+     * Issue the given command to the native daemon and return any
+     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
+     * final terminal response.
+     *
+     * @throws NativeDaemonConnectorException when problem communicating with
+     *             native daemon, or if the response matches
+     *             {@link NativeDaemonEvent#isClassClientError()} or
+     *             {@link NativeDaemonEvent#isClassServerError()}.
+     */
+    public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
+        return executeForList(cmd.mCmd, cmd.mArguments.toArray());
+    }
 
-        for (int i = 0; i < rsp.size(); i++) {
-            String line = rsp.get(i);
+    /**
+     * Issue the given command to the native daemon and return any
+     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
+     * final terminal response.
+     *
+     * @throws NativeDaemonConnectorException when problem communicating with
+     *             native daemon, or if the response matches
+     *             {@link NativeDaemonEvent#isClassClientError()} or
+     *             {@link NativeDaemonEvent#isClassServerError()}.
+     */
+    public NativeDaemonEvent[] executeForList(String cmd, Object... args)
+            throws NativeDaemonConnectorException {
+        synchronized (mDaemonLock) {
+            return executeLocked(cmd, args);
+        }
+    }
+
+    private NativeDaemonEvent[] executeLocked(String cmd, Object... args)
+            throws NativeDaemonConnectorException {
+        final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
+
+        mResponseQueue.clear();
+
+        final String sentCommand = sendCommandLocked(cmd, args);
+
+        NativeDaemonEvent event = null;
+        do {
             try {
-                String[] tok = line.split(" ");
-                int code = Integer.parseInt(tok[0]);
-                if (code == expectedResponseCode) {
-                    rdata[idx++] = line.substring(tok[0].length() + 1);
-                } else if (code == NativeDaemonConnector.ResponseCode.CommandOkay) {
-                    if (LOCAL_LOGD) Slog.d(TAG, String.format("List terminated with {%s}", line));
-                    int last = rsp.size() -1;
-                    if (i != last) {
-                        Slog.w(TAG, String.format("Recv'd %d lines after end of list {%s}", (last-i), cmd));
-                        for (int j = i; j <= last ; j++) {
-                            Slog.w(TAG, String.format("ExtraData <%s>", rsp.get(i)));
-                        }
-                    }
-                    return rdata;
-                } else {
-                    throw new NativeDaemonConnectorException(
-                            String.format("Expected list response %d, but got %d",
-                                    expectedResponseCode, code));
-                }
-            } catch (NumberFormatException nfe) {
+                event = mResponseQueue.take();
+            } catch (InterruptedException e) {
+                Slog.w(TAG, "interrupted waiting for event line");
+                continue;
+            }
+            events.add(event);
+        } while (event.isClassContinue());
+
+        if (event.isClassClientError()) {
+            throw new NativeDaemonArgumentException(sentCommand, event);
+        }
+        if (event.isClassServerError()) {
+            throw new NativeDaemonFailureException(sentCommand, event);
+        }
+
+        return events.toArray(new NativeDaemonEvent[events.size()]);
+    }
+
+    /**
+     * Issue a command to the native daemon and return the raw responses.
+     *
+     * @deprecated callers should move to {@link #execute(String, Object...)}
+     *             which returns parsed {@link NativeDaemonEvent}.
+     */
+    @Deprecated
+    public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException {
+        final ArrayList<String> rawEvents = Lists.newArrayList();
+        final NativeDaemonEvent[] events = executeForList(cmd);
+        for (NativeDaemonEvent event : events) {
+            rawEvents.add(event.getRawEvent());
+        }
+        return rawEvents;
+    }
+
+    /**
+     * Issues a list command and returns the cooked list of all
+     * {@link NativeDaemonEvent#getMessage()} which match requested code.
+     */
+    @Deprecated
+    public String[] doListCommand(String cmd, int expectedCode)
+            throws NativeDaemonConnectorException {
+        final ArrayList<String> list = Lists.newArrayList();
+
+        final NativeDaemonEvent[] events = executeForList(cmd);
+        for (int i = 0; i < events.length - 1; i++) {
+            final NativeDaemonEvent event = events[i];
+            final int code = event.getCode();
+            if (code == expectedCode) {
+                list.add(event.getMessage());
+            } else {
                 throw new NativeDaemonConnectorException(
-                        String.format("Error reading code '%s'", line));
+                        "unexpected list response " + code + " instead of " + expectedCode);
             }
         }
-        throw new NativeDaemonConnectorException("Got an empty response");
+
+        final NativeDaemonEvent finalEvent = events[events.length - 1];
+        if (!finalEvent.isClassOk()) {
+            throw new NativeDaemonConnectorException("unexpected final event: " + finalEvent);
+        }
+
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * Append the given argument to {@link StringBuilder}, escaping as needed,
+     * and surrounding with quotes when it contains spaces.
+     */
+    // @VisibleForTesting
+    static void appendEscaped(StringBuilder builder, String arg) {
+        final boolean hasSpaces = arg.indexOf(' ') >= 0;
+        if (hasSpaces) {
+            builder.append('"');
+        }
+
+        final int length = arg.length();
+        for (int i = 0; i < length; i++) {
+            final char c = arg.charAt(i);
+
+            if (c == '"') {
+                builder.append("\\\"");
+            } else if (c == '\\') {
+                builder.append("\\\\");
+            } else {
+                builder.append(c);
+            }
+        }
+
+        if (hasSpaces) {
+            builder.append('"');
+        }
+    }
+
+    private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
+        public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
+            super(command, event);
+        }
+
+        @Override
+        public IllegalArgumentException rethrowAsParcelableException() {
+            throw new IllegalArgumentException(getMessage(), this);
+        }
+    }
+
+    private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
+        public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
+            super(command, event);
+        }
+    }
+
+    /**
+     * Command builder that handles argument list building.
+     */
+    public static class Command {
+        private String mCmd;
+        private ArrayList<Object> mArguments = Lists.newArrayList();
+
+        public Command(String cmd, Object... args) {
+            mCmd = cmd;
+            for (Object arg : args) {
+                appendArg(arg);
+            }
+        }
+
+        public Command appendArg(Object arg) {
+            mArguments.add(arg);
+            return this;
+        }
     }
 
     /** {@inheritDoc} */
     public void monitor() {
         synchronized (mDaemonLock) { }
     }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mLocalLog.dump(fd, pw, args);
+    }
+
+    private void log(String logstring) {
+        if (LOGD) Slog.d(TAG, logstring);
+        mLocalLog.log(logstring);
+    }
 }
diff --git a/services/java/com/android/server/NativeDaemonConnectorException.java b/services/java/com/android/server/NativeDaemonConnectorException.java
index 426742b..590bbcc 100644
--- a/services/java/com/android/server/NativeDaemonConnectorException.java
+++ b/services/java/com/android/server/NativeDaemonConnectorException.java
@@ -16,33 +16,43 @@
 
 package com.android.server;
 
+import android.os.Parcel;
+
 /**
- * An exception that indicates there was an error with a NativeDaemonConnector operation
+ * An exception that indicates there was an error with a
+ * {@link NativeDaemonConnector} operation.
  */
-public class NativeDaemonConnectorException extends RuntimeException
-{
-    private int mCode = -1;
+public class NativeDaemonConnectorException extends Exception {
     private String mCmd;
+    private NativeDaemonEvent mEvent;
 
-    public NativeDaemonConnectorException() {}
-
-    public NativeDaemonConnectorException(String error)
-    {
-        super(error);
+    public NativeDaemonConnectorException(String detailMessage) {
+        super(detailMessage);
     }
 
-    public NativeDaemonConnectorException(int code, String cmd, String error)
-    {
-        super(String.format("Cmd {%s} failed with code %d : {%s}", cmd, code, error));
-        mCode = code;
+    public NativeDaemonConnectorException(String detailMessage, Throwable throwable) {
+        super(detailMessage, throwable);
+    }
+
+    public NativeDaemonConnectorException(String cmd, NativeDaemonEvent event) {
+        super("command '" + cmd + "' failed with '" + event + "'");
         mCmd = cmd;
+        mEvent = event;
     }
 
     public int getCode() {
-        return mCode;
+        return mEvent.getCode();
     }
 
     public String getCmd() {
         return mCmd;
     }
+
+    /**
+     * Rethrow as a {@link RuntimeException} subclass that is handled by
+     * {@link Parcel#writeException(Exception)}.
+     */
+    public IllegalArgumentException rethrowAsParcelableException() {
+        throw new IllegalStateException(getMessage(), this);
+    }
 }
diff --git a/services/java/com/android/server/NativeDaemonEvent.java b/services/java/com/android/server/NativeDaemonEvent.java
new file mode 100644
index 0000000..62084c0
--- /dev/null
+++ b/services/java/com/android/server/NativeDaemonEvent.java
@@ -0,0 +1,142 @@
+/*
+ * 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.server;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Parsed event from native side of {@link NativeDaemonConnector}.
+ */
+public class NativeDaemonEvent {
+
+    // TODO: keep class ranges in sync with ResponseCode.h
+    // TODO: swap client and server error ranges to roughly mirror HTTP spec
+
+    private final int mCode;
+    private final String mMessage;
+    private final String mRawEvent;
+
+    private NativeDaemonEvent(int code, String message, String rawEvent) {
+        mCode = code;
+        mMessage = message;
+        mRawEvent = rawEvent;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+
+    public String getMessage() {
+        return mMessage;
+    }
+
+    @Deprecated
+    public String getRawEvent() {
+        return mRawEvent;
+    }
+
+    @Override
+    public String toString() {
+        return mRawEvent;
+    }
+
+    /**
+     * Test if event represents a partial response which is continued in
+     * additional subsequent events.
+     */
+    public boolean isClassContinue() {
+        return mCode >= 100 && mCode < 200;
+    }
+
+    /**
+     * Test if event represents a command success.
+     */
+    public boolean isClassOk() {
+        return mCode >= 200 && mCode < 300;
+    }
+
+    /**
+     * Test if event represents a remote native daemon error.
+     */
+    public boolean isClassServerError() {
+        return mCode >= 400 && mCode < 500;
+    }
+
+    /**
+     * Test if event represents a command syntax or argument error.
+     */
+    public boolean isClassClientError() {
+        return mCode >= 500 && mCode < 600;
+    }
+
+    /**
+     * Test if event represents an unsolicited event from native daemon.
+     */
+    public boolean isClassUnsolicited() {
+        return mCode >= 600 && mCode < 700;
+    }
+
+    /**
+     * Verify this event matches the given code.
+     *
+     * @throws IllegalStateException if {@link #getCode()} doesn't match.
+     */
+    public void checkCode(int code) {
+        if (mCode != code) {
+            throw new IllegalStateException("Expected " + code + " but was: " + this);
+        }
+    }
+
+    /**
+     * Parse the given raw event into {@link NativeDaemonEvent} instance.
+     *
+     * @throws IllegalArgumentException when line doesn't match format expected
+     *             from native side.
+     */
+    public static NativeDaemonEvent parseRawEvent(String rawEvent) {
+        final int splitIndex = rawEvent.indexOf(' ');
+        if (splitIndex == -1) {
+            throw new IllegalArgumentException("unable to find ' ' separator");
+        }
+
+        final int code;
+        try {
+            code = Integer.parseInt(rawEvent.substring(0, splitIndex));
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("problem parsing code", e);
+        }
+
+        final String message = rawEvent.substring(splitIndex + 1);
+        return new NativeDaemonEvent(code, message, rawEvent);
+    }
+
+    /**
+     * Filter the given {@link NativeDaemonEvent} list, returning
+     * {@link #getMessage()} for any events matching the requested code.
+     */
+    public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
+        final ArrayList<String> result = Lists.newArrayList();
+        for (NativeDaemonEvent event : events) {
+            if (event.getCode() == matchCode) {
+                result.add(event.getMessage());
+            }
+        }
+        return result.toArray(new String[result.size()]);
+    }
+}
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index f660520..eeb7fec 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -71,7 +71,7 @@
 import android.net.NetworkInfo;
 import android.net.NetworkState;
 import android.net.NetworkStats;
-import android.net.NetworkStats.NonMonotonicException;
+import android.net.NetworkStats.NonMonotonicObserver;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.os.Binder;
@@ -268,6 +268,11 @@
     }
 
     public void systemReady() {
+        if (!isBandwidthControlEnabled()) {
+            Slog.w(TAG, "bandwidth controls disabled, unable to track stats");
+            return;
+        }
+
         synchronized (mStatsLock) {
             // read historical network stats from disk, since policy service
             // might need them right away. we delay loading detailed UID stats
@@ -543,7 +548,14 @@
 
         // TODO: switch to data layer stats once kernel exports
         // for now, read network layer stats and flatten across all ifaces
-        final NetworkStats networkLayer = mNetworkManager.getNetworkStatsUidDetail(uid);
+        final long token = Binder.clearCallingIdentity();
+        final NetworkStats networkLayer;
+        try {
+            networkLayer = mNetworkManager.getNetworkStatsUidDetail(uid);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
         final NetworkStats dataLayer = new NetworkStats(
                 networkLayer.getElapsedRealtime(), networkLayer.size());
 
@@ -1539,6 +1551,30 @@
         }
     }
 
+    private StatsObserver mStatsObserver = new StatsObserver();
+
+    private class StatsObserver implements NonMonotonicObserver {
+        private String mCurrentType;
+
+        public void setCurrentType(String type) {
+            mCurrentType = type;
+        }
+
+        /** {@inheritDoc} */
+        public void foundNonMonotonic(
+                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) {
+            Log.w(TAG, "found non-monotonic values; saving to dropbox");
+
+            // record error for debugging
+            final StringBuilder builder = new StringBuilder();
+            builder.append("found non-monotonic " + mCurrentType + " values at left[" + leftIndex
+                    + "] - right[" + rightIndex + "]\n");
+            builder.append("left=").append(left).append('\n');
+            builder.append("right=").append(right).append('\n');
+            mDropBox.addText(TAG_NETSTATS_ERROR, builder.toString());
+        }
+    }
+
     /**
      * Return the delta between two {@link NetworkStats} snapshots, where {@code
      * before} can be {@code null}.
@@ -1546,27 +1582,8 @@
     private NetworkStats computeStatsDelta(
             NetworkStats before, NetworkStats current, boolean collectStale, String type) {
         if (before != null) {
-            try {
-                return current.subtract(before, false);
-            } catch (NonMonotonicException e) {
-                Log.w(TAG, "found non-monotonic values; saving to dropbox");
-
-                // record error for debugging
-                final StringBuilder builder = new StringBuilder();
-                builder.append("found non-monotonic " + type + " values at left[" + e.leftIndex
-                        + "] - right[" + e.rightIndex + "]\n");
-                builder.append("left=").append(e.left).append('\n');
-                builder.append("right=").append(e.right).append('\n');
-                mDropBox.addText(TAG_NETSTATS_ERROR, builder.toString());
-
-                try {
-                    // return clamped delta to help recover
-                    return current.subtract(before, true);
-                } catch (NonMonotonicException e1) {
-                    Log.wtf(TAG, "found non-monotonic values; returning empty delta", e1);
-                    return new NetworkStats(0L, 10);
-                }
-            }
+            mStatsObserver.setCurrentType(type);
+            return NetworkStats.subtract(current, before, mStatsObserver);
         } else if (collectStale) {
             // caller is okay collecting stale stats for first call.
             return current;
@@ -1646,6 +1663,15 @@
         return telephony.getSubscriberId();
     }
 
+    private boolean isBandwidthControlEnabled() {
+        try {
+            return mNetworkManager.isBandwidthControlEnabled();
+        } catch (RemoteException e) {
+            // ignored; service lives in system_server
+            return false;
+        }
+    }
+
     /**
      * Key uniquely identifying a {@link NetworkStatsHistory} for a UID.
      */
diff --git a/services/tests/servicestests/src/com/android/server/NativeDaemonConnectorTest.java b/services/tests/servicestests/src/com/android/server/NativeDaemonConnectorTest.java
new file mode 100644
index 0000000..275d807
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NativeDaemonConnectorTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.server;
+
+import static com.android.server.NativeDaemonConnector.appendEscaped;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+/**
+ * Tests for {@link NativeDaemonConnector}.
+ */
+@MediumTest
+public class NativeDaemonConnectorTest extends AndroidTestCase {
+    private static final String TAG = "NativeDaemonConnectorTest";
+
+    public void testArgumentNormal() throws Exception {
+        final StringBuilder builder = new StringBuilder();
+
+        builder.setLength(0);
+        appendEscaped(builder, "");
+        assertEquals("", builder.toString());
+
+        builder.setLength(0);
+        appendEscaped(builder, "foo");
+        assertEquals("foo", builder.toString());
+
+        builder.setLength(0);
+        appendEscaped(builder, "foo\"bar");
+        assertEquals("foo\\\"bar", builder.toString());
+
+        builder.setLength(0);
+        appendEscaped(builder, "foo\\bar\\\"baz");
+        assertEquals("foo\\\\bar\\\\\\\"baz", builder.toString());
+    }
+
+    public void testArgumentWithSpaces() throws Exception {
+        final StringBuilder builder = new StringBuilder();
+
+        builder.setLength(0);
+        appendEscaped(builder, "foo bar");
+        assertEquals("\"foo bar\"", builder.toString());
+
+        builder.setLength(0);
+        appendEscaped(builder, "foo\"bar\\baz foo");
+        assertEquals("\"foo\\\"bar\\\\baz foo\"", builder.toString());
+    }
+
+    public void testArgumentWithUtf() throws Exception {
+        final StringBuilder builder = new StringBuilder();
+
+        builder.setLength(0);
+        appendEscaped(builder, "caf\u00E9 c\u00F6ffee");
+        assertEquals("\"caf\u00E9 c\u00F6ffee\"", builder.toString());
+    }
+}