Merge "Rename HdmiCecKeycodeTranslater to HdmiCecKeycode."
diff --git a/core/java/android/net/INetworkManagementEventObserver.aidl b/core/java/android/net/INetworkManagementEventObserver.aidl
index dd9c39f..b7af374 100644
--- a/core/java/android/net/INetworkManagementEventObserver.aidl
+++ b/core/java/android/net/INetworkManagementEventObserver.aidl
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.net.LinkAddress;
+import android.net.RouteInfo;
 
 /**
  * Callback class for receiving events from an INetworkManagementService
@@ -98,4 +99,14 @@
      * @param servers The IP addresses of the DNS servers.
      */
     void interfaceDnsServerInfo(String iface, long lifetime, in String[] servers);
+
+    /**
+     * A route has been added or updated.
+     */
+    void routeUpdated(in RouteInfo route);
+
+    /**
+     * A route has been removed.
+     */
+    void routeRemoved(in RouteInfo route);
 }
diff --git a/core/java/android/net/IpPrefix.java b/core/java/android/net/IpPrefix.java
index a14d13f..f1fa3eb 100644
--- a/core/java/android/net/IpPrefix.java
+++ b/core/java/android/net/IpPrefix.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Pair;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -46,9 +47,18 @@
     private final byte[] address;  // network byte order
     private final int prefixLength;
 
+    private void checkAndMaskAddressAndPrefixLength() {
+        if (address.length != 4 && address.length != 16) {
+            throw new IllegalArgumentException(
+                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
+        }
+        NetworkUtils.maskRawAddress(address, prefixLength);
+    }
+
     /**
      * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
-     * network byte order and a prefix length.
+     * network byte order and a prefix length. Silently truncates the address to the prefix length,
+     * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}.
      *
      * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
      * @param prefixLength the prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6).
@@ -56,24 +66,46 @@
      * @hide
      */
     public IpPrefix(byte[] address, int prefixLength) {
-        if (address.length != 4 && address.length != 16) {
-            throw new IllegalArgumentException(
-                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
-        }
-        if (prefixLength < 0 || prefixLength > (address.length * 8)) {
-            throw new IllegalArgumentException("IpPrefix with " + address.length +
-                    " bytes has invalid prefix length " + prefixLength);
-        }
         this.address = address.clone();
         this.prefixLength = prefixLength;
-        // TODO: Validate that the non-prefix bits are zero
+        checkAndMaskAddressAndPrefixLength();
     }
 
     /**
+     * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently
+     * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently
+     * converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
      * @hide
      */
     public IpPrefix(InetAddress address, int prefixLength) {
-        this(address.getAddress(), prefixLength);
+        // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array,
+        // which is unnecessary because getAddress() already returns a clone.
+        this.address = address.getAddress();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64".
+     * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24}
+     * is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param prefix the prefix to parse
+     *
+     * @hide
+     */
+    public IpPrefix(String prefix) {
+        // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
+        // first statement in constructor". We could factor out setting the member variables to an
+        // init() method, but if we did, then we'd have to make the members non-final, or "error:
+        // cannot assign a value to final variable address". So we just duplicate the code here.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(prefix);
+        this.address = ipAndMask.first.getAddress();
+        this.prefixLength = ipAndMask.second;
+        checkAndMaskAddressAndPrefixLength();
     }
 
     /**
@@ -129,7 +161,7 @@
     }
 
     /**
-     * Returns the prefix length of this {@code IpAddress}.
+     * Returns the prefix length of this {@code IpPrefix}.
      *
      * @return the prefix length.
      */
@@ -138,6 +170,20 @@
     }
 
     /**
+     * Returns a string representation of this {@code IpPrefix}.
+     *
+     * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::"}.
+     */
+    public String toString() {
+        try {
+            return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength;
+        } catch(UnknownHostException e) {
+            // Cosmic rays?
+            throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e);
+        }
+    }
+
+    /**
      * Implement the Parcelable interface.
      */
     public int describeContents() {
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index 5246078..f9a25f9 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Pair;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -166,23 +167,9 @@
      * @hide
      */
     public LinkAddress(String address, int flags, int scope) {
-        InetAddress inetAddress = null;
-        int prefixLength = -1;
-        try {
-            String [] pieces = address.split("/", 2);
-            prefixLength = Integer.parseInt(pieces[1]);
-            inetAddress = InetAddress.parseNumericAddress(pieces[0]);
-        } catch (NullPointerException e) {            // Null string.
-        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
-        } catch (NumberFormatException e) {           // Non-numeric prefix.
-        } catch (IllegalArgumentException e) {        // Invalid IP address.
-        }
-
-        if (inetAddress == null || prefixLength == -1) {
-            throw new IllegalArgumentException("Bad LinkAddress params " + address);
-        }
-
-        init(inetAddress, prefixLength, flags, scope);
+        // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+        init(ipAndMask.first, ipAndMask.second, flags, scope);
     }
 
     /**
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
index 8eefa0f..e7184ed 100644
--- a/core/java/android/net/LinkProperties.java
+++ b/core/java/android/net/LinkProperties.java
@@ -31,6 +31,7 @@
 import java.util.Collections;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Describes the properties of a network link.
@@ -334,15 +335,17 @@
     }
 
     /**
-     * Adds a {@link RouteInfo} to this {@code LinkProperties}.  If the {@link RouteInfo}
-     * had an interface name set and that differs from the interface set for this
-     * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown.  The
-     * proper course is to add either un-named or properly named {@link RouteInfo}.
+     * Adds a {@link RouteInfo} to this {@code LinkProperties}, if not present. If the
+     * {@link RouteInfo} had an interface name set and that differs from the interface set for this
+     * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown.  The proper
+     * course is to add either un-named or properly named {@link RouteInfo}.
      *
      * @param route A {@link RouteInfo} to add to this object.
+     * @return {@code false} if the route was already present, {@code true} if it was added.
+     *
      * @hide
      */
-    public void addRoute(RouteInfo route) {
+    public boolean addRoute(RouteInfo route) {
         if (route != null) {
             String routeIface = route.getInterface();
             if (routeIface != null && !routeIface.equals(mIfaceName)) {
@@ -350,8 +353,28 @@
                    "Route added with non-matching interface: " + routeIface +
                    " vs. " + mIfaceName);
             }
-            mRoutes.add(routeWithInterface(route));
+            route = routeWithInterface(route);
+            if (!mRoutes.contains(route)) {
+                mRoutes.add(route);
+                return true;
+            }
         }
+        return false;
+    }
+
+    /**
+     * Removes a {@link RouteInfo} from this {@code LinkProperties}, if present. The route must
+     * specify an interface and the interface must match the interface of this
+     * {@code LinkProperties}, or it will not be removed.
+     *
+     * @return {@code true} if the route was removed, {@code false} if it was not present.
+     *
+     * @hide
+     */
+    public boolean removeRoute(RouteInfo route) {
+        return route != null &&
+                Objects.equals(mIfaceName, route.getInterface()) &&
+                mRoutes.remove(route);
     }
 
     /**
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index b02f88e..15c0a71 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -24,6 +24,8 @@
 import java.util.Locale;
 
 import android.util.Log;
+import android.util.Pair;
+
 
 /**
  * Native methods for managing network interfaces.
@@ -218,24 +220,17 @@
     }
 
     /**
-     * Get InetAddress masked with prefixLength.  Will never return null.
-     * @param IP address which will be masked with specified prefixLength
-     * @param prefixLength the prefixLength used to mask the IP
+     *  Masks a raw IP address byte array with the specified prefix length.
      */
-    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
-        if (address == null) {
-            throw new RuntimeException("getNetworkPart doesn't accept null address");
-        }
-
-        byte[] array = address.getAddress();
-
+    public static void maskRawAddress(byte[] array, int prefixLength) {
         if (prefixLength < 0 || prefixLength > array.length * 8) {
-            throw new RuntimeException("getNetworkPart - bad prefixLength");
+            throw new RuntimeException("IP address with " + array.length +
+                    " bytes has invalid prefix length " + prefixLength);
         }
 
         int offset = prefixLength / 8;
-        int reminder = prefixLength % 8;
-        byte mask = (byte)(0xFF << (8 - reminder));
+        int remainder = prefixLength % 8;
+        byte mask = (byte)(0xFF << (8 - remainder));
 
         if (offset < array.length) array[offset] = (byte)(array[offset] & mask);
 
@@ -244,6 +239,16 @@
         for (; offset < array.length; offset++) {
             array[offset] = 0;
         }
+    }
+
+    /**
+     * Get InetAddress masked with prefixLength.  Will never return null.
+     * @param address the IP address to mask with
+     * @param prefixLength the prefixLength used to mask the IP
+     */
+    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
+        byte[] array = address.getAddress();
+        maskRawAddress(array, prefixLength);
 
         InetAddress netPart = null;
         try {
@@ -255,6 +260,30 @@
     }
 
     /**
+     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
+     * @hide
+     */
+    public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) {
+        InetAddress address = null;
+        int prefixLength = -1;
+        try {
+            String[] pieces = ipAndMaskString.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            address = InetAddress.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        }
+
+        if (address == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
+        }
+
+        return new Pair<InetAddress, Integer>(address, prefixLength);
+    }
+
+    /**
      * Check if IP address type is consistent between two InetAddress.
      * @return true if both are the same type.  False otherwise.
      */
diff --git a/core/java/com/android/server/net/BaseNetworkObserver.java b/core/java/com/android/server/net/BaseNetworkObserver.java
index 430dd63..3d9fb5c 100644
--- a/core/java/com/android/server/net/BaseNetworkObserver.java
+++ b/core/java/com/android/server/net/BaseNetworkObserver.java
@@ -18,6 +18,7 @@
 
 import android.net.INetworkManagementEventObserver;
 import android.net.LinkAddress;
+import android.net.RouteInfo;
 
 /**
  * Base {@link INetworkManagementEventObserver} that provides no-op
@@ -70,4 +71,14 @@
     public void interfaceDnsServerInfo(String iface, long lifetime, String[] servers) {
         // default no-op
     }
+
+    @Override
+    public void routeUpdated(RouteInfo route) {
+        // default no-op
+    }
+
+    @Override
+    public void routeRemoved(RouteInfo route) {
+        // default no-op
+    }
 }
diff --git a/core/java/com/android/server/net/NetlinkTracker.java b/core/java/com/android/server/net/NetlinkTracker.java
new file mode 100644
index 0000000..7dd8dd8
--- /dev/null
+++ b/core/java/com/android/server/net/NetlinkTracker.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.util.Log;
+
+/**
+ * Keeps track of link configuration received from Netlink.
+ *
+ * Instances of this class are expected to be owned by subsystems such as Wi-Fi
+ * or Ethernet that manage one or more network interfaces. Each interface to be
+ * tracked needs its own {@code NetlinkTracker}.
+ *
+ * An instance of this class is constructed by passing in an interface name and
+ * a callback. The owner is then responsible for registering the tracker with
+ * NetworkManagementService. When the class receives update notifications from
+ * the NetworkManagementService notification threads, it applies the update to
+ * its local LinkProperties, and if something has changed, notifies its owner of
+ * the update via the callback.
+ *
+ * The owner can then call {@code getLinkProperties()} in order to find out
+ * what changed. If in the meantime the LinkProperties stored here have changed,
+ * this class will return the current LinkProperties. Because each change
+ * triggers an update callback after the change is made, the owner may get more
+ * callbacks than strictly necessary (some of which may be no-ops), but will not
+ * be out of sync once all callbacks have been processed.
+ *
+ * Threading model:
+ *
+ * - The owner of this class is expected to create it, register it, and call
+ *   getLinkProperties or clearLinkProperties on its thread.
+ * - Most of the methods in the class are inherited from BaseNetworkObserver
+ *   and are called by NetworkManagementService notification threads.
+ * - All accesses to mLinkProperties must be synchronized(this). All the other
+ *   member variables are immutable once the object is constructed.
+ *
+ * This class currently tracks IPv4 and IPv6 addresses. In the future it will
+ * track routes and DNS servers.
+ *
+ * @hide
+ */
+public class NetlinkTracker extends BaseNetworkObserver {
+
+    private final String TAG;
+
+    public interface Callback {
+        public void update();
+    }
+
+    private final String mInterfaceName;
+    private final Callback mCallback;
+    private final LinkProperties mLinkProperties;
+
+    private static final boolean DBG = true;
+
+    public NetlinkTracker(String iface, Callback callback) {
+        TAG = "NetlinkTracker/" + iface;
+        mInterfaceName = iface;
+        mCallback = callback;
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+    }
+
+    private void maybeLog(String operation, String iface, LinkAddress address) {
+        if (DBG) {
+            Log.d(TAG, operation + ": " + address + " on " + iface +
+                    " flags " + address.getFlags() + " scope " + address.getScope());
+        }
+    }
+
+    private void maybeLog(String operation, Object o) {
+        if (DBG) {
+            Log.d(TAG, operation + ": " + o.toString());
+        }
+    }
+
+    @Override
+    public void addressUpdated(String iface, LinkAddress address) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("addressUpdated", iface, address);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.addLinkAddress(address);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void addressRemoved(String iface, LinkAddress address) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("addressRemoved", iface, address);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.removeLinkAddress(address);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void routeUpdated(RouteInfo route) {
+        if (mInterfaceName.equals(route.getInterface())) {
+            maybeLog("routeUpdated", route);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.addRoute(route);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    @Override
+    public void routeRemoved(RouteInfo route) {
+        if (mInterfaceName.equals(route.getInterface())) {
+            maybeLog("routeRemoved", route);
+            boolean changed;
+            synchronized (this) {
+                changed = mLinkProperties.removeRoute(route);
+            }
+            if (changed) {
+                mCallback.update();
+            }
+        }
+    }
+
+    /**
+     * Returns a copy of this object's LinkProperties.
+     */
+    public synchronized LinkProperties getLinkProperties() {
+        return new LinkProperties(mLinkProperties);
+    }
+
+    public synchronized void clearLinkProperties() {
+        mLinkProperties.clear();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/IpPrefixTest.java b/core/tests/coretests/src/android/net/IpPrefixTest.java
new file mode 100644
index 0000000..cf278fb
--- /dev/null
+++ b/core/tests/coretests/src/android/net/IpPrefixTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2014 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.net.IpPrefix;
+import android.os.Parcel;
+import static android.test.MoreAsserts.assertNotEqual;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static org.junit.Assert.assertArrayEquals;
+import java.net.InetAddress;
+import java.util.Random;
+import junit.framework.TestCase;
+
+
+public class IpPrefixTest extends TestCase {
+
+    // Explicitly cast everything to byte because "error: possible loss of precision".
+    private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4};
+    private static final byte[] IPV6_BYTES = {
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
+        (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0
+    };
+
+    @SmallTest
+    public void testConstructor() {
+        IpPrefix p;
+        try {
+            p = new IpPrefix((byte[]) null, 9);
+            fail("Expected NullPointerException: null byte array");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix((InetAddress) null, 10);
+            fail("Expected NullPointerException: null InetAddress");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix((String) null);
+            fail("Expected NullPointerException: null String");
+        } catch(RuntimeException expected) {}
+
+
+        try {
+            byte[] b2 = {1, 2, 3, 4, 5};
+            p = new IpPrefix(b2, 29);
+            fail("Expected IllegalArgumentException: invalid array length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.4");
+            fail("Expected IllegalArgumentException: no prefix length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.4/");
+            fail("Expected IllegalArgumentException: empty prefix length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: invalid address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1/32");
+            fail("Expected IllegalArgumentException: deprecated IPv4 format");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.256/32");
+            fail("Expected IllegalArgumentException: invalid IPv4 address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: non-address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("f00:::/32");
+            fail("Expected IllegalArgumentException: invalid IPv6 address");
+        } catch(IllegalArgumentException expected) {}
+    }
+
+    public void testTruncation() {
+        IpPrefix p;
+
+        p = new IpPrefix(IPV4_BYTES, 32);
+        assertEquals("192.0.2.4/32", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 29);
+        assertEquals("192.0.2.0/29", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 8);
+        assertEquals("192.0.0.0/8", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 0);
+        assertEquals("0.0.0.0/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 33);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 128);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+        p = new IpPrefix(IPV6_BYTES, 128);
+        assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 64);
+        assertEquals("2001:db8:dead:beef::/64", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 3);
+        assertEquals("2000::/3", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 0);
+        assertEquals("::/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, 129);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+    }
+
+    private void assertAreEqual(Object o1, Object o2) {
+        assertTrue(o1.equals(o2));
+        assertTrue(o2.equals(o1));
+    }
+
+    private void assertAreNotEqual(Object o1, Object o2) {
+        assertFalse(o1.equals(o2));
+        assertFalse(o2.equals(o1));
+    }
+
+    @SmallTest
+    public void testEquals() {
+        IpPrefix p1, p2;
+
+        p1 = new IpPrefix("192.0.2.251/23");
+        p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23);
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/23");
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/24");
+        assertAreNotEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.4.5/23");
+        assertAreNotEqual(p1, p2);
+
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122");
+        p2 = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString());
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122");
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123");
+        assertAreNotEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef::/122");
+        assertAreNotEqual(p1, p2);
+
+        // 192.0.2.4/32 != c000:0204::/32.
+        byte[] ipv6bytes = new byte[16];
+        System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length);
+        p1 = new IpPrefix(ipv6bytes, 32);
+        assertAreEqual(p1, new IpPrefix("c000:0204::/32"));
+
+        p2 = new IpPrefix(IPV4_BYTES, 32);
+        assertAreNotEqual(p1, p2);
+    }
+
+    @SmallTest
+    public void testHashCode() {
+        IpPrefix p;
+        int oldCode = -1;
+        Random random = new Random();
+        for (int i = 0; i < 100; i++) {
+            if (random.nextBoolean()) {
+                // IPv4.
+                byte[] b = new byte[4];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(33));
+                assertNotEqual(oldCode, p.hashCode());
+                oldCode = p.hashCode();
+            } else {
+                // IPv6.
+                byte[] b = new byte[16];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(129));
+                assertNotEqual(oldCode, p.hashCode());
+                oldCode = p.hashCode();
+            }
+        }
+    }
+
+    @SmallTest
+    public void testMappedAddressesAreBroken() {
+        // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress,
+        // we are unable to comprehend that.
+        byte[] ipv6bytes = {
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff,
+            (byte) 192, (byte) 0, (byte) 2, (byte) 0};
+        IpPrefix p = new IpPrefix(ipv6bytes, 120);
+        assertEquals(16, p.getRawAddress().length);       // Fine.
+        assertArrayEquals(ipv6bytes, p.getRawAddress());  // Fine.
+
+        // Broken.
+        assertEquals("192.0.2.0/120", p.toString());
+        assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress());
+    }
+
+    public IpPrefix passThroughParcel(IpPrefix p) {
+        Parcel parcel = Parcel.obtain();
+        IpPrefix p2 = null;
+        try {
+            p.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            p2 = IpPrefix.CREATOR.createFromParcel(parcel);
+        } finally {
+            parcel.recycle();
+        }
+        assertNotNull(p2);
+        return p2;
+    }
+
+    public void assertParcelingIsLossless(IpPrefix p) {
+      IpPrefix p2 = passThroughParcel(p);
+      assertEquals(p, p2);
+    }
+
+    public void testParceling() {
+        IpPrefix p;
+
+        p = new IpPrefix("2001:4860:db8::/64");
+        assertParcelingIsLossless(p);
+
+        p = new IpPrefix("192.0.2.0/25");
+        assertParcelingIsLossless(p);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/RouteInfoTest.java b/core/tests/coretests/src/android/net/RouteInfoTest.java
index c80d0bf..af6a32b 100644
--- a/core/tests/coretests/src/android/net/RouteInfoTest.java
+++ b/core/tests/coretests/src/android/net/RouteInfoTest.java
@@ -19,7 +19,7 @@
 import java.lang.reflect.Method;
 import java.net.InetAddress;
 
-import android.net.LinkAddress;
+import android.net.IpPrefix;
 import android.net.RouteInfo;
 import android.os.Parcel;
 
@@ -32,9 +32,8 @@
         return InetAddress.parseNumericAddress(addr);
     }
 
-    private LinkAddress Prefix(String prefix) {
-        String[] parts = prefix.split("/");
-        return new LinkAddress(Address(parts[0]), Integer.parseInt(parts[1]));
+    private IpPrefix Prefix(String prefix) {
+        return new IpPrefix(prefix);
     }
 
     @SmallTest
@@ -43,17 +42,17 @@
 
         // Invalid input.
         try {
-            r = new RouteInfo((LinkAddress) null, null, "rmnet0");
+            r = new RouteInfo((IpPrefix) null, null, "rmnet0");
             fail("Expected RuntimeException:  destination and gateway null");
         } catch(RuntimeException e) {}
 
         // Null destination is default route.
-        r = new RouteInfo((LinkAddress) null, Address("2001:db8::1"), null);
+        r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null);
         assertEquals(Prefix("::/0"), r.getDestination());
         assertEquals(Address("2001:db8::1"), r.getGateway());
         assertNull(r.getInterface());
 
-        r = new RouteInfo((LinkAddress) null, Address("192.0.2.1"), "wlan0");
+        r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0");
         assertEquals(Prefix("0.0.0.0/0"), r.getDestination());
         assertEquals(Address("192.0.2.1"), r.getGateway());
         assertEquals("wlan0", r.getInterface());
@@ -74,7 +73,7 @@
         class PatchedRouteInfo {
             private final RouteInfo mRouteInfo;
 
-            public PatchedRouteInfo(LinkAddress destination, InetAddress gateway, String iface) {
+            public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) {
                 mRouteInfo = new RouteInfo(destination, gateway, iface);
             }
 
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index d26f3fc..7022294 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -41,6 +41,7 @@
 import android.net.ConnectivityManager;
 import android.net.INetworkManagementEventObserver;
 import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.NetworkStats;
 import android.net.NetworkUtils;
@@ -145,6 +146,7 @@
         public static final int InterfaceClassActivity    = 613;
         public static final int InterfaceAddressChange    = 614;
         public static final int InterfaceDnsServerInfo    = 615;
+        public static final int RouteChange               = 616;
     }
 
     static final int DAEMON_MSG_MOBILE_CONN_REAL_TIME_INFO = 1;
@@ -580,6 +582,28 @@
         }
     }
 
+    /**
+     * Notify our observers of a route change.
+     */
+    private void notifyRouteChange(String action, RouteInfo route) {
+        final int length = mObservers.beginBroadcast();
+        try {
+            for (int i = 0; i < length; i++) {
+                try {
+                    if (action.equals("updated")) {
+                        mObservers.getBroadcastItem(i).routeUpdated(route);
+                    } else {
+                        mObservers.getBroadcastItem(i).routeRemoved(route);
+                    }
+                } catch (RemoteException e) {
+                } catch (RuntimeException e) {
+                }
+            }
+        } finally {
+            mObservers.finishBroadcast();
+        }
+    }
+
     //
     // Netd Callback handling
     //
@@ -722,6 +746,47 @@
                     }
                     return true;
                     // break;
+            case NetdResponseCode.RouteChange:
+                    /*
+                     * A route has been updated or removed.
+                     * Format: "NNN Route <updated|removed> <dst> [via <gateway] [dev <iface>]"
+                     */
+                    if (!cooked[1].equals("Route") || cooked.length < 6) {
+                        throw new IllegalStateException(errorMessage);
+                    }
+
+                    String via = null;
+                    String dev = null;
+                    boolean valid = true;
+                    for (int i = 4; (i + 1) < cooked.length && valid; i += 2) {
+                        if (cooked[i].equals("dev")) {
+                            if (dev == null) {
+                                dev = cooked[i+1];
+                            } else {
+                                valid = false;  // Duplicate interface.
+                            }
+                        } else if (cooked[i].equals("via")) {
+                            if (via == null) {
+                                via = cooked[i+1];
+                            } else {
+                                valid = false;  // Duplicate gateway.
+                            }
+                        } else {
+                            valid = false;      // Unknown syntax.
+                        }
+                    }
+                    if (valid) {
+                        try {
+                            // InetAddress.parseNumericAddress(null) inexplicably returns ::1.
+                            InetAddress gateway = null;
+                            if (via != null) gateway = InetAddress.parseNumericAddress(via);
+                            RouteInfo route = new RouteInfo(new IpPrefix(cooked[3]), gateway, dev);
+                            notifyRouteChange(cooked[2], route);
+                            return true;
+                        } catch (IllegalArgumentException e) {}
+                    }
+                    throw new IllegalStateException(errorMessage);
+                    // break;
             default: break;
             }
             return false;