Merge "Annotate @SystemApi with required permissions." into oc-dev
am: c1406978a4

Change-Id: I305967cad945a807c3f8234efabaad0ef8b591d1
diff --git a/core/java/android/net/IIpSecService.aidl b/core/java/android/net/IIpSecService.aidl
index 0aa3ce6..0b1ea98 100644
--- a/core/java/android/net/IIpSecService.aidl
+++ b/core/java/android/net/IIpSecService.aidl
@@ -18,6 +18,9 @@
 
 import android.net.Network;
 import android.net.IpSecConfig;
+import android.net.IpSecUdpEncapResponse;
+import android.net.IpSecSpiResponse;
+import android.net.IpSecTransformResponse;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
@@ -27,16 +30,16 @@
  */
 interface IIpSecService
 {
-    Bundle reserveSecurityParameterIndex(
+    IpSecSpiResponse reserveSecurityParameterIndex(
             int direction, in String remoteAddress, int requestedSpi, in IBinder binder);
 
     void releaseSecurityParameterIndex(int resourceId);
 
-    Bundle openUdpEncapsulationSocket(int port, in IBinder binder);
+    IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, in IBinder binder);
 
-    void closeUdpEncapsulationSocket(in ParcelFileDescriptor socket);
+    void closeUdpEncapsulationSocket(int resourceId);
 
-    Bundle createTransportModeTransform(in IpSecConfig c, in IBinder binder);
+    IpSecTransformResponse createTransportModeTransform(in IpSecConfig c, in IBinder binder);
 
     void deleteTransportModeTransform(int transformId);
 
diff --git a/core/java/android/net/IpSecConfig.java b/core/java/android/net/IpSecConfig.java
index 13dc19f..8b80f2b 100644
--- a/core/java/android/net/IpSecConfig.java
+++ b/core/java/android/net/IpSecConfig.java
@@ -40,7 +40,7 @@
         // Minimum requirements for identifying a transform
         // SPI identifying the IPsec flow in packet processing
         // and a remote IP address
-        int spi;
+        int spiResourceId;
 
         // Encryption Algorithm
         IpSecAlgorithm encryption;
@@ -54,7 +54,7 @@
     // For tunnel mode IPv4 UDP Encapsulation
     // IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE
     int encapType;
-    int encapLocalPort;
+    int encapLocalPortResourceId;
     int encapRemotePort;
 
     // An interval, in seconds between the NattKeepalive packets
@@ -69,8 +69,8 @@
         return localAddress;
     }
 
-    public int getSpi(int direction) {
-        return flow[direction].spi;
+    public int getSpiResourceId(int direction) {
+        return flow[direction].spiResourceId;
     }
 
     public InetAddress getRemoteAddress() {
@@ -93,8 +93,8 @@
         return encapType;
     }
 
-    public int getEncapLocalPort() {
-        return encapLocalPort;
+    public int getEncapLocalResourceId() {
+        return encapLocalPortResourceId;
     }
 
     public int getEncapRemotePort() {
@@ -119,14 +119,14 @@
         // TODO: Use a byte array or other better method for storing IPs that can also include scope
         out.writeString((remoteAddress != null) ? remoteAddress.getHostAddress() : null);
         out.writeParcelable(network, flags);
-        out.writeInt(flow[IpSecTransform.DIRECTION_IN].spi);
+        out.writeInt(flow[IpSecTransform.DIRECTION_IN].spiResourceId);
         out.writeParcelable(flow[IpSecTransform.DIRECTION_IN].encryption, flags);
         out.writeParcelable(flow[IpSecTransform.DIRECTION_IN].authentication, flags);
-        out.writeInt(flow[IpSecTransform.DIRECTION_OUT].spi);
+        out.writeInt(flow[IpSecTransform.DIRECTION_OUT].spiResourceId);
         out.writeParcelable(flow[IpSecTransform.DIRECTION_OUT].encryption, flags);
         out.writeParcelable(flow[IpSecTransform.DIRECTION_OUT].authentication, flags);
         out.writeInt(encapType);
-        out.writeInt(encapLocalPort);
+        out.writeInt(encapLocalPortResourceId);
         out.writeInt(encapRemotePort);
     }
 
@@ -151,18 +151,18 @@
         localAddress = readInetAddressFromParcel(in);
         remoteAddress = readInetAddressFromParcel(in);
         network = (Network) in.readParcelable(Network.class.getClassLoader());
-        flow[IpSecTransform.DIRECTION_IN].spi = in.readInt();
+        flow[IpSecTransform.DIRECTION_IN].spiResourceId = in.readInt();
         flow[IpSecTransform.DIRECTION_IN].encryption =
                 (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
         flow[IpSecTransform.DIRECTION_IN].authentication =
                 (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
-        flow[IpSecTransform.DIRECTION_OUT].spi = in.readInt();
+        flow[IpSecTransform.DIRECTION_OUT].spiResourceId = in.readInt();
         flow[IpSecTransform.DIRECTION_OUT].encryption =
                 (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
         flow[IpSecTransform.DIRECTION_OUT].authentication =
                 (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
         encapType = in.readInt();
-        encapLocalPort = in.readInt();
+        encapLocalPortResourceId = in.readInt();
         encapRemotePort = in.readInt();
     }
 
diff --git a/core/java/android/net/IpSecManager.java b/core/java/android/net/IpSecManager.java
index e3cda5e..2f791e1 100644
--- a/core/java/android/net/IpSecManager.java
+++ b/core/java/android/net/IpSecManager.java
@@ -21,10 +21,10 @@
 import android.annotation.SystemService;
 import android.content.Context;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.util.AndroidException;
+import android.util.Log;
 import dalvik.system.CloseGuard;
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -57,12 +57,6 @@
     }
 
     /** @hide */
-    public static final String KEY_STATUS = "status";
-    /** @hide */
-    public static final String KEY_RESOURCE_ID = "resourceId";
-    /** @hide */
-    public static final String KEY_SPI = "spi";
-    /** @hide */
     public static final int INVALID_RESOURCE_ID = 0;
 
     /**
@@ -127,7 +121,11 @@
          */
         @Override
         public void close() {
-            mSpi = INVALID_SECURITY_PARAMETER_INDEX;
+            try {
+                mService.releaseSecurityParameterIndex(mResourceId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
             mCloseGuard.close();
         }
 
@@ -146,7 +144,7 @@
             mService = service;
             mRemoteAddress = remoteAddress;
             try {
-                Bundle result =
+                IpSecSpiResponse result =
                         mService.reserveSecurityParameterIndex(
                                 direction, remoteAddress.getHostAddress(), spi, new Binder());
 
@@ -154,7 +152,7 @@
                     throw new NullPointerException("Received null response from IpSecService");
                 }
 
-                int status = result.getInt(KEY_STATUS);
+                int status = result.status;
                 switch (status) {
                     case Status.OK:
                         break;
@@ -167,8 +165,8 @@
                         throw new RuntimeException(
                                 "Unknown status returned by IpSecService: " + status);
                 }
-                mSpi = result.getInt(KEY_SPI);
-                mResourceId = result.getInt(KEY_RESOURCE_ID);
+                mSpi = result.spi;
+                mResourceId = result.resourceId;
 
                 if (mSpi == INVALID_SECURITY_PARAMETER_INDEX) {
                     throw new RuntimeException("Invalid SPI returned by IpSecService: " + status);
@@ -184,6 +182,11 @@
             }
             mCloseGuard.open("open");
         }
+
+        /** @hide */
+        int getResourceId() {
+            return mResourceId;
+        }
     }
 
     /**
@@ -200,8 +203,7 @@
      * @throws SpiUnavailableException indicating that a particular SPI cannot be reserved
      */
     public SecurityParameterIndex reserveSecurityParameterIndex(
-            int direction, InetAddress remoteAddress)
-            throws ResourceUnavailableException {
+            int direction, InetAddress remoteAddress) throws ResourceUnavailableException {
         try {
             return new SecurityParameterIndex(
                     mService,
@@ -250,7 +252,9 @@
      */
     public void applyTransportModeTransform(Socket socket, IpSecTransform transform)
             throws IOException {
-        applyTransportModeTransform(ParcelFileDescriptor.fromSocket(socket), transform);
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
+            applyTransportModeTransform(pfd, transform);
+        }
     }
 
     /**
@@ -268,15 +272,8 @@
      */
     public void applyTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
             throws IOException {
-        applyTransportModeTransform(ParcelFileDescriptor.fromDatagramSocket(socket), transform);
-    }
-
-    /* Call down to activate a transform */
-    private void applyTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
-        try {
-            mService.applyTransportModeTransform(pfd, transform.getResourceId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
+            applyTransportModeTransform(pfd, transform);
         }
     }
 
@@ -294,7 +291,22 @@
      */
     public void applyTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
             throws IOException {
-        applyTransportModeTransform(new ParcelFileDescriptor(socket), transform);
+        // We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor()
+        // constructor takes control and closes the user's FD when we exit the method
+        // This is behaviorally the same as the other versions, but the PFD constructor does not
+        // dup() automatically, whereas PFD.fromSocket() and PDF.fromDatagramSocket() do dup().
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            applyTransportModeTransform(pfd, transform);
+        }
+    }
+
+    /* Call down to activate a transform */
+    private void applyTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
+        try {
+            mService.applyTransportModeTransform(pfd, transform.getResourceId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -323,7 +335,9 @@
      */
     public void removeTransportModeTransform(Socket socket, IpSecTransform transform)
             throws IOException {
-        removeTransportModeTransform(ParcelFileDescriptor.fromSocket(socket), transform);
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
+            removeTransportModeTransform(pfd, transform);
+        }
     }
 
     /**
@@ -339,7 +353,9 @@
      */
     public void removeTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
             throws IOException {
-        removeTransportModeTransform(ParcelFileDescriptor.fromDatagramSocket(socket), transform);
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
+            removeTransportModeTransform(pfd, transform);
+        }
     }
 
     /**
@@ -354,7 +370,9 @@
      */
     public void removeTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
             throws IOException {
-        removeTransportModeTransform(new ParcelFileDescriptor(socket), transform);
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            removeTransportModeTransform(pfd, transform);
+        }
     }
 
     /* Call down to activate a transform */
@@ -387,33 +405,48 @@
      * FileDescriptor. Instead, disposing of this socket requires a call to close().
      */
     public static final class UdpEncapsulationSocket implements AutoCloseable {
-        private final FileDescriptor mFd;
+        private final ParcelFileDescriptor mPfd;
         private final IIpSecService mService;
+        private final int mResourceId;
+        private final int mPort;
         private final CloseGuard mCloseGuard = CloseGuard.get();
 
         private UdpEncapsulationSocket(@NonNull IIpSecService service, int port)
-                throws ResourceUnavailableException {
+                throws ResourceUnavailableException, IOException {
             mService = service;
+            try {
+                IpSecUdpEncapResponse result =
+                        mService.openUdpEncapsulationSocket(port, new Binder());
+                switch (result.status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more Sockets may be allocated by this requester.");
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + result.status);
+                }
+                mResourceId = result.resourceId;
+                mPort = result.port;
+                mPfd = result.fileDescriptor;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
             mCloseGuard.open("constructor");
-            // TODO: go down to the kernel and get a socket on the specified
-            mFd = new FileDescriptor();
-        }
-
-        private UdpEncapsulationSocket(IIpSecService service) throws ResourceUnavailableException {
-            mService = service;
-            mCloseGuard.open("constructor");
-            // TODO: go get a random socket on a random port
-            mFd = new FileDescriptor();
         }
 
         /** Access the inner UDP Encapsulation Socket */
         public FileDescriptor getSocket() {
-            return mFd;
+            if (mPfd == null) {
+                return null;
+            }
+            return mPfd.getFileDescriptor();
         }
 
         /** Retrieve the port number of the inner encapsulation socket */
         public int getPort() {
-            return 0; // TODO get the port number from the Socket;
+            return mPort;
         }
 
         @Override
@@ -428,7 +461,18 @@
          * @param fd a file descriptor previously returned as a UDP Encapsulation socket.
          */
         public void close() throws IOException {
-            // TODO: Go close the socket
+            try {
+                mService.closeUdpEncapsulationSocket(mResourceId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+
+            try {
+                mPfd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to close UDP Encapsulation Socket with Port= " + mPort);
+                throw e;
+            }
             mCloseGuard.close();
         }
 
@@ -437,9 +481,13 @@
             if (mCloseGuard != null) {
                 mCloseGuard.warnIfOpen();
             }
-
             close();
         }
+
+        /** @hide */
+        int getResourceId() {
+            return mResourceId;
+        }
     };
 
     /**
@@ -466,7 +514,13 @@
     // socket.
     public UdpEncapsulationSocket openUdpEncapsulationSocket(int port)
             throws IOException, ResourceUnavailableException {
-        // Temporary code
+        /*
+         * Most range checking is done in the service, but this version of the constructor expects
+         * a valid port number, and zero cannot be checked after being passed to the service.
+         */
+        if (port == 0) {
+            throw new IllegalArgumentException("Specified port must be a valid port number!");
+        }
         return new UdpEncapsulationSocket(mService, port);
     }
 
@@ -490,8 +544,7 @@
     // socket.
     public UdpEncapsulationSocket openUdpEncapsulationSocket()
             throws IOException, ResourceUnavailableException {
-        // Temporary code
-        return new UdpEncapsulationSocket(mService);
+        return new UdpEncapsulationSocket(mService, 0);
     }
 
     /**
diff --git a/core/java/android/net/IpSecSpiResponse.aidl b/core/java/android/net/IpSecSpiResponse.aidl
new file mode 100644
index 0000000..6484a00
--- /dev/null
+++ b/core/java/android/net/IpSecSpiResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+/** @hide */
+parcelable IpSecSpiResponse;
diff --git a/core/java/android/net/IpSecSpiResponse.java b/core/java/android/net/IpSecSpiResponse.java
new file mode 100644
index 0000000..71dfa03
--- /dev/null
+++ b/core/java/android/net/IpSecSpiResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an SPI and corresponding status from the IpSecService to an
+ * IpSecManager.SecurityParameterIndex.
+ *
+ * @hide
+ */
+public final class IpSecSpiResponse implements Parcelable {
+    private static final String TAG = "IpSecSpiResponse";
+
+    public final int resourceId;
+    public final int status;
+    public final int spi;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(spi);
+    }
+
+    public IpSecSpiResponse(int inStatus, int inResourceId, int inSpi) {
+        status = inStatus;
+        resourceId = inResourceId;
+        spi = inSpi;
+    }
+
+    public IpSecSpiResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+    }
+
+    private IpSecSpiResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        spi = in.readInt();
+    }
+
+    public static final Parcelable.Creator<IpSecSpiResponse> CREATOR =
+            new Parcelable.Creator<IpSecSpiResponse>() {
+                public IpSecSpiResponse createFromParcel(Parcel in) {
+                    return new IpSecSpiResponse(in);
+                }
+
+                public IpSecSpiResponse[] newArray(int size) {
+                    return new IpSecSpiResponse[size];
+                }
+            };
+}
diff --git a/core/java/android/net/IpSecTransform.java b/core/java/android/net/IpSecTransform.java
index 639d1f2..e65f534 100644
--- a/core/java/android/net/IpSecTransform.java
+++ b/core/java/android/net/IpSecTransform.java
@@ -16,15 +16,12 @@
 package android.net;
 
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
-import static android.net.IpSecManager.KEY_RESOURCE_ID;
-import static android.net.IpSecManager.KEY_STATUS;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -78,23 +75,23 @@
     public static final int ENCAP_NONE = 0;
 
     /**
-     * IpSec traffic will be encapsulated within UDP as per <a
-     * href="https://tools.ietf.org/html/rfc3948">RFC3498</a>.
-     *
-     * @hide
-     */
-    public static final int ENCAP_ESPINUDP = 1;
-
-    /**
      * IpSec traffic will be encapsulated within a UDP header with an additional 8-byte header pad
      * (of '0'-value bytes) that prevents traffic from being interpreted as IKE or as ESP over UDP.
      *
      * @hide
      */
-    public static final int ENCAP_ESPINUDP_NONIKE = 2;
+    public static final int ENCAP_ESPINUDP_NON_IKE = 1;
+
+    /**
+     * IpSec traffic will be encapsulated within UDP as per <a
+     * href="https://tools.ietf.org/html/rfc3948">RFC3498</a>.
+     *
+     * @hide
+     */
+    public static final int ENCAP_ESPINUDP = 2;
 
     /** @hide */
-    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NONIKE})
+    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface EncapType {}
 
@@ -139,10 +136,11 @@
         synchronized (this) {
             try {
                 IIpSecService svc = getIpSecService();
-                Bundle result = svc.createTransportModeTransform(mConfig, new Binder());
-                int status = result.getInt(KEY_STATUS);
+                IpSecTransformResponse result =
+                        svc.createTransportModeTransform(mConfig, new Binder());
+                int status = result.status;
                 checkResultStatusAndThrow(status);
-                mResourceId = result.getInt(KEY_RESOURCE_ID, INVALID_RESOURCE_ID);
+                mResourceId = result.resourceId;
 
                 /* Keepalive will silently fail if not needed by the config; but, if needed and
                  * it fails to start, we need to bail because a transform will not be reliable
@@ -263,7 +261,10 @@
                             mConfig.getNattKeepaliveInterval(),
                             mKeepaliveCallback,
                             mConfig.getLocalAddress(),
-                            mConfig.getEncapLocalPort(),
+                            0x1234, /* FIXME: get the real port number again,
+                                    which we need to retrieve from the provided
+                                    EncapsulationSocket, and which isn't currently
+                                    stashed in IpSecConfig */
                             mConfig.getRemoteAddress());
             try {
                 // FIXME: this is still a horrible way to fudge the synchronous callback
@@ -360,7 +361,7 @@
                 @TransformDirection int direction, IpSecManager.SecurityParameterIndex spi) {
             // TODO: convert to using the resource Id of the SPI. Then build() can validate
             // the owner in the IpSecService
-            mConfig.flow[direction].spi = spi.getSpi();
+            mConfig.flow[direction].spiResourceId = spi.getResourceId();
             return this;
         }
 
@@ -394,7 +395,7 @@
                 IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
             // TODO: check encap type is valid.
             mConfig.encapType = ENCAP_ESPINUDP;
-            mConfig.encapLocalPort = localSocket.getPort(); // TODO: plug in the encap socket
+            mConfig.encapLocalPortResourceId = localSocket.getResourceId();
             mConfig.encapRemotePort = remotePort;
             return this;
         }
diff --git a/core/java/android/net/IpSecTransformResponse.aidl b/core/java/android/net/IpSecTransformResponse.aidl
new file mode 100644
index 0000000..546230d
--- /dev/null
+++ b/core/java/android/net/IpSecTransformResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+/** @hide */
+parcelable IpSecTransformResponse;
diff --git a/core/java/android/net/IpSecTransformResponse.java b/core/java/android/net/IpSecTransformResponse.java
new file mode 100644
index 0000000..cfc1762
--- /dev/null
+++ b/core/java/android/net/IpSecTransformResponse.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTransform resource Id and and corresponding status from the
+ * IpSecService to an IpSecTransform object.
+ *
+ * @hide
+ */
+public final class IpSecTransformResponse implements Parcelable {
+    private static final String TAG = "IpSecTransformResponse";
+
+    public final int resourceId;
+    public final int status;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+    }
+
+    public IpSecTransformResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+    }
+
+    public IpSecTransformResponse(int inStatus, int inResourceId) {
+        status = inStatus;
+        resourceId = inResourceId;
+    }
+
+    private IpSecTransformResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+    }
+
+    public static final Parcelable.Creator<IpSecTransformResponse> CREATOR =
+            new Parcelable.Creator<IpSecTransformResponse>() {
+                public IpSecTransformResponse createFromParcel(Parcel in) {
+                    return new IpSecTransformResponse(in);
+                }
+
+                public IpSecTransformResponse[] newArray(int size) {
+                    return new IpSecTransformResponse[size];
+                }
+            };
+}
diff --git a/core/java/android/net/IpSecUdpEncapResponse.aidl b/core/java/android/net/IpSecUdpEncapResponse.aidl
new file mode 100644
index 0000000..5e451f3
--- /dev/null
+++ b/core/java/android/net/IpSecUdpEncapResponse.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+/** @hide */
+parcelable IpSecUdpEncapResponse;
diff --git a/core/java/android/net/IpSecUdpEncapResponse.java b/core/java/android/net/IpSecUdpEncapResponse.java
new file mode 100644
index 0000000..4679267
--- /dev/null
+++ b/core/java/android/net/IpSecUdpEncapResponse.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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 android.net;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * This class is used to return a UDP Socket and corresponding status from the IpSecService to an
+ * IpSecManager.UdpEncapsulationSocket.
+ *
+ * @hide
+ */
+public final class IpSecUdpEncapResponse implements Parcelable {
+    private static final String TAG = "IpSecUdpEncapResponse";
+
+    public final int resourceId;
+    public final int port;
+    public final int status;
+    // There is a weird asymmetry with FileDescriptor: you can write a FileDescriptor
+    // but you read a ParcelFileDescriptor. To circumvent this, when we receive a FD
+    // from the user, we immediately create a ParcelFileDescriptor DUP, which we invalidate
+    // on writeParcel() by setting the flag to do close-on-write.
+    // TODO: tests to ensure this doesn't leak
+    public final ParcelFileDescriptor fileDescriptor;
+
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return (fileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(port);
+        out.writeParcelable(fileDescriptor, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+    }
+
+    public IpSecUdpEncapResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        port = -1;
+        fileDescriptor = null; // yes I know it's redundant, but readability
+    }
+
+    public IpSecUdpEncapResponse(int inStatus, int inResourceId, int inPort, FileDescriptor inFd)
+            throws IOException {
+        if (inStatus == IpSecManager.Status.OK && inFd == null) {
+            throw new IllegalArgumentException("Valid status implies FD must be non-null");
+        }
+        status = inStatus;
+        resourceId = inResourceId;
+        port = inPort;
+        fileDescriptor = (status == IpSecManager.Status.OK) ? ParcelFileDescriptor.dup(inFd) : null;
+    }
+
+    private IpSecUdpEncapResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        port = in.readInt();
+        fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+    }
+
+    public static final Parcelable.Creator<IpSecUdpEncapResponse> CREATOR =
+            new Parcelable.Creator<IpSecUdpEncapResponse>() {
+                public IpSecUdpEncapResponse createFromParcel(Parcel in) {
+                    return new IpSecUdpEncapResponse(in);
+                }
+
+                public IpSecUdpEncapResponse[] newArray(int size) {
+                    return new IpSecUdpEncapResponse[size];
+                }
+            };
+}
diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java
index 2d480a1..1e41eea 100644
--- a/core/java/android/net/nsd/NsdManager.java
+++ b/core/java/android/net/nsd/NsdManager.java
@@ -16,6 +16,10 @@
 
 package android.net.nsd;
 
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkStringNotEmpty;
+
 import android.annotation.SdkConstant;
 import android.annotation.SystemService;
 import android.annotation.SdkConstant.SdkConstantType;
@@ -32,6 +36,7 @@
 
 import java.util.concurrent.CountDownLatch;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.Protocol;
 
@@ -239,12 +244,12 @@
         return name;
     }
 
+    private static int FIRST_LISTENER_KEY = 1;
+
     private final INsdManager mService;
     private final Context mContext;
 
-    private static final int INVALID_LISTENER_KEY = 0;
-    private static final int BUSY_LISTENER_KEY = -1;
-    private int mListenerKey = 1;
+    private int mListenerKey = FIRST_LISTENER_KEY;
     private final SparseArray mListenerMap = new SparseArray();
     private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
     private final Object mMapLock = new Object();
@@ -268,6 +273,14 @@
     }
 
     /**
+     * @hide
+     */
+    @VisibleForTesting
+    public void disconnect() {
+        mAsyncChannel.disconnect();
+    }
+
+    /**
      * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
      * {@link RegistrationListener#onUnregistrationFailed},
      * {@link DiscoveryListener#onStartDiscoveryFailed},
@@ -302,7 +315,6 @@
         public void onServiceFound(NsdServiceInfo serviceInfo);
 
         public void onServiceLost(NsdServiceInfo serviceInfo);
-
     }
 
     /** Interface for callback invocation for service registration */
@@ -325,15 +337,17 @@
         public void onServiceResolved(NsdServiceInfo serviceInfo);
     }
 
-    private class ServiceHandler extends Handler {
+    @VisibleForTesting
+    class ServiceHandler extends Handler {
         ServiceHandler(Looper looper) {
             super(looper);
         }
 
         @Override
         public void handleMessage(Message message) {
-            if (DBG) Log.d(TAG, "received " + nameOf(message.what));
-            switch (message.what) {
+            final int what = message.what;
+            final int key = message.arg2;
+            switch (what) {
                 case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
                     mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
                     return;
@@ -346,19 +360,26 @@
                 default:
                     break;
             }
-            Object listener = getListener(message.arg2);
+            final Object listener;
+            final NsdServiceInfo ns;
+            synchronized (mMapLock) {
+                listener = mListenerMap.get(key);
+                ns = mServiceMap.get(key);
+            }
             if (listener == null) {
                 Log.d(TAG, "Stale key " + message.arg2);
                 return;
             }
-            NsdServiceInfo ns = getNsdService(message.arg2);
-            switch (message.what) {
+            if (DBG) {
+                Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+            }
+            switch (what) {
                 case DISCOVER_SERVICES_STARTED:
                     String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
                     ((DiscoveryListener) listener).onDiscoveryStarted(s);
                     break;
                 case DISCOVER_SERVICES_FAILED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns),
                             message.arg1);
                     break;
@@ -369,16 +390,18 @@
                     ((DiscoveryListener) listener).onServiceLost((NsdServiceInfo) message.obj);
                     break;
                 case STOP_DISCOVERY_FAILED:
-                    removeListener(message.arg2);
+                    // TODO: failure to stop discovery should be internal and retried internally, as
+                    // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
+                    removeListener(key);
                     ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns),
                             message.arg1);
                     break;
                 case STOP_DISCOVERY_SUCCEEDED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns));
                     break;
                 case REGISTER_SERVICE_FAILED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1);
                     break;
                 case REGISTER_SERVICE_SUCCEEDED:
@@ -386,19 +409,21 @@
                             (NsdServiceInfo) message.obj);
                     break;
                 case UNREGISTER_SERVICE_FAILED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1);
                     break;
                 case UNREGISTER_SERVICE_SUCCEEDED:
+                    // TODO: do not unregister listener until service is unregistered, or provide
+                    // alternative way for unregistering ?
                     removeListener(message.arg2);
                     ((RegistrationListener) listener).onServiceUnregistered(ns);
                     break;
                 case RESOLVE_SERVICE_FAILED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((ResolveListener) listener).onResolveFailed(ns, message.arg1);
                     break;
                 case RESOLVE_SERVICE_SUCCEEDED:
-                    removeListener(message.arg2);
+                    removeListener(key);
                     ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj);
                     break;
                 default:
@@ -408,40 +433,27 @@
         }
     }
 
-    // if the listener is already in the map, reject it.  Otherwise, add it and
-    // return its key.
+    private int nextListenerKey() {
+        // Ensure mListenerKey >= FIRST_LISTENER_KEY;
+        mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1);
+        return mListenerKey;
+    }
+
+    // Assert that the listener is not in the map, then add it and returns its key
     private int putListener(Object listener, NsdServiceInfo s) {
-        if (listener == null) return INVALID_LISTENER_KEY;
-        int key;
+        checkListener(listener);
+        final int key;
         synchronized (mMapLock) {
             int valueIndex = mListenerMap.indexOfValue(listener);
-            if (valueIndex != -1) {
-                return BUSY_LISTENER_KEY;
-            }
-            do {
-                key = mListenerKey++;
-            } while (key == INVALID_LISTENER_KEY);
+            checkArgument(valueIndex == -1, "listener already in use");
+            key = nextListenerKey();
             mListenerMap.put(key, listener);
             mServiceMap.put(key, s);
         }
         return key;
     }
 
-    private Object getListener(int key) {
-        if (key == INVALID_LISTENER_KEY) return null;
-        synchronized (mMapLock) {
-            return mListenerMap.get(key);
-        }
-    }
-
-    private NsdServiceInfo getNsdService(int key) {
-        synchronized (mMapLock) {
-            return mServiceMap.get(key);
-        }
-    }
-
     private void removeListener(int key) {
-        if (key == INVALID_LISTENER_KEY) return;
         synchronized (mMapLock) {
             mListenerMap.remove(key);
             mServiceMap.remove(key);
@@ -449,16 +461,15 @@
     }
 
     private int getListenerKey(Object listener) {
+        checkListener(listener);
         synchronized (mMapLock) {
             int valueIndex = mListenerMap.indexOfValue(listener);
-            if (valueIndex != -1) {
-                return mListenerMap.keyAt(valueIndex);
-            }
+            checkArgument(valueIndex != -1, "listener not registered");
+            return mListenerMap.keyAt(valueIndex);
         }
-        return INVALID_LISTENER_KEY;
     }
 
-    private String getNsdServiceInfoType(NsdServiceInfo s) {
+    private static String getNsdServiceInfoType(NsdServiceInfo s) {
         if (s == null) return "?";
         return s.getServiceType();
     }
@@ -468,7 +479,9 @@
      */
     private void init() {
         final Messenger messenger = getMessenger();
-        if (messenger == null) throw new RuntimeException("Failed to initialize");
+        if (messenger == null) {
+            fatal("Failed to obtain service Messenger");
+        }
         HandlerThread t = new HandlerThread("NsdManager");
         t.start();
         mHandler = new ServiceHandler(t.getLooper());
@@ -476,10 +489,15 @@
         try {
             mConnected.await();
         } catch (InterruptedException e) {
-            Log.e(TAG, "interrupted wait at init");
+            fatal("Interrupted wait at init");
         }
     }
 
+    private static void fatal(String msg) {
+        Log.e(TAG, msg);
+        throw new RuntimeException(msg);
+    }
+
     /**
      * Register a service to be discovered by other services.
      *
@@ -499,23 +517,10 @@
      */
     public void registerService(NsdServiceInfo serviceInfo, int protocolType,
             RegistrationListener listener) {
-        if (TextUtils.isEmpty(serviceInfo.getServiceName()) ||
-                TextUtils.isEmpty(serviceInfo.getServiceType())) {
-            throw new IllegalArgumentException("Service name or type cannot be empty");
-        }
-        if (serviceInfo.getPort() <= 0) {
-            throw new IllegalArgumentException("Invalid port number");
-        }
-        if (listener == null) {
-            throw new IllegalArgumentException("listener cannot be null");
-        }
-        if (protocolType != PROTOCOL_DNS_SD) {
-            throw new IllegalArgumentException("Unsupported protocol");
-        }
+        checkArgument(serviceInfo.getPort() > 0, "Invalid port number");
+        checkServiceInfo(serviceInfo);
+        checkProtocol(protocolType);
         int key = putListener(listener, serviceInfo);
-        if (key == BUSY_LISTENER_KEY) {
-            throw new IllegalArgumentException("listener already in use");
-        }
         mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo);
     }
 
@@ -534,12 +539,6 @@
      */
     public void unregisterService(RegistrationListener listener) {
         int id = getListenerKey(listener);
-        if (id == INVALID_LISTENER_KEY) {
-            throw new IllegalArgumentException("listener not registered");
-        }
-        if (listener == null) {
-            throw new IllegalArgumentException("listener cannot be null");
-        }
         mAsyncChannel.sendMessage(UNREGISTER_SERVICE, 0, id);
     }
 
@@ -572,25 +571,13 @@
      * Cannot be null. Cannot be in use for an active service discovery.
      */
     public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("listener cannot be null");
-        }
-        if (TextUtils.isEmpty(serviceType)) {
-            throw new IllegalArgumentException("Service type cannot be empty");
-        }
-
-        if (protocolType != PROTOCOL_DNS_SD) {
-            throw new IllegalArgumentException("Unsupported protocol");
-        }
+        checkStringNotEmpty(serviceType, "Service type cannot be empty");
+        checkProtocol(protocolType);
 
         NsdServiceInfo s = new NsdServiceInfo();
         s.setServiceType(serviceType);
 
         int key = putListener(listener, s);
-        if (key == BUSY_LISTENER_KEY) {
-            throw new IllegalArgumentException("listener already in use");
-        }
-
         mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, key, s);
     }
 
@@ -612,12 +599,6 @@
      */
     public void stopServiceDiscovery(DiscoveryListener listener) {
         int id = getListenerKey(listener);
-        if (id == INVALID_LISTENER_KEY) {
-            throw new IllegalArgumentException("service discovery not active on listener");
-        }
-        if (listener == null) {
-            throw new IllegalArgumentException("listener cannot be null");
-        }
         mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, id);
     }
 
@@ -631,19 +612,8 @@
      * Cannot be in use for an active service resolution.
      */
     public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
-        if (TextUtils.isEmpty(serviceInfo.getServiceName()) ||
-                TextUtils.isEmpty(serviceInfo.getServiceType())) {
-            throw new IllegalArgumentException("Service name or type cannot be empty");
-        }
-        if (listener == null) {
-            throw new IllegalArgumentException("listener cannot be null");
-        }
-
+        checkServiceInfo(serviceInfo);
         int key = putListener(listener, serviceInfo);
-
-        if (key == BUSY_LISTENER_KEY) {
-            throw new IllegalArgumentException("listener already in use");
-        }
         mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo);
     }
 
@@ -657,10 +627,10 @@
     }
 
     /**
-     * Get a reference to NetworkService handler. This is used to establish
+     * Get a reference to NsdService handler. This is used to establish
      * an AsyncChannel communication with the service
      *
-     * @return Messenger pointing to the NetworkService handler
+     * @return Messenger pointing to the NsdService handler
      */
     private Messenger getMessenger() {
         try {
@@ -669,4 +639,18 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    private static void checkListener(Object listener) {
+        checkNotNull(listener, "listener cannot be null");
+    }
+
+    private static void checkProtocol(int protocolType) {
+        checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol");
+    }
+
+    private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+        checkNotNull(serviceInfo, "NsdServiceInfo cannot be null");
+        checkStringNotEmpty(serviceInfo.getServiceName(),"Service name cannot be empty");
+        checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty");
+    }
 }
diff --git a/core/java/android/net/nsd/NsdServiceInfo.java b/core/java/android/net/nsd/NsdServiceInfo.java
index 7b845be..bccaf60 100644
--- a/core/java/android/net/nsd/NsdServiceInfo.java
+++ b/core/java/android/net/nsd/NsdServiceInfo.java
@@ -30,7 +30,6 @@
 import java.util.Collections;
 import java.util.Map;
 
-
 /**
  * A class representing service information for network service discovery
  * {@see NsdManager}
@@ -43,7 +42,7 @@
 
     private String mServiceType;
 
-    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<String, byte[]>();
+    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
 
     private InetAddress mHost;
 
diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java
index 2fb3532..3d3e148 100644
--- a/core/java/com/android/internal/net/NetworkStatsFactory.java
+++ b/core/java/com/android/internal/net/NetworkStatsFactory.java
@@ -192,46 +192,43 @@
             NetworkStats lastStats) throws IOException {
         final NetworkStats stats =
               readNetworkStatsDetailInternal(limitUid, limitIfaces, limitTag, lastStats);
-        NetworkStats.Entry entry = null; // for recycling
-
+        final ArrayMap<String, String> stackedIfaces;
         synchronized (sStackedIfaces) {
-            // For 464xlat traffic, xt_qtaguid sees every IPv4 packet twice, once as a native IPv4
-            // packet on the stacked interface, and once as translated to an IPv6 packet on the
-            // base interface. For correct stats accounting on the base interface, every 464xlat
-            // packet needs to be subtracted from the root UID on the base interface both for tx
-            // and rx traffic (http://b/12249687, http:/b/33681750).
-            final int size = sStackedIfaces.size();
-            for (int i = 0; i < size; i++) {
-                final String stackedIface = sStackedIfaces.keyAt(i);
-                final String baseIface = sStackedIfaces.valueAt(i);
-                if (!stackedIface.startsWith(CLATD_INTERFACE_PREFIX)) {
-                    continue;
-                }
-
-                NetworkStats.Entry adjust =
-                    new NetworkStats.Entry(baseIface, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
-                for (int j = 0; j < stats.size(); j++) {
-                    entry = stats.getValues(j, entry);
-                    if (Objects.equals(entry.iface, stackedIface)) {
-                        adjust.rxBytes -= (entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA);
-                        adjust.txBytes -= (entry.txBytes + entry.txPackets * IPV4V6_HEADER_DELTA);
-                        adjust.rxPackets -= entry.rxPackets;
-                        adjust.txPackets -= entry.txPackets;
-                    }
-                }
-                stats.combineValues(adjust);
-            }
+            stackedIfaces = new ArrayMap<>(sStackedIfaces);
         }
+        // Total 464xlat traffic to subtract from uid 0 on all base interfaces.
+        final NetworkStats adjustments = new NetworkStats(0, stackedIfaces.size());
 
-        // For 464xlat traffic, xt_qtaguid only counts the bytes of the inner IPv4 packet sent on
-        // the stacked interface with prefix "v4-" and drops the IPv6 header size after unwrapping.
-        // To account correctly for on-the-wire traffic, add the 20 additional bytes difference
-        // for all packets (http://b/12249687, http:/b/33681750).
+        NetworkStats.Entry entry = null; // For recycling
+
+        // For 464xlat traffic, xt_qtaguid sees every IPv4 packet twice, once as a native IPv4
+        // packet on the stacked interface, and once as translated to an IPv6 packet on the
+        // base interface. For correct stats accounting on the base interface, every 464xlat
+        // packet needs to be subtracted from the root UID on the base interface both for tx
+        // and rx traffic (http://b/12249687, http:/b/33681750).
         for (int i = 0; i < stats.size(); i++) {
             entry = stats.getValues(i, entry);
             if (entry.iface == null || !entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) {
                 continue;
             }
+            final String baseIface = stackedIfaces.get(entry.iface);
+            if (baseIface == null) {
+                continue;
+            }
+
+            NetworkStats.Entry adjust =
+                    new NetworkStats.Entry(baseIface, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
+            // Subtract any 464lat traffic seen for the root UID on the current base interface.
+            adjust.rxBytes -= (entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA);
+            adjust.txBytes -= (entry.txBytes + entry.txPackets * IPV4V6_HEADER_DELTA);
+            adjust.rxPackets -= entry.rxPackets;
+            adjust.txPackets -= entry.txPackets;
+            adjustments.combineValues(adjust);
+
+            // For 464xlat traffic, xt_qtaguid only counts the bytes of the native IPv4 packet sent
+            // on the stacked interface with prefix "v4-" and drops the IPv6 header size after
+            // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes
+            // difference for all packets (http://b/12249687, http:/b/33681750).
             entry.rxBytes = entry.rxPackets * IPV4V6_HEADER_DELTA;
             entry.txBytes = entry.txPackets * IPV4V6_HEADER_DELTA;
             entry.rxPackets = 0;
@@ -239,6 +236,8 @@
             stats.combineValues(entry);
         }
 
+        stats.combineAllValues(adjustments);
+
         return stats;
     }
 
diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java
index a7ce95b..ec275cc 100644
--- a/services/core/java/com/android/server/IpSecService.java
+++ b/services/core/java/com/android/server/IpSecService.java
@@ -18,9 +18,10 @@
 
 import static android.Manifest.permission.DUMP;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
-import static android.net.IpSecManager.KEY_RESOURCE_ID;
-import static android.net.IpSecManager.KEY_SPI;
-import static android.net.IpSecManager.KEY_STATUS;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static com.android.internal.util.Preconditions.checkNotNull;
 
 import android.content.Context;
 import android.net.IIpSecService;
@@ -28,47 +29,92 @@
 import android.net.IpSecAlgorithm;
 import android.net.IpSecConfig;
 import android.net.IpSecManager;
+import android.net.IpSecSpiResponse;
 import android.net.IpSecTransform;
+import android.net.IpSecTransformResponse;
+import android.net.IpSecUdpEncapResponse;
 import android.net.util.NetdService;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 import com.android.internal.annotations.GuardedBy;
 import java.io.FileDescriptor;
+import java.io.IOException;
 import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.util.concurrent.atomic.AtomicInteger;
+import libcore.io.IoUtils;
 
 /** @hide */
 public class IpSecService extends IIpSecService.Stub {
     private static final String TAG = "IpSecService";
     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
     private static final String NETD_SERVICE_NAME = "netd";
     private static final int[] DIRECTIONS =
             new int[] {IpSecTransform.DIRECTION_OUT, IpSecTransform.DIRECTION_IN};
 
-    /** Binder context for this service */
+    private static final int NETD_FETCH_TIMEOUT = 5000; //ms
+    private static final int MAX_PORT_BIND_ATTEMPTS = 10;
+    private static final InetAddress INADDR_ANY;
+
+    static {
+        try {
+            INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0});
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static final int FREE_PORT_MIN = 1024; // ports 1-1023 are reserved
+    static final int PORT_MAX = 0xFFFF; // ports are an unsigned 16-bit integer
+
+    /* Binder context for this service */
     private final Context mContext;
 
-    private Object mLock = new Object();
+    /** Should be a never-repeating global ID for resources */
+    private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0);
 
-    private static final int NETD_FETCH_TIMEOUT = 5000; //ms
+    @GuardedBy("this")
+    private final ManagedResourceArray<SpiRecord> mSpiRecords = new ManagedResourceArray<>();
 
-    private AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0);
+    @GuardedBy("this")
+    private final ManagedResourceArray<TransformRecord> mTransformRecords =
+            new ManagedResourceArray<>();
 
+    @GuardedBy("this")
+    private final ManagedResourceArray<UdpSocketRecord> mUdpSocketRecords =
+            new ManagedResourceArray<>();
+
+    /**
+     * The ManagedResource class provides a facility to cleanly and reliably release system
+     * resources. It relies on two things: an IBinder that allows ManagedResource to automatically
+     * clean up in the event that the Binder dies and a user-provided resourceId that should
+     * uniquely identify the managed resource. To use this class, the user should implement the
+     * releaseResources() method that is responsible for releasing system resources when invoked.
+     */
     private abstract class ManagedResource implements IBinder.DeathRecipient {
         final int pid;
         final int uid;
         private IBinder mBinder;
+        protected int mResourceId;
 
-        ManagedResource(IBinder binder) {
+        private AtomicInteger mReferenceCount = new AtomicInteger(0);
+
+        ManagedResource(int resourceId, IBinder binder) {
             super();
             mBinder = binder;
+            mResourceId = resourceId;
             pid = Binder.getCallingPid();
             uid = Binder.getCallingUid();
 
@@ -79,21 +125,53 @@
             }
         }
 
+        public void addReference() {
+            mReferenceCount.incrementAndGet();
+        }
+
+        public void removeReference() {
+            if (mReferenceCount.decrementAndGet() < 0) {
+                Log.wtf(TAG, "Programming error: negative reference count");
+            }
+        }
+
+        public boolean isReferenced() {
+            return (mReferenceCount.get() > 0);
+        }
+
+        public void checkOwnerOrSystemAndThrow() {
+            if (uid != Binder.getCallingUid()
+                    && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) {
+                throw new SecurityException("Only the owner may access managed resources!");
+            }
+        }
+
         /**
          * When this record is no longer needed for managing system resources this function should
-         * unlink all references held by the record to allow efficient garbage collection.
+         * clean up all system resources and nullify the record. This function shall perform all
+         * necessary cleanup of the resources managed by this record.
+         *
+         * <p>NOTE: this function verifies ownership before allowing resources to be freed.
          */
-        public final void release() {
-            //Release all the underlying system resources first
-            releaseResources();
+        public final void release() throws RemoteException {
+            synchronized (IpSecService.this) {
+                if (isReferenced()) {
+                    throw new IllegalStateException(
+                            "Cannot release a resource that has active references!");
+                }
 
-            if (mBinder != null) {
-                mBinder.unlinkToDeath(this, 0);
+                if (mResourceId == INVALID_RESOURCE_ID) {
+                    return;
+                }
+
+                releaseResources();
+                if (mBinder != null) {
+                    mBinder.unlinkToDeath(this, 0);
+                }
+                mBinder = null;
+
+                mResourceId = INVALID_RESOURCE_ID;
             }
-            mBinder = null;
-
-            //remove this record so that it can be cleaned up
-            nullifyRecord();
         }
 
         /**
@@ -102,41 +180,85 @@
          * collection
          */
         public final void binderDied() {
-            release();
+            try {
+                release();
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to release resource: " + e);
+            }
         }
 
         /**
-         * Implement this method to release all object references contained in the subclass to allow
-         * efficient garbage collection of the record. This should remove any references to the
-         * record from all other locations that hold a reference as the record is no longer valid.
-         */
-        protected abstract void nullifyRecord();
-
-        /**
          * Implement this method to release all system resources that are being protected by this
          * record. Once the resources are released, the record should be invalidated and no longer
-         * used by calling releaseRecord()
+         * used by calling release(). This should NEVER be called directly.
+         *
+         * <p>Calls to this are always guarded by IpSecService#this
          */
-        protected abstract void releaseResources();
+        protected abstract void releaseResources() throws RemoteException;
     };
 
-    private final class TransformRecord extends ManagedResource {
-        private IpSecConfig mConfig;
-        private int mResourceId;
+    /**
+     * Minimal wrapper around SparseArray that performs ownership
+     * validation on element accesses.
+     */
+    private class ManagedResourceArray<T extends ManagedResource> {
+        SparseArray<T> mArray = new SparseArray<>();
 
-        TransformRecord(IpSecConfig config, int resourceId, IBinder binder) {
-            super(binder);
+        T get(int key) {
+            T val = mArray.get(key);
+            val.checkOwnerOrSystemAndThrow();
+            return val;
+        }
+
+        void put(int key, T obj) {
+            checkNotNull(obj, "Null resources cannot be added");
+            mArray.put(key, obj);
+        }
+
+        void remove(int key) {
+            mArray.remove(key);
+        }
+    }
+
+    private final class TransformRecord extends ManagedResource {
+        private final IpSecConfig mConfig;
+        private final SpiRecord[] mSpis;
+        private final UdpSocketRecord mSocket;
+
+        TransformRecord(
+                int resourceId,
+                IBinder binder,
+                IpSecConfig config,
+                SpiRecord[] spis,
+                UdpSocketRecord socket) {
+            super(resourceId, binder);
             mConfig = config;
-            mResourceId = resourceId;
+            mSpis = spis;
+            mSocket = socket;
+
+            for (int direction : DIRECTIONS) {
+                mSpis[direction].addReference();
+                mSpis[direction].setOwnedByTransform();
+            }
+
+            if (mSocket != null) {
+                mSocket.addReference();
+            }
         }
 
         public IpSecConfig getConfig() {
             return mConfig;
         }
 
+        public SpiRecord getSpiRecord(int direction) {
+            return mSpis[direction];
+        }
+
+        /** always guarded by IpSecService#this */
         @Override
         protected void releaseResources() {
             for (int direction : DIRECTIONS) {
+                int spi = mSpis[direction].getSpi();
                 try {
                     getNetdInstance()
                             .ipSecDeleteSecurityAssociation(
@@ -148,19 +270,21 @@
                                     (mConfig.getRemoteAddress() != null)
                                             ? mConfig.getRemoteAddress().getHostAddress()
                                             : "",
-                                    mConfig.getSpi(direction));
+                                    spi);
                 } catch (ServiceSpecificException e) {
                     // FIXME: get the error code and throw is at an IOException from Errno Exception
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to delete SA with ID: " + mResourceId);
                 }
             }
-        }
 
-        @Override
-        protected void nullifyRecord() {
-            mConfig = null;
-            mResourceId = INVALID_RESOURCE_ID;
+            for (int direction : DIRECTIONS) {
+                mSpis[direction].removeReference();
+            }
+
+            if (mSocket != null) {
+                mSocket.removeReference();
+            }
         }
     }
 
@@ -168,27 +292,37 @@
         private final int mDirection;
         private final String mLocalAddress;
         private final String mRemoteAddress;
-        private final IBinder mBinder;
         private int mSpi;
-        private int mResourceId;
+
+        private boolean mOwnedByTransform = false;
 
         SpiRecord(
                 int resourceId,
+                IBinder binder,
                 int direction,
                 String localAddress,
                 String remoteAddress,
-                int spi,
-                IBinder binder) {
-            super(binder);
-            mResourceId = resourceId;
+                int spi) {
+            super(resourceId, binder);
             mDirection = direction;
             mLocalAddress = localAddress;
             mRemoteAddress = remoteAddress;
             mSpi = spi;
-            mBinder = binder;
         }
 
+        /** always guarded by IpSecService#this */
+        @Override
         protected void releaseResources() {
+            if (mOwnedByTransform) {
+                Log.d(TAG, "Cannot release Spi " + mSpi + ": Currently locked by a Transform");
+                // Because SPIs are "handed off" to transform, objects, they should never be
+                // freed from the SpiRecord once used in a transform. (They refer to the same SA,
+                // thus ownership and responsibility for freeing these resources passes to the
+                // Transform object). Thus, we should let the user free them without penalty once
+                // they are applied in a Transform object.
+                return;
+            }
+
             try {
                 getNetdInstance()
                         .ipSecDeleteSecurityAssociation(
@@ -198,19 +332,50 @@
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to delete SPI reservation with ID: " + mResourceId);
             }
+
+            mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
         }
 
-        protected void nullifyRecord() {
-            mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
-            mResourceId = INVALID_RESOURCE_ID;
+        public int getSpi() {
+            return mSpi;
+        }
+
+        public void setOwnedByTransform() {
+            if (mOwnedByTransform) {
+                // Programming error
+                new IllegalStateException("Cannot own an SPI twice!");
+            }
+
+            mOwnedByTransform = true;
         }
     }
 
-    @GuardedBy("mSpiRecords")
-    private final SparseArray<SpiRecord> mSpiRecords = new SparseArray<>();
+    private final class UdpSocketRecord extends ManagedResource {
+        private FileDescriptor mSocket;
+        private final int mPort;
 
-    @GuardedBy("mTransformRecords")
-    private final SparseArray<TransformRecord> mTransformRecords = new SparseArray<>();
+        UdpSocketRecord(int resourceId, IBinder binder, FileDescriptor socket, int port) {
+            super(resourceId, binder);
+            mSocket = socket;
+            mPort = port;
+        }
+
+        /** always guarded by IpSecService#this */
+        @Override
+        protected void releaseResources() {
+            Log.d(TAG, "Closing port " + mPort);
+            IoUtils.closeQuietly(mSocket);
+            mSocket = null;
+        }
+
+        public int getPort() {
+            return mPort;
+        }
+
+        public FileDescriptor getSocket() {
+            return mSocket;
+        }
+    }
 
     /**
      * Constructs a new IpSecService instance
@@ -242,7 +407,7 @@
                         new Runnable() {
                             @Override
                             public void run() {
-                                synchronized (mLock) {
+                                synchronized (IpSecService.this) {
                                     NetdService.get(NETD_FETCH_TIMEOUT);
                                 }
                             }
@@ -258,30 +423,27 @@
         return netd;
     }
 
-    boolean isNetdAlive() {
-        synchronized (mLock) {
-            try {
-                final INetd netd = getNetdInstance();
-                if (netd == null) {
-                    return false;
-                }
-                return netd.isAlive();
-            } catch (RemoteException re) {
+    synchronized boolean isNetdAlive() {
+        try {
+            final INetd netd = getNetdInstance();
+            if (netd == null) {
                 return false;
             }
+            return netd.isAlive();
+        } catch (RemoteException re) {
+            return false;
         }
     }
 
     @Override
     /** Get a new SPI and maintain the reservation in the system server */
-    public Bundle reserveSecurityParameterIndex(
+    public synchronized IpSecSpiResponse reserveSecurityParameterIndex(
             int direction, String remoteAddress, int requestedSpi, IBinder binder)
             throws RemoteException {
         int resourceId = mNextResourceId.getAndIncrement();
 
         int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
         String localAddress = "";
-        Bundle retBundle = new Bundle(3);
         try {
             spi =
                     getNetdInstance()
@@ -292,29 +454,78 @@
                                     remoteAddress,
                                     requestedSpi);
             Log.d(TAG, "Allocated SPI " + spi);
-            retBundle.putInt(KEY_STATUS, IpSecManager.Status.OK);
-            retBundle.putInt(KEY_RESOURCE_ID, resourceId);
-            retBundle.putInt(KEY_SPI, spi);
-            synchronized (mSpiRecords) {
-                mSpiRecords.put(
-                        resourceId,
-                        new SpiRecord(
-                                resourceId, direction, localAddress, remoteAddress, spi, binder));
-            }
+            mSpiRecords.put(
+                    resourceId,
+                    new SpiRecord(resourceId, binder, direction, localAddress, remoteAddress, spi));
         } catch (ServiceSpecificException e) {
             // TODO: Add appropriate checks when other ServiceSpecificException types are supported
-            retBundle.putInt(KEY_STATUS, IpSecManager.Status.SPI_UNAVAILABLE);
-            retBundle.putInt(KEY_RESOURCE_ID, resourceId);
-            retBundle.putInt(KEY_SPI, spi);
+            return new IpSecSpiResponse(
+                    IpSecManager.Status.SPI_UNAVAILABLE, IpSecManager.INVALID_RESOURCE_ID, spi);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-        return retBundle;
+        return new IpSecSpiResponse(IpSecManager.Status.OK, resourceId, spi);
+    }
+
+    /* This method should only be called from Binder threads. Do not call this from
+     * within the system server as it will crash the system on failure.
+     */
+    private synchronized <T extends ManagedResource> void releaseManagedResource(
+            ManagedResourceArray<T> resArray, int resourceId, String typeName)
+            throws RemoteException {
+        // We want to non-destructively get so that we can check credentials before removing
+        // this from the records.
+        T record = resArray.get(resourceId);
+
+        if (record == null) {
+            throw new IllegalArgumentException(
+                    typeName + " " + resourceId + " is not available to be deleted");
+        }
+
+        record.release();
+        resArray.remove(resourceId);
     }
 
     /** Release a previously allocated SPI that has been registered with the system server */
     @Override
-    public void releaseSecurityParameterIndex(int resourceId) throws RemoteException {}
+    public void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
+        releaseManagedResource(mSpiRecords, resourceId, "SecurityParameterIndex");
+    }
+
+    /**
+     * This function finds and forcibly binds to a random system port, ensuring that the port cannot
+     * be unbound.
+     *
+     * <p>A socket cannot be un-bound from a port if it was bound to that port by number. To select
+     * a random open port and then bind by number, this function creates a temp socket, binds to a
+     * random port (specifying 0), gets that port number, and then uses is to bind the user's UDP
+     * Encapsulation Socket forcibly, so that it cannot be un-bound by the user with the returned
+     * FileHandle.
+     *
+     * <p>The loop in this function handles the inherent race window between un-binding to a port
+     * and re-binding, during which the system could *technically* hand that port out to someone
+     * else.
+     */
+    private void bindToRandomPort(FileDescriptor sockFd) throws IOException {
+        for (int i = MAX_PORT_BIND_ATTEMPTS; i > 0; i--) {
+            try {
+                FileDescriptor probeSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+                Os.bind(probeSocket, INADDR_ANY, 0);
+                int port = ((InetSocketAddress) Os.getsockname(probeSocket)).getPort();
+                Os.close(probeSocket);
+                Log.v(TAG, "Binding to port " + port);
+                Os.bind(sockFd, INADDR_ANY, port);
+                return;
+            } catch (ErrnoException e) {
+                // Someone miraculously claimed the port just after we closed probeSocket.
+                if (e.errno == OsConstants.EADDRINUSE) {
+                    continue;
+                }
+                throw e.rethrowAsIOException();
+            }
+        }
+        throw new IOException("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+    }
 
     /**
      * Open a socket via the system server and bind it to the specified port (random if port=0).
@@ -323,13 +534,47 @@
      * needed.
      */
     @Override
-    public Bundle openUdpEncapsulationSocket(int port, IBinder binder) throws RemoteException {
-        return null;
+    public synchronized IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, IBinder binder)
+            throws RemoteException {
+        if (port != 0 && (port < FREE_PORT_MIN || port > PORT_MAX)) {
+            throw new IllegalArgumentException(
+                    "Specified port number must be a valid non-reserved UDP port");
+        }
+        int resourceId = mNextResourceId.getAndIncrement();
+        FileDescriptor sockFd = null;
+        try {
+            sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+
+            if (port != 0) {
+                Log.v(TAG, "Binding to port " + port);
+                Os.bind(sockFd, INADDR_ANY, port);
+            } else {
+                bindToRandomPort(sockFd);
+            }
+            // This code is common to both the unspecified and specified port cases
+            Os.setsockoptInt(
+                    sockFd,
+                    OsConstants.IPPROTO_UDP,
+                    OsConstants.UDP_ENCAP,
+                    OsConstants.UDP_ENCAP_ESPINUDP);
+
+            mUdpSocketRecords.put(
+                    resourceId, new UdpSocketRecord(resourceId, binder, sockFd, port));
+            return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port, sockFd);
+        } catch (IOException | ErrnoException e) {
+            IoUtils.closeQuietly(sockFd);
+        }
+        // If we make it to here, then something has gone wrong and we couldn't open a socket.
+        // The only reasonable condition that would cause that is resource unavailable.
+        return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
     }
 
     /** close a socket that has been been allocated by and registered with the system server */
     @Override
-    public void closeUdpEncapsulationSocket(ParcelFileDescriptor socket) {}
+    public void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
+
+        releaseManagedResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket");
+    }
 
     /**
      * Create a transport mode transform, which represent two security associations (one in each
@@ -339,13 +584,26 @@
      * receive data.
      */
     @Override
-    public Bundle createTransportModeTransform(IpSecConfig c, IBinder binder)
-            throws RemoteException {
-        // TODO: Basic input validation here since it's coming over the Binder
+    public synchronized IpSecTransformResponse createTransportModeTransform(
+            IpSecConfig c, IBinder binder) throws RemoteException {
         int resourceId = mNextResourceId.getAndIncrement();
+        SpiRecord[] spis = new SpiRecord[DIRECTIONS.length];
+        // TODO: Basic input validation here since it's coming over the Binder
+        int encapType, encapLocalPort = 0, encapRemotePort = 0;
+        UdpSocketRecord socketRecord = null;
+        encapType = c.getEncapType();
+        if (encapType != IpSecTransform.ENCAP_NONE) {
+            socketRecord = mUdpSocketRecords.get(c.getEncapLocalResourceId());
+            encapLocalPort = socketRecord.getPort();
+            encapRemotePort = c.getEncapRemotePort();
+        }
+
         for (int direction : DIRECTIONS) {
             IpSecAlgorithm auth = c.getAuthentication(direction);
             IpSecAlgorithm crypt = c.getEncryption(direction);
+
+            spis[direction] = mSpiRecords.get(c.getSpiResourceId(direction));
+            int spi = spis[direction].getSpi();
             try {
                 int result =
                         getNetdInstance()
@@ -362,35 +620,29 @@
                                         (c.getNetwork() != null)
                                                 ? c.getNetwork().getNetworkHandle()
                                                 : 0,
-                                        c.getSpi(direction),
+                                        spi,
                                         (auth != null) ? auth.getName() : "",
                                         (auth != null) ? auth.getKey() : null,
                                         (auth != null) ? auth.getTruncationLengthBits() : 0,
                                         (crypt != null) ? crypt.getName() : "",
                                         (crypt != null) ? crypt.getKey() : null,
                                         (crypt != null) ? crypt.getTruncationLengthBits() : 0,
-                                        c.getEncapType(),
-                                        c.getEncapLocalPort(),
-                                        c.getEncapRemotePort());
-                if (result != c.getSpi(direction)) {
+                                        encapType,
+                                        encapLocalPort,
+                                        encapRemotePort);
+                if (result != spi) {
                     // TODO: cleanup the first SA if creation of second SA fails
-                    Bundle retBundle = new Bundle(2);
-                    retBundle.putInt(KEY_STATUS, IpSecManager.Status.SPI_UNAVAILABLE);
-                    retBundle.putInt(KEY_RESOURCE_ID, INVALID_RESOURCE_ID);
-                    return retBundle;
+                    return new IpSecTransformResponse(
+                            IpSecManager.Status.SPI_UNAVAILABLE, INVALID_RESOURCE_ID);
                 }
             } catch (ServiceSpecificException e) {
                 // FIXME: get the error code and throw is at an IOException from Errno Exception
             }
         }
-        synchronized (mTransformRecords) {
-            mTransformRecords.put(resourceId, new TransformRecord(c, resourceId, binder));
-        }
-
-        Bundle retBundle = new Bundle(2);
-        retBundle.putInt(KEY_STATUS, IpSecManager.Status.OK);
-        retBundle.putInt(KEY_RESOURCE_ID, resourceId);
-        return retBundle;
+        // Both SAs were created successfully, time to construct a record and lock it away
+        mTransformRecords.put(
+                resourceId, new TransformRecord(resourceId, binder, c, spis, socketRecord));
+        return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId);
     }
 
     /**
@@ -401,27 +653,7 @@
      */
     @Override
     public void deleteTransportModeTransform(int resourceId) throws RemoteException {
-        synchronized (mTransformRecords) {
-            TransformRecord record;
-            // We want to non-destructively get so that we can check credentials before removing
-            // this from the records.
-            record = mTransformRecords.get(resourceId);
-
-            if (record == null) {
-                throw new IllegalArgumentException(
-                        "Transform " + resourceId + " is not available to be deleted");
-            }
-
-            if (record.pid != Binder.getCallingPid() || record.uid != Binder.getCallingUid()) {
-                throw new SecurityException("Only the owner of an IpSec Transform may delete it!");
-            }
-
-            // TODO: if releaseResources() throws RemoteException, we can try again to clean up on
-            // binder death. Need to make sure that path is actually functional.
-            record.releaseResources();
-            mTransformRecords.remove(resourceId);
-            record.nullifyRecord();
-        }
+        releaseManagedResource(mTransformRecords, resourceId, "IpSecTransform");
     }
 
     /**
@@ -429,44 +661,43 @@
      * association as a correspondent policy to the provided socket
      */
     @Override
-    public void applyTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
-            throws RemoteException {
+    public synchronized void applyTransportModeTransform(
+            ParcelFileDescriptor socket, int resourceId) throws RemoteException {
+        // Synchronize liberally here because we are using ManagedResources in this block
+        TransformRecord info;
+        // FIXME: this code should be factored out into a security check + getter
+        info = mTransformRecords.get(resourceId);
 
-        synchronized (mTransformRecords) {
-            TransformRecord info;
-            // FIXME: this code should be factored out into a security check + getter
-            info = mTransformRecords.get(resourceId);
+        if (info == null) {
+            throw new IllegalArgumentException("Transform " + resourceId + " is not active");
+        }
 
-            if (info == null) {
-                throw new IllegalArgumentException("Transform " + resourceId + " is not active");
+        // TODO: make this a function.
+        if (info.pid != getCallingPid() || info.uid != getCallingUid()) {
+            throw new SecurityException("Only the owner of an IpSec Transform may apply it!");
+        }
+
+        IpSecConfig c = info.getConfig();
+        try {
+            for (int direction : DIRECTIONS) {
+                getNetdInstance()
+                        .ipSecApplyTransportModeTransform(
+                                socket.getFileDescriptor(),
+                                resourceId,
+                                direction,
+                                (c.getLocalAddress() != null)
+                                        ? c.getLocalAddress().getHostAddress()
+                                        : "",
+                                (c.getRemoteAddress() != null)
+                                        ? c.getRemoteAddress().getHostAddress()
+                                        : "",
+                                info.getSpiRecord(direction).getSpi());
             }
-
-            // TODO: make this a function.
-            if (info.pid != getCallingPid() || info.uid != getCallingUid()) {
-                throw new SecurityException("Only the owner of an IpSec Transform may apply it!");
-            }
-
-            IpSecConfig c = info.getConfig();
-            try {
-                for (int direction : DIRECTIONS) {
-                    getNetdInstance()
-                            .ipSecApplyTransportModeTransform(
-                                    socket.getFileDescriptor(),
-                                    resourceId,
-                                    direction,
-                                    (c.getLocalAddress() != null)
-                                            ? c.getLocalAddress().getHostAddress()
-                                            : "",
-                                    (c.getRemoteAddress() != null)
-                                            ? c.getRemoteAddress().getHostAddress()
-                                            : "",
-                                    c.getSpi(direction));
-                }
-            } catch (ServiceSpecificException e) {
-                // FIXME: get the error code and throw is at an IOException from Errno Exception
-            }
+        } catch (ServiceSpecificException e) {
+            // FIXME: get the error code and throw is at an IOException from Errno Exception
         }
     }
+
     /**
      * Remove a transport mode transform from a socket, applying the default (empty) policy. This
      * will ensure that NO IPsec policy is applied to the socket (would be the equivalent of
@@ -486,7 +717,7 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
-
+        // TODO: Add dump code to print out a log of all the resources being tracked
         pw.println("IpSecService Log:");
         pw.println("NetdNativeService Connection: " + (isNetdAlive() ? "alive" : "dead"));
         pw.println();
diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java
index bb530fb..3efef01 100644
--- a/services/core/java/com/android/server/NsdService.java
+++ b/services/core/java/com/android/server/NsdService.java
@@ -21,11 +21,14 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
+import android.net.Uri;
 import android.net.nsd.NsdServiceInfo;
 import android.net.nsd.DnsSdTxtRecord;
 import android.net.nsd.INsdManager;
 import android.net.nsd.NsdManager;
 import android.os.Binder;
+import android.os.HandlerThread;
+import android.os.Handler;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.UserHandle;
@@ -33,19 +36,21 @@
 import android.util.Base64;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.net.InetAddress;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Protocol;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.server.NativeDaemonConnector.Command;
 
 /**
  * Network Service Discovery Service handles remote service discovery operation requests by
@@ -60,10 +65,10 @@
     private static final boolean DBG = true;
 
     private final Context mContext;
-    private final ContentResolver mContentResolver;
+    private final NsdSettings mNsdSettings;
     private final NsdStateMachine mNsdStateMachine;
-    private final NativeDaemonConnector mNativeConnector;
-    private final CountDownLatch mNativeDaemonConnected = new CountDownLatch(1);
+    private final DaemonConnection mDaemon;
+    private final NativeCallbackReceiver mDaemonCallback;
 
     /**
      * Clients receiving asynchronous messages
@@ -93,32 +98,24 @@
          * Observes the NSD on/off setting, and takes action when changed.
          */
         private void registerForNsdSetting() {
-            ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
+            final ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
                 @Override
                 public void onChange(boolean selfChange) {
-                    if (isNsdEnabled()) {
-                        mNsdStateMachine.sendMessage(NsdManager.ENABLE);
-                    } else {
-                        mNsdStateMachine.sendMessage(NsdManager.DISABLE);
-                    }
+                    notifyEnabled(isNsdEnabled());
                 }
             };
 
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.NSD_ON),
-                    false, contentObserver);
+            final Uri uri = Settings.Global.getUriFor(Settings.Global.NSD_ON);
+            mNsdSettings.registerContentObserver(uri, contentObserver);
         }
 
-        NsdStateMachine(String name) {
-            super(name);
+        NsdStateMachine(String name, Handler handler) {
+            super(name, handler);
             addState(mDefaultState);
                 addState(mDisabledState, mDefaultState);
                 addState(mEnabledState, mDefaultState);
-            if (isNsdEnabled()) {
-                setInitialState(mEnabledState);
-            } else {
-                setInitialState(mDisabledState);
-            }
+            State initialState = isNsdEnabled() ? mEnabledState : mDisabledState;
+            setInitialState(initialState);
             setLogRecSize(25);
             registerForNsdSetting();
         }
@@ -158,7 +155,7 @@
                         }
                         //Last client
                         if (mClients.size() == 0) {
-                            stopMDnsDaemon();
+                            mDaemon.stop();
                         }
                         break;
                     case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
@@ -218,14 +215,14 @@
             public void enter() {
                 sendNsdStateChangeBroadcast(true);
                 if (mClients.size() > 0) {
-                    startMDnsDaemon();
+                    mDaemon.start();
                 }
             }
 
             @Override
             public void exit() {
                 if (mClients.size() > 0) {
-                    stopMDnsDaemon();
+                    mDaemon.stop();
                 }
             }
 
@@ -244,8 +241,8 @@
             }
 
             private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) {
-                clientInfo.mClientIds.remove(clientId);
-                clientInfo.mClientRequests.remove(clientId);
+                clientInfo.mClientIds.delete(clientId);
+                clientInfo.mClientRequests.delete(clientId);
                 mIdToClientInfoMap.remove(globalId);
             }
 
@@ -259,7 +256,7 @@
                         //First client
                         if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL &&
                                 mClients.size() == 0) {
-                            startMDnsDaemon();
+                            mDaemon.start();
                         }
                         return NOT_HANDLED;
                     case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
@@ -298,7 +295,7 @@
                         clientInfo = mClients.get(msg.replyTo);
 
                         try {
-                            id = clientInfo.mClientIds.get(msg.arg2).intValue();
+                            id = clientInfo.mClientIds.get(msg.arg2);
                         } catch (NullPointerException e) {
                             replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED,
                                     NsdManager.FAILURE_INTERNAL_ERROR);
@@ -336,7 +333,7 @@
                         if (DBG) Slog.d(TAG, "unregister service");
                         clientInfo = mClients.get(msg.replyTo);
                         try {
-                            id = clientInfo.mClientIds.get(msg.arg2).intValue();
+                            id = clientInfo.mClientIds.get(msg.arg2);
                         } catch (NullPointerException e) {
                             replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED,
                                     NsdManager.FAILURE_INTERNAL_ERROR);
@@ -542,57 +539,55 @@
         return sb.toString();
     }
 
-    private NsdService(Context context) {
-        mContext = context;
-        mContentResolver = context.getContentResolver();
-
-        mNativeConnector = new NativeDaemonConnector(new NativeCallbackReceiver(), "mdns", 10,
-                MDNS_TAG, 25, null);
-
-        mNsdStateMachine = new NsdStateMachine(TAG);
+    @VisibleForTesting
+    NsdService(Context ctx, NsdSettings settings, Handler handler, DaemonConnectionSupplier fn) {
+        mContext = ctx;
+        mNsdSettings = settings;
+        mNsdStateMachine = new NsdStateMachine(TAG, handler);
         mNsdStateMachine.start();
-
-        Thread th = new Thread(mNativeConnector, MDNS_TAG);
-        th.start();
+        mDaemonCallback = new NativeCallbackReceiver();
+        mDaemon = fn.get(mDaemonCallback);
     }
 
     public static NsdService create(Context context) throws InterruptedException {
-        NsdService service = new NsdService(context);
-        service.mNativeDaemonConnected.await();
+        NsdSettings settings = NsdSettings.makeDefault(context);
+        HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        NsdService service = new NsdService(context, settings, handler, DaemonConnection::new);
+        service.mDaemonCallback.awaitConnection();
         return service;
     }
 
     public Messenger getMessenger() {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
-            "NsdService");
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
         return new Messenger(mNsdStateMachine.getHandler());
     }
 
-    public void setEnabled(boolean enable) {
+    public void setEnabled(boolean isEnabled) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL,
                 "NsdService");
-        Settings.Global.putInt(mContentResolver, Settings.Global.NSD_ON, enable ? 1 : 0);
-        if (enable) {
-            mNsdStateMachine.sendMessage(NsdManager.ENABLE);
-        } else {
-            mNsdStateMachine.sendMessage(NsdManager.DISABLE);
-        }
+        mNsdSettings.putEnabledStatus(isEnabled);
+        notifyEnabled(isEnabled);
     }
 
-    private void sendNsdStateChangeBroadcast(boolean enabled) {
+    private void notifyEnabled(boolean isEnabled) {
+        mNsdStateMachine.sendMessage(isEnabled ? NsdManager.ENABLE : NsdManager.DISABLE);
+    }
+
+    private void sendNsdStateChangeBroadcast(boolean isEnabled) {
         final Intent intent = new Intent(NsdManager.ACTION_NSD_STATE_CHANGED);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        if (enabled) {
-            intent.putExtra(NsdManager.EXTRA_NSD_STATE, NsdManager.NSD_STATE_ENABLED);
-        } else {
-            intent.putExtra(NsdManager.EXTRA_NSD_STATE, NsdManager.NSD_STATE_DISABLED);
-        }
+        int nsdState = isEnabled ? NsdManager.NSD_STATE_ENABLED : NsdManager.NSD_STATE_DISABLED;
+        intent.putExtra(NsdManager.EXTRA_NSD_STATE, nsdState);
         mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
     }
 
     private boolean isNsdEnabled() {
-        boolean ret = Settings.Global.getInt(mContentResolver, Settings.Global.NSD_ON, 1) == 1;
-        if (DBG) Slog.d(TAG, "Network service discovery enabled " + ret);
+        boolean ret = mNsdSettings.isEnabled();
+        if (DBG) {
+            Slog.d(TAG, "Network service discovery is " + (ret ? "enabled" : "disabled"));
+        }
         return ret;
     }
 
@@ -656,14 +651,23 @@
     }
 
     class NativeCallbackReceiver implements INativeDaemonConnectorCallbacks {
-        public void onDaemonConnected() {
-            mNativeDaemonConnected.countDown();
+        private final CountDownLatch connected = new CountDownLatch(1);
+
+        public void awaitConnection() throws InterruptedException {
+            connected.await();
         }
 
+        @Override
+        public void onDaemonConnected() {
+            connected.countDown();
+        }
+
+        @Override
         public boolean onCheckHoldWakeLock(int code) {
             return false;
         }
 
+        @Override
         public boolean onEvent(int code, String raw, String[] cooked) {
             // TODO: NDC translates a message to a callback, we could enhance NDC to
             // directly interact with a state machine through messages
@@ -673,132 +677,88 @@
         }
     }
 
-    private boolean startMDnsDaemon() {
-        if (DBG) Slog.d(TAG, "startMDnsDaemon");
-        try {
-            mNativeConnector.execute("mdnssd", "start-service");
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to start daemon" + e);
-            return false;
-        }
-        return true;
+    interface DaemonConnectionSupplier {
+        DaemonConnection get(NativeCallbackReceiver callback);
     }
 
-    private boolean stopMDnsDaemon() {
-        if (DBG) Slog.d(TAG, "stopMDnsDaemon");
-        try {
-            mNativeConnector.execute("mdnssd", "stop-service");
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to start daemon" + e);
-            return false;
+    @VisibleForTesting
+    public static class DaemonConnection {
+        final NativeDaemonConnector mNativeConnector;
+
+        DaemonConnection(NativeCallbackReceiver callback) {
+            mNativeConnector = new NativeDaemonConnector(callback, "mdns", 10, MDNS_TAG, 25, null);
+            new Thread(mNativeConnector, MDNS_TAG).start();
         }
-        return true;
+
+        public boolean execute(Object... args) {
+            if (DBG) {
+                Slog.d(TAG, "mdnssd " + Arrays.toString(args));
+            }
+            try {
+                mNativeConnector.execute("mdnssd", args);
+            } catch (NativeDaemonConnectorException e) {
+                Slog.e(TAG, "Failed to execute mdnssd " + Arrays.toString(args), e);
+                return false;
+            }
+            return true;
+        }
+
+        public void start() {
+            execute("start-service");
+        }
+
+        public void stop() {
+            execute("stop-service");
+        }
     }
 
     private boolean registerService(int regId, NsdServiceInfo service) {
-        if (DBG) Slog.d(TAG, "registerService: " + regId + " " + service);
-        try {
-            Command cmd = new Command("mdnssd", "register", regId, service.getServiceName(),
-                    service.getServiceType(), service.getPort(),
-                    Base64.encodeToString(service.getTxtRecord(), Base64.DEFAULT)
-                            .replace("\n", ""));
-
-            mNativeConnector.execute(cmd);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to execute registerService " + e);
-            return false;
+        if (DBG) {
+            Slog.d(TAG, "registerService: " + regId + " " + service);
         }
-        return true;
+        String name = service.getServiceName();
+        String type = service.getServiceType();
+        int port = service.getPort();
+        byte[] textRecord = service.getTxtRecord();
+        String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", "");
+        return mDaemon.execute("register", regId, name, type, port, record);
     }
 
     private boolean unregisterService(int regId) {
-        if (DBG) Slog.d(TAG, "unregisterService: " + regId);
-        try {
-            mNativeConnector.execute("mdnssd", "stop-register", regId);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to execute unregisterService " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("stop-register", regId);
     }
 
     private boolean updateService(int regId, DnsSdTxtRecord t) {
-        if (DBG) Slog.d(TAG, "updateService: " + regId + " " + t);
-        try {
-            if (t == null) return false;
-            mNativeConnector.execute("mdnssd", "update", regId, t.size(), t.getRawData());
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to updateServices " + e);
+        if (t == null) {
             return false;
         }
-        return true;
+        return mDaemon.execute("update", regId, t.size(), t.getRawData());
     }
 
     private boolean discoverServices(int discoveryId, String serviceType) {
-        if (DBG) Slog.d(TAG, "discoverServices: " + discoveryId + " " + serviceType);
-        try {
-            mNativeConnector.execute("mdnssd", "discover", discoveryId, serviceType);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to discoverServices " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("discover", discoveryId, serviceType);
     }
 
     private boolean stopServiceDiscovery(int discoveryId) {
-        if (DBG) Slog.d(TAG, "stopServiceDiscovery: " + discoveryId);
-        try {
-            mNativeConnector.execute("mdnssd", "stop-discover", discoveryId);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to stopServiceDiscovery " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("stop-discover", discoveryId);
     }
 
     private boolean resolveService(int resolveId, NsdServiceInfo service) {
-        if (DBG) Slog.d(TAG, "resolveService: " + resolveId + " " + service);
-        try {
-            mNativeConnector.execute("mdnssd", "resolve", resolveId, service.getServiceName(),
-                    service.getServiceType(), "local.");
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to resolveService " + e);
-            return false;
-        }
-        return true;
+        String name = service.getServiceName();
+        String type = service.getServiceType();
+        return mDaemon.execute("resolve", resolveId, name, type, "local.");
     }
 
     private boolean stopResolveService(int resolveId) {
-        if (DBG) Slog.d(TAG, "stopResolveService: " + resolveId);
-        try {
-            mNativeConnector.execute("mdnssd", "stop-resolve", resolveId);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to stop resolve " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("stop-resolve", resolveId);
     }
 
     private boolean getAddrInfo(int resolveId, String hostname) {
-        if (DBG) Slog.d(TAG, "getAdddrInfo: " + resolveId);
-        try {
-            mNativeConnector.execute("mdnssd", "getaddrinfo", resolveId, hostname);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to getAddrInfo " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("getaddrinfo", resolveId, hostname);
     }
 
     private boolean stopGetAddrInfo(int resolveId) {
-        if (DBG) Slog.d(TAG, "stopGetAdddrInfo: " + resolveId);
-        try {
-            mNativeConnector.execute("mdnssd", "stop-getaddrinfo", resolveId);
-        } catch(NativeDaemonConnectorException e) {
-            Slog.e(TAG, "Failed to stopGetAddrInfo " + e);
-            return false;
-        }
-        return true;
+        return mDaemon.execute("stop-getaddrinfo", resolveId);
     }
 
     @Override
@@ -854,10 +814,10 @@
         private NsdServiceInfo mResolvedService;
 
         /* A map from client id to unique id sent to mDns */
-        private final SparseArray<Integer> mClientIds = new SparseArray<>();
+        private final SparseIntArray mClientIds = new SparseIntArray();
 
         /* A map from client id to the type of the request we had received */
-        private final SparseArray<Integer> mClientRequests = new SparseArray<>();
+        private final SparseIntArray mClientRequests = new SparseIntArray();
 
         private ClientInfo(AsyncChannel c, Messenger m) {
             mChannel = c;
@@ -884,6 +844,7 @@
         // and send cancellations to the daemon.
         private void expungeAllRequests() {
             int globalId, clientId, i;
+            // TODO: to keep handler responsive, do not clean all requests for that client at once.
             for (i = 0; i < mClientIds.size(); i++) {
                 clientId = mClientIds.keyAt(i);
                 globalId = mClientIds.valueAt(i);
@@ -911,15 +872,42 @@
         // mClientIds is a sparse array of listener id -> mDnsClient id.  For a given mDnsClient id,
         // return the corresponding listener id.  mDnsClient id is also called a global id.
         private int getClientId(final int globalId) {
-            // This doesn't use mClientIds.indexOfValue because indexOfValue uses == (not .equals)
-            // while also coercing the int primitives to Integer objects.
-            for (int i = 0, nSize = mClientIds.size(); i < nSize; i++) {
-                int mDnsId = mClientIds.valueAt(i);
-                if (globalId == mDnsId) {
-                    return mClientIds.keyAt(i);
-                }
+            int idx = mClientIds.indexOfValue(globalId);
+            if (idx < 0) {
+                return idx;
             }
-            return -1;
+            return mClientIds.keyAt(idx);
+        }
+    }
+
+    /**
+     * Interface which encapsulates dependencies of NsdService that are hard to mock, hard to
+     * override, or have side effects on global state in unit tests.
+     */
+    @VisibleForTesting
+    public interface NsdSettings {
+        boolean isEnabled();
+        void putEnabledStatus(boolean isEnabled);
+        void registerContentObserver(Uri uri, ContentObserver observer);
+
+        static NsdSettings makeDefault(Context context) {
+            final ContentResolver resolver = context.getContentResolver();
+            return new NsdSettings() {
+                @Override
+                public boolean isEnabled() {
+                    return Settings.Global.getInt(resolver, Settings.Global.NSD_ON, 1) == 1;
+                }
+
+                @Override
+                public void putEnabledStatus(boolean isEnabled) {
+                    Settings.Global.putInt(resolver, Settings.Global.NSD_ON, isEnabled ? 1 : 0);
+                }
+
+                @Override
+                public void registerContentObserver(Uri uri, ContentObserver observer) {
+                    resolver.registerContentObserver(uri, false, observer);
+                }
+            };
         }
     }
 }
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index e746355..8d46351 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -31,7 +31,6 @@
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
 import static android.net.NetworkStats.SET_FOREGROUND;
-import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkTemplate.buildTemplateMobileWildcard;
@@ -58,14 +57,13 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.internal.util.Preconditions.checkArgument;
+
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.server.NetworkManagementService.LIMIT_GLOBAL_ALERT;
 import static com.android.server.NetworkManagementSocketTagger.resetKernelUidStats;
 import static com.android.server.NetworkManagementSocketTagger.setKernelCounterSet;
 
 import android.app.AlarmManager;
-import android.app.IAlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -1041,7 +1039,7 @@
         // snapshot and record current counters; read UID stats first to
         // avoid over counting dev stats.
         final NetworkStats uidSnapshot = getNetworkStatsUidDetail();
-        final NetworkStats xtSnapshot = getNetworkStatsXtAndVt();
+        final NetworkStats xtSnapshot = getNetworkStatsXt();
         final NetworkStats devSnapshot = mNetworkManager.getNetworkStatsSummaryDev();
 
 
@@ -1367,7 +1365,8 @@
 
     /**
      * Return snapshot of current UID statistics, including any
-     * {@link TrafficStats#UID_TETHERING} and {@link #mUidOperations} values.
+     * {@link TrafficStats#UID_TETHERING}, video calling data usage, and {@link #mUidOperations}
+     * values.
      */
     private NetworkStats getNetworkStatsUidDetail() throws RemoteException {
         final NetworkStats uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL);
@@ -1375,43 +1374,34 @@
         // fold tethering stats and operations into uid snapshot
         final NetworkStats tetherSnapshot = getNetworkStatsTethering();
         uidSnapshot.combineAllValues(tetherSnapshot);
+
+        final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
+                Context.TELEPHONY_SERVICE);
+
+        // fold video calling data usage stats into uid snapshot
+        final NetworkStats vtStats = telephonyManager.getVtDataUsage(true);
+        if (vtStats != null) {
+            uidSnapshot.combineAllValues(vtStats);
+        }
         uidSnapshot.combineAllValues(mUidOperations);
 
         return uidSnapshot;
     }
 
     /**
-     * Return snapshot of current XT plus VT statistics.
+     * Return snapshot of current XT statistics with video calling data usage statistics.
      */
-    private NetworkStats getNetworkStatsXtAndVt() throws RemoteException {
+    private NetworkStats getNetworkStatsXt() throws RemoteException {
         final NetworkStats xtSnapshot = mNetworkManager.getNetworkStatsSummaryXt();
 
-        TelephonyManager tm = (TelephonyManager) mContext.getSystemService(
+        final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
                 Context.TELEPHONY_SERVICE);
 
-        long usage = tm.getVtDataUsage();
-
-        if (LOGV) Slog.d(TAG, "VT call data usage = " + usage);
-
-        final NetworkStats vtSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 1);
-
-        final NetworkStats.Entry entry = new NetworkStats.Entry();
-        entry.iface = VT_INTERFACE;
-        entry.uid = -1;
-        entry.set = TAG_ALL;
-        entry.tag = TAG_NONE;
-
-        // Since modem only tell us the total usage instead of each usage for RX and TX,
-        // we need to split it up (though it might not quite accurate). At
-        // least we can make sure the data usage report to the user will still be accurate.
-        entry.rxBytes = usage / 2;
-        entry.rxPackets = 0;
-        entry.txBytes = usage - entry.rxBytes;
-        entry.txPackets = 0;
-        vtSnapshot.combineValues(entry);
-
-        // Merge VT int XT
-        xtSnapshot.combineAllValues(vtSnapshot);
+        // Merge video calling data usage into XT
+        final NetworkStats vtSnapshot = telephonyManager.getVtDataUsage(false);
+        if (vtSnapshot != null) {
+            xtSnapshot.combineAllValues(vtSnapshot);
+        }
 
         return xtSnapshot;
     }