IpConn metrics: correctly read RA lifetimes

This patch
  - adds a Builder class for RaEvent.
  - uses this Builder class for correctly recording the minimum
    lifetime seen for every ICMP6 options tracked, instead of
    recording the last lifetime seen.
  - adds unit test coverage for RaEvent logging.

Change-Id: I6443932f5cf7a613a5c695c65a60eab01e60602a
diff --git a/core/java/android/net/metrics/RaEvent.java b/core/java/android/net/metrics/RaEvent.java
index 69013c0..91bd023 100644
--- a/core/java/android/net/metrics/RaEvent.java
+++ b/core/java/android/net/metrics/RaEvent.java
@@ -27,6 +27,9 @@
 @SystemApi
 public final class RaEvent implements Parcelable {
 
+    /** {@hide} */
+    public static final long NO_LIFETIME = -1L;
+
     // Lifetime in seconds of options found in a single RA packet.
     // When an option is not set, the value of the associated field is -1;
     public final long routerLifetime;
@@ -92,4 +95,60 @@
             return new RaEvent[size];
         }
     };
+
+    /** {@hide} */
+    public static class Builder {
+
+        long routerLifetime          = NO_LIFETIME;
+        long prefixValidLifetime     = NO_LIFETIME;
+        long prefixPreferredLifetime = NO_LIFETIME;
+        long routeInfoLifetime       = NO_LIFETIME;
+        long rdnssLifetime           = NO_LIFETIME;
+        long dnsslLifetime           = NO_LIFETIME;
+
+        public Builder() {
+        }
+
+        public RaEvent build() {
+            return new RaEvent(routerLifetime, prefixValidLifetime, prefixPreferredLifetime,
+                    routeInfoLifetime, rdnssLifetime, dnsslLifetime);
+        }
+
+        public Builder updateRouterLifetime(long lifetime) {
+            routerLifetime = updateLifetime(routerLifetime, lifetime);
+            return this;
+        }
+
+        public Builder updatePrefixValidLifetime(long lifetime) {
+            prefixValidLifetime = updateLifetime(prefixValidLifetime, lifetime);
+            return this;
+        }
+
+        public Builder updatePrefixPreferredLifetime(long lifetime) {
+            prefixPreferredLifetime = updateLifetime(prefixPreferredLifetime, lifetime);
+            return this;
+        }
+
+        public Builder updateRouteInfoLifetime(long lifetime) {
+            routeInfoLifetime = updateLifetime(routeInfoLifetime, lifetime);
+            return this;
+        }
+
+        public Builder updateRdnssLifetime(long lifetime) {
+            rdnssLifetime = updateLifetime(rdnssLifetime, lifetime);
+            return this;
+        }
+
+        public Builder updateDnsslLifetime(long lifetime) {
+            dnsslLifetime = updateLifetime(dnsslLifetime, lifetime);
+            return this;
+        }
+
+        private long updateLifetime(long currentLifetime, long newLifetime) {
+            if (currentLifetime == RaEvent.NO_LIFETIME) {
+                return newLifetime;
+            }
+            return Math.min(currentLifetime, newLifetime);
+        }
+    }
 }
diff --git a/services/net/java/android/net/apf/ApfFilter.java b/services/net/java/android/net/apf/ApfFilter.java
index 0ef9d7a..57f784a 100644
--- a/services/net/java/android/net/apf/ApfFilter.java
+++ b/services/net/java/android/net/apf/ApfFilter.java
@@ -213,7 +213,7 @@
     private final ApfCapabilities mApfCapabilities;
     private final IpManager.Callback mIpManagerCallback;
     private final NetworkInterface mNetworkInterface;
-    private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+    private final IpConnectivityLog mMetricsLog;
     @VisibleForTesting
     byte[] mHardwareAddress;
     @VisibleForTesting
@@ -228,11 +228,12 @@
 
     @VisibleForTesting
     ApfFilter(ApfCapabilities apfCapabilities, NetworkInterface networkInterface,
-            IpManager.Callback ipManagerCallback, boolean multicastFilter) {
+            IpManager.Callback ipManagerCallback, boolean multicastFilter, IpConnectivityLog log) {
         mApfCapabilities = apfCapabilities;
         mIpManagerCallback = ipManagerCallback;
         mNetworkInterface = networkInterface;
         mMulticastFilter = multicastFilter;
+        mMetricsLog = log;
 
         maybeStartFilter();
     }
@@ -371,6 +372,14 @@
             return i & 0xffffffffL;
         }
 
+        private long getUint16(ByteBuffer buffer, int position) {
+            return uint16(buffer.getShort(position));
+        }
+
+        private long getUint32(ByteBuffer buffer, int position) {
+            return uint32(buffer.getInt(position));
+        }
+
         private void prefixOptionToString(StringBuffer sb, int offset) {
             String prefix = IPv6AddresstoString(offset + 16);
             int length = uint8(mPacket.get(offset + 2));
@@ -417,7 +426,7 @@
          * @param lifetimeOffset offset from mPacket.position() to the next lifetime data.
          * @param lifetimeLength length of the next lifetime data.
          * @return offset within packet of where the next binary range of data not including
-         *         a lifetime.  This can be passed into the next invocation of this function
+         *         a lifetime. This can be passed into the next invocation of this function
          *         via {@code lastNonLifetimeStart}.
          */
         private int addNonLifetime(int lastNonLifetimeStart, int lifetimeOffset,
@@ -438,9 +447,9 @@
         // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
         // specifications.
         Ra(byte[] packet, int length) {
-            mPacket = ByteBuffer.allocate(length).put(ByteBuffer.wrap(packet, 0, length));
-            mPacket.clear();
+            mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
             mLastSeen = curTime();
+            RaEvent.Builder builder = new RaEvent.Builder();
 
             // Ignore the checksum.
             int lastNonLifetimeStart = addNonLifetime(0,
@@ -451,14 +460,7 @@
             lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
                     ICMP6_RA_ROUTER_LIFETIME_OFFSET,
                     ICMP6_RA_ROUTER_LIFETIME_LEN);
-
-            long routerLifetime = uint16(mPacket.getShort(
-                    ICMP6_RA_ROUTER_LIFETIME_OFFSET + mPacket.position()));
-            long prefixValidLifetime = -1L;
-            long prefixPreferredLifetime = -1L;
-            long routeInfoLifetime = -1L;
-            long dnsslLifetime = - 1L;
-            long rdnssLifetime = -1L;
+            builder.updateRouterLifetime(getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET));
 
             // Ensures that the RA is not truncated.
             mPacket.position(ICMP6_RA_OPTION_OFFSET);
@@ -466,39 +468,42 @@
                 final int position = mPacket.position();
                 final int optionType = uint8(mPacket.get(position));
                 final int optionLength = uint8(mPacket.get(position + 1)) * 8;
+                long lifetime;
                 switch (optionType) {
                     case ICMP6_PREFIX_OPTION_TYPE:
                         // Parse valid lifetime
                         lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
                                 ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET,
                                 ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN);
+                        lifetime = getUint32(mPacket,
+                                position + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
+                        builder.updatePrefixValidLifetime(lifetime);
                         // Parse preferred lifetime
                         lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
                                 ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
                                 ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
+                        lifetime = getUint32(mPacket,
+                                position + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET);
+                        builder.updatePrefixPreferredLifetime(lifetime);
                         mPrefixOptionOffsets.add(position);
-                        prefixValidLifetime = uint32(mPacket.getInt(
-                                ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET + position));
-                        prefixPreferredLifetime = uint32(mPacket.getInt(
-                                ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET + position));
                         break;
                     // These three options have the same lifetime offset and size, and
-                    // are processed with the same specialized addNonLifetime4B:
+                    // are processed with the same specialized addNonLifetimeU32:
                     case ICMP6_RDNSS_OPTION_TYPE:
                         mRdnssOptionOffsets.add(position);
                         lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
-                        rdnssLifetime =
-                                uint32(mPacket.getInt(ICMP6_4_BYTE_LIFETIME_OFFSET + position));
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateRdnssLifetime(lifetime);
                         break;
                     case ICMP6_ROUTE_INFO_OPTION_TYPE:
                         lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
-                        routeInfoLifetime =
-                                uint32(mPacket.getInt(ICMP6_4_BYTE_LIFETIME_OFFSET + position));
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateRouteInfoLifetime(lifetime);
                         break;
                     case ICMP6_DNSSL_OPTION_TYPE:
                         lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
-                        dnsslLifetime =
-                                uint32(mPacket.getInt(ICMP6_4_BYTE_LIFETIME_OFFSET + position));
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateDnsslLifetime(lifetime);
                         break;
                     default:
                         // RFC4861 section 4.2 dictates we ignore unknown options for fowards
@@ -514,9 +519,7 @@
             // Mark non-lifetime bytes since last lifetime.
             addNonLifetime(lastNonLifetimeStart, 0, 0);
             mMinLifetime = minLifetime(packet, length);
-            // TODO: record per-option minimum lifetimes instead of last seen lifetimes
-            mMetricsLog.log(new RaEvent(routerLifetime, prefixValidLifetime,
-                    prefixPreferredLifetime, routeInfoLifetime, rdnssLifetime, dnsslLifetime));
+            mMetricsLog.log(builder.build());
         }
 
         // Ignoring lifetimes (which may change) does {@code packet} match this RA?
@@ -1000,7 +1003,8 @@
             Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
             return null;
         }
-        return new ApfFilter(apfCapabilities, networkInterface, ipManagerCallback, multicastFilter);
+        return new ApfFilter(apfCapabilities, networkInterface, ipManagerCallback,
+                multicastFilter, new IpConnectivityLog());
     }
 
     public synchronized void shutdown() {
diff --git a/services/tests/servicestests/src/android/net/apf/ApfTest.java b/services/tests/servicestests/src/android/net/apf/ApfTest.java
index af78839..815133a 100644
--- a/services/tests/servicestests/src/android/net/apf/ApfTest.java
+++ b/services/tests/servicestests/src/android/net/apf/ApfTest.java
@@ -26,14 +26,23 @@
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IpManager;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.RaEvent;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.os.ConditionVariable;
+import android.os.Parcelable;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
@@ -43,6 +52,7 @@
 import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.nio.ByteBuffer;
+import java.util.List;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -56,9 +66,12 @@
 public class ApfTest extends AndroidTestCase {
     private static final int TIMEOUT_MS = 500;
 
+    @Mock IpConnectivityLog mLog;
+
     @Override
     public void setUp() throws Exception {
         super.setUp();
+        MockitoAnnotations.initMocks(this);
         // Load up native shared library containing APF interpreter exposed via JNI.
         System.loadLibrary("servicestestsjni");
     }
@@ -70,6 +83,9 @@
     // least the minimum packet size.
     private final static int MIN_PKT_SIZE = 15;
 
+    private final static boolean DROP_MULTICAST = true;
+    private final static boolean ALLOW_MULTICAST = false;
+
     private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) {
         assertEquals(expected, apfSimulate(program, packet, filterAge));
     }
@@ -562,10 +578,10 @@
         public final static byte[] MOCK_MAC_ADDR = new byte[]{1,2,3,4,5,6};
         private FileDescriptor mWriteSocket;
 
-        public TestApfFilter(IpManager.Callback ipManagerCallback, boolean multicastFilter) throws
-                Exception {
+        public TestApfFilter(IpManager.Callback ipManagerCallback, boolean multicastFilter,
+                IpConnectivityLog log) throws Exception {
             super(new ApfCapabilities(2, 1000, ARPHRD_ETHER), NetworkInterface.getByName("lo"),
-                    ipManagerCallback, multicastFilter);
+                    ipManagerCallback, multicastFilter, log);
         }
 
         // Pretend an RA packet has been received and show it to ApfFilter.
@@ -667,7 +683,7 @@
     @LargeTest
     public void testApfFilterIPv4() throws Exception {
         MockIpManagerCallback ipManagerCallback = new MockIpManagerCallback();
-        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, true /* multicastFilter */);
+        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, DROP_MULTICAST, mLog);
         byte[] program = ipManagerCallback.getApfProgram();
 
         // Verify empty packet of 100 zero bytes is passed
@@ -699,7 +715,7 @@
     @LargeTest
     public void testApfFilterIPv6() throws Exception {
         MockIpManagerCallback ipManagerCallback = new MockIpManagerCallback();
-        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, false /* multicastFilter */);
+        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, ALLOW_MULTICAST, mLog);
         byte[] program = ipManagerCallback.getApfProgram();
 
         // Verify empty IPv6 packet is passed
@@ -726,7 +742,7 @@
     @LargeTest
     public void testApfFilterMulticast() throws Exception {
         MockIpManagerCallback ipManagerCallback = new MockIpManagerCallback();
-        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, false /* multicastFilter */);
+        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, ALLOW_MULTICAST, mLog);
         byte[] program = ipManagerCallback.getApfProgram();
 
         // Construct IPv4 and IPv6 multicast packets.
@@ -772,7 +788,7 @@
         // Verify it can be initialized to on
         ipManagerCallback.resetApfProgramWait();
         apfFilter.shutdown();
-        apfFilter = new TestApfFilter(ipManagerCallback, true /* multicastFilter */);
+        apfFilter = new TestApfFilter(ipManagerCallback, DROP_MULTICAST, mLog);
         program = ipManagerCallback.getApfProgram();
         assertDrop(program, bcastv4packet.array(), 0);
         assertDrop(program, mcastv4packet.array(), 0);
@@ -804,7 +820,7 @@
     @LargeTest
     public void testApfFilterArp() throws Exception {
         MockIpManagerCallback ipManagerCallback = new MockIpManagerCallback();
-        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, false /* multicastFilter */);
+        ApfFilter apfFilter = new TestApfFilter(ipManagerCallback, ALLOW_MULTICAST, mLog);
         byte[] program = ipManagerCallback.getApfProgram();
 
         // Verify initially ARP filter is off
@@ -867,6 +883,35 @@
         verifyRaLifetime(ipManagerCallback, packet, lifetime);
     }
 
+    private void verifyRaEvent(RaEvent expected) {
+        ArgumentCaptor<Parcelable> captor = ArgumentCaptor.forClass(Parcelable.class);
+        verify(mLog, atLeastOnce()).log(captor.capture());
+        RaEvent got = lastRaEvent(captor.getAllValues());
+        if (!raEventEquals(expected, got)) {
+            assertEquals(expected, got);  // fail for printing an assertion error message.
+        }
+    }
+
+    private RaEvent lastRaEvent(List<Parcelable> events) {
+        RaEvent got = null;
+        for (Parcelable ev : events) {
+            if (ev instanceof RaEvent) {
+                got = (RaEvent) ev;
+            }
+        }
+        return got;
+    }
+
+    private boolean raEventEquals(RaEvent ev1, RaEvent ev2) {
+        return (ev1 != null) && (ev2 != null)
+                && (ev1.routerLifetime == ev2.routerLifetime)
+                && (ev1.prefixValidLifetime == ev2.prefixValidLifetime)
+                && (ev1.prefixPreferredLifetime == ev2.prefixPreferredLifetime)
+                && (ev1.routeInfoLifetime == ev2.routeInfoLifetime)
+                && (ev1.rdnssLifetime == ev2.rdnssLifetime)
+                && (ev1.dnsslLifetime == ev2.dnsslLifetime);
+    }
+
     private void assertInvalidRa(TestApfFilter apfFilter, MockIpManagerCallback ipManagerCallback,
             ByteBuffer packet) throws IOException, ErrnoException {
         ipManagerCallback.resetApfProgramWait();
@@ -877,7 +922,7 @@
     @LargeTest
     public void testApfFilterRa() throws Exception {
         MockIpManagerCallback ipManagerCallback = new MockIpManagerCallback();
-        TestApfFilter apfFilter = new TestApfFilter(ipManagerCallback, true /* multicastFilter */);
+        TestApfFilter apfFilter = new TestApfFilter(ipManagerCallback, DROP_MULTICAST, mLog);
         byte[] program = ipManagerCallback.getApfProgram();
 
         // Verify RA is passed the first time
@@ -891,6 +936,7 @@
         assertPass(program, basePacket.array(), 0);
 
         testRaLifetime(apfFilter, ipManagerCallback, basePacket, 1000);
+        verifyRaEvent(new RaEvent(1000, -1, -1, -1, -1, -1));
 
         // Ensure zero-length options cause the packet to be silently skipped.
         // Do this before we test other packets. http://b/29586253
@@ -916,6 +962,7 @@
         prefixOptionPacket.putInt(
                 ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET, 200);
         testRaLifetime(apfFilter, ipManagerCallback, prefixOptionPacket, 100);
+        verifyRaEvent(new RaEvent(1000, 200, 100, -1, -1, -1));
 
         ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(
                 new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
@@ -926,6 +973,7 @@
         rdnssOptionPacket.putInt(
                 ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, 300);
         testRaLifetime(apfFilter, ipManagerCallback, rdnssOptionPacket, 300);
+        verifyRaEvent(new RaEvent(1000, -1, -1, -1, 300, -1));
 
         ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(
                 new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
@@ -936,6 +984,7 @@
         routeInfoOptionPacket.putInt(
                 ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, 400);
         testRaLifetime(apfFilter, ipManagerCallback, routeInfoOptionPacket, 400);
+        verifyRaEvent(new RaEvent(1000, -1, -1, 400, -1, -1));
 
         ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(
                 new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
@@ -948,6 +997,7 @@
         // Note that lifetime of 2000 will be ignored in favor of shorter
         // route lifetime of 1000.
         testRaLifetime(apfFilter, ipManagerCallback, dnsslOptionPacket, 1000);
+        verifyRaEvent(new RaEvent(1000, -1, -1, -1, -1, 2000));
 
         // Verify that current program filters all five RAs:
         verifyRaLifetime(ipManagerCallback, basePacket, 1000);