Merge changes from topic 'network_specifier'

* changes:
  [CM] Unhide the NetworkSpecifier as object API
  Make the NetworkSpecifier a class instead of a string.
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index e163365..2a985e7 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -88,6 +88,13 @@
      * sent as an extra; it should be consulted to see what kind of
      * connectivity event occurred.
      * <p/>
+     * Apps targeting Android 7.0 (API level 24) and higher do not receive this
+     * broadcast if they declare the broadcast receiver in their manifest. Apps
+     * will still receive broadcasts if they register their
+     * {@link android.content.BroadcastReceiver} with
+     * {@link android.content.Context#registerReceiver Context.registerReceiver()}
+     * and that context is still valid.
+     * <p/>
      * If this is a connection that was the result of failing over from a
      * disconnected network, then the FAILOVER_CONNECTION boolean extra is
      * set to true.
@@ -3400,6 +3407,75 @@
     }
 
     /**
+     * It is acceptable to briefly use multipath data to provide seamless connectivity for
+     * time-sensitive user-facing operations when the system default network is temporarily
+     * unresponsive. The amount of data should be limited (less than one megabyte), and the
+     * operation should be infrequent to ensure that data usage is limited.
+     *
+     * An example of such an operation might be a time-sensitive foreground activity, such as a
+     * voice command, that the user is performing while walking out of range of a Wi-Fi network.
+     */
+    public static final int MULTIPATH_PREFERENCE_HANDOVER = 1 << 0;
+
+    /**
+     * It is acceptable to use small amounts of multipath data on an ongoing basis to provide
+     * a backup channel for traffic that is primarily going over another network.
+     *
+     * An example might be maintaining backup connections to peers or servers for the purpose of
+     * fast fallback if the default network is temporarily unresponsive or disconnects. The traffic
+     * on backup paths should be negligible compared to the traffic on the main path.
+     */
+    public static final int MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1;
+
+    /**
+     * It is acceptable to use metered data to improve network latency and performance.
+     */
+    public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2;
+
+    /**
+     * Return value to use for unmetered networks. On such networks we currently set all the flags
+     * to true.
+     * @hide
+     */
+    public static final int MULTIPATH_PREFERENCE_UNMETERED =
+            MULTIPATH_PREFERENCE_HANDOVER |
+            MULTIPATH_PREFERENCE_RELIABILITY |
+            MULTIPATH_PREFERENCE_PERFORMANCE;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            MULTIPATH_PREFERENCE_HANDOVER,
+            MULTIPATH_PREFERENCE_RELIABILITY,
+            MULTIPATH_PREFERENCE_PERFORMANCE,
+    })
+    public @interface MultipathPreference {
+    }
+
+    /**
+     * Provides a hint to the calling application on whether it is desirable to use the
+     * multinetwork APIs (e.g., {@link Network#openConnection}, {@link Network#bindSocket}, etc.)
+     * for multipath data transfer on this network when it is not the system default network.
+     * Applications desiring to use multipath network protocols should call this method before
+     * each such operation.
+     * <p>
+     * This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     *
+     * @param network The network on which the application desires to use multipath data.
+     *                If {@code null}, this method will return the a preference that will generally
+     *                apply to metered networks.
+     * @return a bitwise OR of zero or more of the  {@code MULTIPATH_PREFERENCE_*} constants.
+     */
+    public @MultipathPreference int getMultipathPreference(Network network) {
+        try {
+            return mService.getMultipathPreference(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Resets all connectivity manager settings back to factory defaults.
      * @hide
      */
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index b123c28..425e494 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -161,6 +161,8 @@
     void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
     void setAvoidUnvalidated(in Network network);
 
+    int getMultipathPreference(in Network Network);
+
     int getRestoreDefaultNetworkDelay(int networkType);
 
     boolean addVpnAddress(String address, int prefixLength);
diff --git a/core/tests/coretests/src/android/net/IpPrefixTest.java b/core/tests/coretests/src/android/net/IpPrefixTest.java
index 4f2387d..fcc6389 100644
--- a/core/tests/coretests/src/android/net/IpPrefixTest.java
+++ b/core/tests/coretests/src/android/net/IpPrefixTest.java
@@ -18,14 +18,14 @@
 
 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;
 
-import static android.test.MoreAsserts.assertNotEqual;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 
 public class IpPrefixTest extends TestCase {
 
@@ -242,42 +242,25 @@
 
     @SmallTest
     public void testHashCode() {
-        IpPrefix p = new IpPrefix(new byte[4], 0);
+        IpPrefix p;
+        int oldCode = -1;
         Random random = new Random();
         for (int i = 0; i < 100; i++) {
-            final IpPrefix oldP = p;
             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();
             }
-            if (p.equals(oldP)) {
-              assertEquals(p.hashCode(), oldP.hashCode());
-            }
-            if (p.hashCode() != oldP.hashCode()) {
-              assertNotEqual(p, oldP);
-            }
-        }
-    }
-
-    @SmallTest
-    public void testHashCodeIsNotConstant() {
-        IpPrefix[] prefixes = {
-            new IpPrefix("2001:db8:f00::ace:d00d/127"),
-            new IpPrefix("192.0.2.0/23"),
-            new IpPrefix("::/0"),
-            new IpPrefix("0.0.0.0/0"),
-        };
-        for (int i = 0; i < prefixes.length; i++) {
-          for (int j = i + 1; j < prefixes.length; j++) {
-            assertNotEqual(prefixes[i].hashCode(), prefixes[j].hashCode());
-          }
         }
     }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 079b670..4f8ff1d 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1304,13 +1304,16 @@
     @Override
     public LinkProperties getLinkProperties(Network network) {
         enforceAccessPermission();
-        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
-        if (nai != null) {
-            synchronized (nai) {
-                return new LinkProperties(nai.linkProperties);
-            }
+        return getLinkProperties(getNetworkAgentInfoForNetwork(network));
+    }
+
+    private LinkProperties getLinkProperties(NetworkAgentInfo nai) {
+        if (nai == null) {
+            return null;
         }
-        return null;
+        synchronized (nai) {
+            return new LinkProperties(nai.linkProperties);
+        }
     }
 
     private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) {
@@ -2836,6 +2839,18 @@
         }
     }
 
+    @Override
+    public int getMultipathPreference(Network network) {
+        enforceAccessPermission();
+
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai != null && !nai.networkInfo.isMetered()) {
+            return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+        }
+
+        return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
+    }
+
     private class InternalHandler extends Handler {
         public InternalHandler(Looper looper) {
             super(looper);
@@ -3134,7 +3149,8 @@
         enforceAccessPermission();
         enforceInternetPermission();
 
-        NetworkAgentInfo nai;
+        // TODO: execute this logic on ConnectivityService handler.
+        final NetworkAgentInfo nai;
         if (network == null) {
             nai = getDefaultNetwork();
         } else {
@@ -3145,21 +3161,24 @@
             return;
         }
         // Revalidate if the app report does not match our current validated state.
-        if (hasConnectivity == nai.lastValidated) return;
+        if (hasConnectivity == nai.lastValidated) {
+            return;
+        }
         final int uid = Binder.getCallingUid();
         if (DBG) {
             log("reportNetworkConnectivity(" + nai.network.netId + ", " + hasConnectivity +
                     ") by " + uid);
         }
-        synchronized (nai) {
-            // Validating a network that has not yet connected could result in a call to
-            // rematchNetworkAndRequests() which is not meant to work on such networks.
-            if (!nai.everConnected) return;
-
-            if (isNetworkWithLinkPropertiesBlocked(nai.linkProperties, uid, false)) return;
-
-            nai.networkMonitor.sendMessage(NetworkMonitor.CMD_FORCE_REEVALUATION, uid);
+        // Validating a network that has not yet connected could result in a call to
+        // rematchNetworkAndRequests() which is not meant to work on such networks.
+        if (!nai.everConnected) {
+            return;
         }
+        LinkProperties lp = getLinkProperties(nai);
+        if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
+            return;
+        }
+        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_FORCE_REEVALUATION, uid);
     }
 
     private ProxyInfo getDefaultProxy() {
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 9ffa40b..4ff6657 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -19,7 +19,6 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.widget.Toast;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -27,18 +26,40 @@
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
-import com.android.internal.annotations.VisibleForTesting;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.widget.Toast;
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsProto.MetricsEvent;
 
-import static android.net.NetworkCapabilities.*;
-
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 public class NetworkNotificationManager {
 
-    public static enum NotificationType { SIGN_IN, NO_INTERNET, LOST_INTERNET, NETWORK_SWITCH };
+    public static enum NotificationType {
+        LOST_INTERNET(MetricsEvent.NOTIFICATION_NETWORK_LOST_INTERNET),
+        NETWORK_SWITCH(MetricsEvent.NOTIFICATION_NETWORK_SWITCH),
+        NO_INTERNET(MetricsEvent.NOTIFICATION_NETWORK_NO_INTERNET),
+        SIGN_IN(MetricsEvent.NOTIFICATION_NETWORK_SIGN_IN);
 
-    @VisibleForTesting
-    static final String NOTIFICATION_ID = "Connectivity.Notification";
+        public final int eventId;
+
+        NotificationType(int eventId) {
+            this.eventId = eventId;
+            Holder.sIdToTypeMap.put(eventId, this);
+        }
+
+        private static class Holder {
+            private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
+        }
+
+        public static NotificationType getFromId(int id) {
+            return Holder.sIdToTypeMap.get(id);
+        }
+    };
 
     private static final String TAG = NetworkNotificationManager.class.getSimpleName();
     private static final boolean DBG = true;
@@ -47,11 +68,14 @@
     private final Context mContext;
     private final TelephonyManager mTelephonyManager;
     private final NotificationManager mNotificationManager;
+    // Tracks the types of notifications managed by this instance, from creation to cancellation.
+    private final SparseIntArray mNotificationTypeMap;
 
     public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
         mContext = c;
         mTelephonyManager = t;
         mNotificationManager = n;
+        mNotificationTypeMap = new SparseIntArray();
     }
 
     // TODO: deal more gracefully with multi-transport networks.
@@ -101,8 +125,10 @@
      */
     public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
             NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
-        int transportType;
-        String extraInfo;
+        final String tag = tagFor(id);
+        final int eventId = notifyType.eventId;
+        final int transportType;
+        final String extraInfo;
         if (nai != null) {
             transportType = getFirstTransportType(nai);
             extraInfo = nai.networkInfo.getExtraInfo();
@@ -115,9 +141,10 @@
         }
 
         if (DBG) {
-            Slog.d(TAG, "showNotification id=" + id + " " + notifyType
-                    + " transportType=" + getTransportName(transportType)
-                    + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
+            Slog.d(TAG, String.format(
+                    "showNotification tag=%s event=%s transport=%s extraInfo=%s highPrioriy=%s",
+                    tag, nameOf(eventId), getTransportName(transportType), extraInfo,
+                    highPriority));
         }
 
         Resources r = Resources.getSystem();
@@ -155,7 +182,7 @@
             details = r.getString(R.string.network_switch_metered_detail, toTransport,
                     fromTransport);
         } else {
-            Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport "
+            Slog.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
                     + getTransportName(transportType));
             return;
         }
@@ -185,22 +212,31 @@
 
         Notification notification = builder.build();
 
+        mNotificationTypeMap.put(id, eventId);
         try {
-            mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
+            mNotificationManager.notifyAsUser(tag, eventId, notification, UserHandle.ALL);
         } catch (NullPointerException npe) {
             Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
         }
     }
 
     public void clearNotification(int id) {
+        if (mNotificationTypeMap.indexOfKey(id) < 0) {
+            return;
+        }
+        final String tag = tagFor(id);
+        final int eventId = mNotificationTypeMap.get(id);
         if (DBG) {
-            Slog.d(TAG, "clearNotification id=" + id);
+            Slog.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
+                   nameOf(eventId)));
         }
         try {
-            mNotificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
+            mNotificationManager.cancelAsUser(tag, eventId, UserHandle.ALL);
         } catch (NullPointerException npe) {
-            Slog.d(TAG, "setNotificationVisible: cancel notificationManager error", npe);
+            Slog.d(TAG, String.format(
+                    "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
         }
+        mNotificationTypeMap.delete(id);
     }
 
     /**
@@ -223,4 +259,15 @@
                 R.string.network_switch_metered_toast, fromTransport, toTransport);
         Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
     }
+
+    @VisibleForTesting
+    static String tagFor(int id) {
+        return String.format("ConnectivityNotification:%d", id);
+    }
+
+    @VisibleForTesting
+    static String nameOf(int eventId) {
+        NotificationType t = NotificationType.getFromId(eventId);
+        return (t != null) ? t.name() : "UNKNOWN";
+    }
 }
diff --git a/services/net/java/android/net/util/MultinetworkPolicyTracker.java b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
index ebd131b..424e40d 100644
--- a/services/net/java/android/net/util/MultinetworkPolicyTracker.java
+++ b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.database.ContentObserver;
+import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Message;
@@ -29,10 +30,14 @@
 import android.provider.Settings;
 import android.util.Slog;
 
+import java.util.Arrays;
+import java.util.List;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.R;
 
 import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
 
 /**
  * A class to encapsulate management of the "Smart Networking" capability of
@@ -57,12 +62,13 @@
     private final Context mContext;
     private final Handler mHandler;
     private final Runnable mReevaluateRunnable;
-    private final Uri mAvoidBadWifiUri;
+    private final List<Uri> mSettingsUris;
     private final ContentResolver mResolver;
     private final SettingObserver mSettingObserver;
     private final BroadcastReceiver mBroadcastReceiver;
 
     private volatile boolean mAvoidBadWifi = true;
+    private volatile int mMeteredMultipathPreference;
 
     public MultinetworkPolicyTracker(Context ctx, Handler handler) {
         this(ctx, handler, null);
@@ -72,9 +78,14 @@
         mContext = ctx;
         mHandler = handler;
         mReevaluateRunnable = () -> {
-            if (updateAvoidBadWifi() && avoidBadWifiCallback != null) avoidBadWifiCallback.run();
+            if (updateAvoidBadWifi() && avoidBadWifiCallback != null) {
+                avoidBadWifiCallback.run();
+            }
+            updateMeteredMultipathPreference();
         };
-        mAvoidBadWifiUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
+        mSettingsUris = Arrays.asList(
+            Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI),
+            Settings.Global.getUriFor(NETWORK_METERED_MULTIPATH_PREFERENCE));
         mResolver = mContext.getContentResolver();
         mSettingObserver = new SettingObserver();
         mBroadcastReceiver = new BroadcastReceiver() {
@@ -85,10 +96,13 @@
         };
 
         updateAvoidBadWifi();
+        updateMeteredMultipathPreference();
     }
 
     public void start() {
-        mResolver.registerContentObserver(mAvoidBadWifiUri, false, mSettingObserver);
+        for (Uri uri : mSettingsUris) {
+            mResolver.registerContentObserver(uri, false, mSettingObserver);
+        }
 
         final IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
@@ -108,6 +122,10 @@
         return mAvoidBadWifi;
     }
 
+    public int getMeteredMultipathPreference() {
+        return mMeteredMultipathPreference;
+    }
+
     /**
      * Whether the device or carrier configuration disables avoiding bad wifi by default.
      */
@@ -138,6 +156,23 @@
         return mAvoidBadWifi != prev;
     }
 
+    /**
+     * The default (device and carrier-dependent) value for metered multipath preference.
+     */
+    public int configMeteredMultipathPreference() {
+        return mContext.getResources().getInteger(
+                R.integer.config_networkMeteredMultipathPreference);
+    }
+
+    public void updateMeteredMultipathPreference() {
+        String setting = Settings.Global.getString(mResolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+        try {
+            mMeteredMultipathPreference = Integer.parseInt(setting);
+        } catch (NumberFormatException e) {
+            mMeteredMultipathPreference = configMeteredMultipathPreference();
+        }
+    }
+
     private class SettingObserver extends ContentObserver {
         public SettingObserver() {
             super(null);
@@ -150,7 +185,9 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
-            if (!mAvoidBadWifiUri.equals(uri)) return;
+            if (!mSettingsUris.contains(uri)) {
+                Slog.wtf(TAG, "Unexpected settings observation: " + uri);
+            }
             reevaluate();
         }
     }
diff --git a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java
similarity index 89%
rename from tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
rename to services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 813e928..21c2de7 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -48,8 +48,6 @@
 
 public class NetworkNotificationManagerTest extends TestCase {
 
-    static final String NOTIFICATION_ID = NetworkNotificationManager.NOTIFICATION_ID;
-
     static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
     static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
     static {
@@ -82,6 +80,7 @@
         when(mCtx.getResources()).thenReturn(mResources);
         when(mCtx.getPackageManager()).thenReturn(mPm);
         when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        when(mNetworkInfo.getExtraInfo()).thenReturn("extra");
         when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
 
         mManager = new NetworkNotificationManager(mCtx, mTelephonyManager, mNotificationManager);
@@ -108,11 +107,11 @@
         }
 
         for (int i = 0; i < ids.size(); i++) {
-            final int expectedId = NETWORK_ID_BASE + i;
-            verify(mNotificationManager, times(1))
-                    .notifyAsUser(eq(NOTIFICATION_ID), eq(expectedId), any(), any());
-            verify(mNotificationManager, times(1))
-                    .cancelAsUser(eq(NOTIFICATION_ID), eq(expectedId), any());
+            final int id = ids.get(i);
+            final int eventId = types.get(i).eventId;
+            final String tag = NetworkNotificationManager.tagFor(id);
+            verify(mNotificationManager, times(1)).notifyAsUser(eq(tag), eq(eventId), any(), any());
+            verify(mNotificationManager, times(1)).cancelAsUser(eq(tag), eq(eventId), any());
         }
     }
 
@@ -125,8 +124,9 @@
 
         mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
 
-        verify(mNotificationManager, times(1))
-                .notifyAsUser(eq(NOTIFICATION_ID), eq(102), any(), any());
+        final int eventId = NO_INTERNET.eventId;
+        final String tag = NetworkNotificationManager.tagFor(102);
+        verify(mNotificationManager, times(1)).notifyAsUser(eq(tag), eq(eventId), any(), any());
     }
 
     @SmallTest
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 51c8907..1be8d5e 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -638,6 +638,7 @@
 
     private class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker {
         public volatile boolean configRestrictsAvoidBadWifi;
+        public volatile int configMeteredMultipathPreference;
 
         public WrappedMultinetworkPolicyTracker(Context c, Handler h, Runnable r) {
             super(c, h, r);
@@ -647,6 +648,11 @@
         public boolean configRestrictsAvoidBadWifi() {
             return configRestrictsAvoidBadWifi;
         }
+
+        @Override
+        public int configMeteredMultipathPreference() {
+            return configMeteredMultipathPreference;
+        }
     }
 
     private class WrappedConnectivityService extends ConnectivityService {
@@ -2496,6 +2502,26 @@
         mCm.unregisterNetworkCallback(defaultCallback);
     }
 
+    @SmallTest
+    public void testMeteredMultipathPreferenceSetting() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final WrappedMultinetworkPolicyTracker tracker = mService.getMultinetworkPolicyTracker();
+        final String settingName = Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+
+        for (int config : Arrays.asList(0, 3, 2)) {
+            for (String setting: Arrays.asList(null, "0", "2", "1")) {
+                tracker.configMeteredMultipathPreference = config;
+                Settings.Global.putString(cr, settingName, setting);
+                tracker.reevaluate();
+                mService.waitForIdle();
+
+                final int expected = (setting != null) ? Integer.parseInt(setting) : config;
+                String msg = String.format("config=%d, setting=%s", config, setting);
+                assertEquals(msg, expected, mCm.getMultipathPreference(null));
+            }
+        }
+    }
+
     /**
      * Validate that a satisfied network request does not trigger onUnavailable() once the
      * time-out period expires.
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 84f0f90..8c16dbb 100644
--- a/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -16,6 +16,17 @@
 
 package com.android.server.connectivity;
 
+import static com.android.server.connectivity.MetricsTestUtil.aBool;
+import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
+import static com.android.server.connectivity.MetricsTestUtil.aLong;
+import static com.android.server.connectivity.MetricsTestUtil.aString;
+import static com.android.server.connectivity.MetricsTestUtil.aType;
+import static com.android.server.connectivity.MetricsTestUtil.anInt;
+import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
+import static com.android.server.connectivity.MetricsTestUtil.b;
+import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
+import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
+
 import android.net.ConnectivityMetricsEvent;
 import android.net.metrics.ApfProgramEvent;
 import android.net.metrics.ApfStats;
@@ -28,24 +39,13 @@
 import android.net.metrics.NetworkEvent;
 import android.net.metrics.RaEvent;
 import android.net.metrics.ValidationProbeEvent;
-import com.google.protobuf.nano.MessageNano;
+import android.test.suitebuilder.annotation.SmallTest;
 import java.util.Arrays;
 import junit.framework.TestCase;
 
-import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
-import static com.android.server.connectivity.MetricsTestUtil.aBool;
-import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
-import static com.android.server.connectivity.MetricsTestUtil.aLong;
-import static com.android.server.connectivity.MetricsTestUtil.aString;
-import static com.android.server.connectivity.MetricsTestUtil.aType;
-import static com.android.server.connectivity.MetricsTestUtil.anInt;
-import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
-import static com.android.server.connectivity.MetricsTestUtil.b;
-import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
-import static com.android.server.connectivity.MetricsTestUtil.ipEv;
-
 public class IpConnectivityEventBuilderTest extends TestCase {
 
+    @SmallTest
     public void testDefaultNetworkEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DefaultNetworkEvent.class),
@@ -71,12 +71,14 @@
                 "    transport_types: 3",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDhcpClientEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DhcpClientEvent.class),
@@ -94,12 +96,14 @@
                 "    state_transition: \"SomeState\"",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDhcpErrorEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DhcpErrorEvent.class),
@@ -116,12 +120,14 @@
                 "    state_transition: \"\"",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDnsEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DnsEvent.class),
@@ -163,12 +169,14 @@
                 "    return_codes: 178",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testIpManagerEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(IpManagerEvent.class),
@@ -185,12 +193,14 @@
                 "    latency_ms: 5678",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testIpReachabilityEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(IpReachabilityEvent.class),
@@ -205,12 +215,14 @@
                 "    if_name: \"wlan0\"",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testNetworkEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(NetworkEvent.class),
@@ -229,12 +241,14 @@
                 "    >",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testValidationProbeEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ValidationProbeEvent.class),
@@ -247,6 +261,7 @@
                 "dropped_events: 0",
                 "events <",
                 "  time_ms: 1",
+                "  transport: 0",
                 "  validation_probe_event <",
                 "    latency_ms: 40730",
                 "    network_id <",
@@ -261,6 +276,7 @@
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testApfProgramEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ApfProgramEvent.class),
@@ -282,12 +298,14 @@
                 "    program_length: 2048",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testApfStatsSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ApfStats.class),
@@ -314,12 +332,14 @@
                 "    zero_lifetime_ras: 1",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testRaEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(RaEvent.class),
@@ -342,6 +362,7 @@
                 "    router_lifetime: 2000",
                 "  >",
                 "  time_ms: 1",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
@@ -350,9 +371,9 @@
 
     static void verifySerialization(String want, ConnectivityMetricsEvent... input) {
         try {
-            byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input));
-            IpConnectivityLog log = new IpConnectivityLog();
-            MessageNano.mergeFrom(log, got);
+            byte[] got = IpConnectivityEventBuilder.serialize(0,
+                    IpConnectivityEventBuilder.toProto(Arrays.asList(input)));
+            IpConnectivityLog log = IpConnectivityLog.parseFrom(got);
             assertEquals(want, log.toString());
         } catch (Exception e) {
             fail(e.toString());
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index aa491bb..9a33cde 100644
--- a/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.connectivity;
 
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
 import android.content.Context;
 import android.net.ConnectivityMetricsEvent;
 import android.net.IIpConnectivityMetrics;
@@ -29,9 +32,9 @@
 import android.net.metrics.RaEvent;
 import android.net.metrics.ValidationProbeEvent;
 import android.os.Parcelable;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
 import com.android.server.connectivity.metrics.IpConnectivityLogClass;
-import com.google.protobuf.nano.MessageNano;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Collections;
@@ -43,10 +46,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
 public class IpConnectivityMetricsTest extends TestCase {
     static final IpReachabilityEvent FAKE_EV =
             new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED);
@@ -61,6 +60,7 @@
         mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000);
     }
 
+    @SmallTest
     public void testLoggingEvents() throws Exception {
         IpConnectivityLog logger = new IpConnectivityLog(mMockService);
 
@@ -74,6 +74,7 @@
         assertEventsEqual(expectedEvent(3), got.get(2));
     }
 
+    @SmallTest
     public void testLoggingEventsWithMultipleCallers() throws Exception {
         IpConnectivityLog logger = new IpConnectivityLog(mMockService);
 
@@ -101,6 +102,7 @@
         }
     }
 
+    @SmallTest
     public void testBufferFlushing() {
         String output1 = getdump("flush");
         assertEquals("", output1);
@@ -113,6 +115,7 @@
         assertEquals("", output3);
     }
 
+    @SmallTest
     public void testRateLimiting() {
         final IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
         final ApfProgramEvent ev = new ApfProgramEvent(0, 0, 0, 0, 0);
@@ -134,6 +137,7 @@
         assertEquals("", output2);
     }
 
+    @SmallTest
     public void testEndToEndLogging() {
         IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
 
@@ -159,6 +163,7 @@
                 "    if_name: \"wlan0\"",
                 "  >",
                 "  time_ms: 100",
+                "  transport: 0",
                 ">",
                 "events <",
                 "  dhcp_event <",
@@ -168,6 +173,7 @@
                 "    state_transition: \"SomeState\"",
                 "  >",
                 "  time_ms: 200",
+                "  transport: 0",
                 ">",
                 "events <",
                 "  default_network_event <",
@@ -183,6 +189,7 @@
                 "    transport_types: 3",
                 "  >",
                 "  time_ms: 300",
+                "  transport: 0",
                 ">",
                 "events <",
                 "  ip_provisioning_event <",
@@ -191,9 +198,11 @@
                 "    latency_ms: 5678",
                 "  >",
                 "  time_ms: 400",
+                "  transport: 0",
                 ">",
                 "events <",
                 "  time_ms: 500",
+                "  transport: 0",
                 "  validation_probe_event <",
                 "    latency_ms: 40730",
                 "    network_id <",
@@ -215,6 +224,7 @@
                 "    zero_lifetime_ras: 1",
                 "  >",
                 "  time_ms: 600",
+                "  transport: 0",
                 ">",
                 "events <",
                 "  ra_event <",
@@ -226,6 +236,7 @@
                 "    router_lifetime: 2000",
                 "  >",
                 "  time_ms: 700",
+                "  transport: 0",
                 ">",
                 "version: 2");
 
@@ -254,8 +265,7 @@
         try {
             byte[] got = Base64.decode(output, Base64.DEFAULT);
             IpConnectivityLogClass.IpConnectivityLog log =
-                    new IpConnectivityLogClass.IpConnectivityLog();
-            MessageNano.mergeFrom(log, got);
+                    IpConnectivityLogClass.IpConnectivityLog.parseFrom(got);
             assertEquals(want, log.toString());
         } catch (Exception e) {
             fail(e.toString());
@@ -283,10 +293,5 @@
     }
 
     static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR =
-        new Comparator<ConnectivityMetricsEvent>() {
-            @Override
-            public int compare(ConnectivityMetricsEvent ev1, ConnectivityMetricsEvent ev2) {
-                return (int) (ev1.timestamp - ev2.timestamp);
-            }
-        };
+        Comparator.comparingLong((ev) -> ev.timestamp);
 }
diff --git a/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index 2bb62bb..6c8babb 100644
--- a/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -16,25 +16,34 @@
 
 package com.android.server.connectivity;
 
-import android.net.ConnectivityManager.NetworkCallback;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.metrics.DnsEvent;
 import android.net.metrics.INetdEventListener;
 import android.net.metrics.IpConnectivityLog;
 import android.os.RemoteException;
+import android.system.OsConstants;
 import android.test.suitebuilder.annotation.SmallTest;
-
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityEvent;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
 import junit.framework.TestCase;
 import org.junit.Before;
 import org.junit.Test;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertTrue;
-
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
@@ -42,13 +51,6 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import java.io.FileOutputStream;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.IntStream;
-
 public class NetdEventListenerServiceTest extends TestCase {
 
     // TODO: read from NetdEventListenerService after this constant is read from system property
@@ -68,45 +70,48 @@
         }
     }
 
+    private static final String EXAMPLE_IPV4 = "192.0.2.1";
+    private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
+
     NetdEventListenerService mNetdEventListenerService;
 
     @Mock ConnectivityManager mCm;
     @Mock IpConnectivityLog mLog;
     ArgumentCaptor<NetworkCallback> mCallbackCaptor;
-    ArgumentCaptor<DnsEvent> mEvCaptor;
+    ArgumentCaptor<DnsEvent> mDnsEvCaptor;
 
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mCallbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class);
-        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
+        mDnsEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
         mNetdEventListenerService = new NetdEventListenerService(mCm, mLog);
 
         verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture());
     }
 
     @SmallTest
-    public void testOneBatch() throws Exception {
+    public void testOneDnsBatch() throws Exception {
         log(105, LATENCIES);
         log(106, Arrays.copyOf(LATENCIES, BATCH_SIZE - 1)); // one lookup short of a batch event
 
-        verifyLoggedEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+        verifyLoggedDnsEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
 
         log(106, Arrays.copyOfRange(LATENCIES, BATCH_SIZE - 1, BATCH_SIZE));
 
-        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
-        verifyLoggedEvents(
+        mDnsEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
+        verifyLoggedDnsEvents(
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
     @SmallTest
-    public void testSeveralBatches() throws Exception {
+    public void testSeveralDmsBatches() throws Exception {
         log(105, LATENCIES);
         log(106, LATENCIES);
         log(105, LATENCIES);
         log(107, LATENCIES);
 
-        verifyLoggedEvents(
+        verifyLoggedDnsEvents(
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
@@ -114,7 +119,7 @@
     }
 
     @SmallTest
-    public void testBatchAndNetworkLost() throws Exception {
+    public void testDnsBatchAndNetworkLost() throws Exception {
         byte[] eventTypes = Arrays.copyOf(EVENT_TYPES, 20);
         byte[] returnCodes = Arrays.copyOf(RETURN_CODES, 20);
         int[] latencies = Arrays.copyOf(LATENCIES, 20);
@@ -124,14 +129,14 @@
         mCallbackCaptor.getValue().onLost(new Network(105));
         log(105, LATENCIES);
 
-        verifyLoggedEvents(
+        verifyLoggedDnsEvents(
             new DnsEvent(105, eventTypes, returnCodes, latencies),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
     @SmallTest
-    public void testConcurrentBatchesAndDumps() throws Exception {
+    public void testConcurrentDnsBatchesAndDumps() throws Exception {
         final long stop = System.currentTimeMillis() + 100;
         final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null"));
         new Thread() {
@@ -142,27 +147,121 @@
             }
         }.start();
 
-        logAsync(105, LATENCIES);
-        logAsync(106, LATENCIES);
-        logAsync(107, LATENCIES);
+        logDnsAsync(105, LATENCIES);
+        logDnsAsync(106, LATENCIES);
+        logDnsAsync(107, LATENCIES);
 
-        verifyLoggedEvents(500,
+        verifyLoggedDnsEvents(500,
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
     @SmallTest
-    public void testConcurrentBatchesAndNetworkLoss() throws Exception {
-        logAsync(105, LATENCIES);
+    public void testConcurrentDnsBatchesAndNetworkLoss() throws Exception {
+        logDnsAsync(105, LATENCIES);
         Thread.sleep(10L);
-        // call onLost() asynchronously to logAsync's onDnsEvent() calls.
+        // call onLost() asynchronously to logDnsAsync's onDnsEvent() calls.
         mCallbackCaptor.getValue().onLost(new Network(105));
 
         // do not verify unpredictable batch
         verify(mLog, timeout(500).times(1)).log(any());
     }
 
+    @SmallTest
+    public void testConnectLogging() throws Exception {
+        final int OK = 0;
+        Thread[] logActions = {
+            // ignored
+            connectEventAction(OsConstants.EALREADY, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EALREADY, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            // valid latencies
+            connectEventAction(OK, 110, EXAMPLE_IPV4),
+            connectEventAction(OK, 23, EXAMPLE_IPV4),
+            connectEventAction(OK, 45, EXAMPLE_IPV4),
+            connectEventAction(OK, 56, EXAMPLE_IPV4),
+            connectEventAction(OK, 523, EXAMPLE_IPV6),
+            connectEventAction(OK, 214, EXAMPLE_IPV6),
+            connectEventAction(OK, 67, EXAMPLE_IPV6),
+            // errors
+            connectEventAction(OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EAGAIN, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EADDRINUSE, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.ECONNREFUSED, 0, EXAMPLE_IPV4),
+        };
+
+        for (Thread t : logActions) {
+            t.start();
+        }
+        for (Thread t : logActions) {
+            t.join();
+        }
+
+        List<IpConnectivityEvent> events = new ArrayList<>();
+        mNetdEventListenerService.flushStatistics(events);
+
+        IpConnectivityEvent got = events.get(0);
+        String want = joinLines(
+                "connect_statistics <",
+                "  connect_count: 12",
+                "  errnos_counters <",
+                "    key: 1",
+                "    value: 2",
+                "  >",
+                "  errnos_counters <",
+                "    key: 11",
+                "    value: 1",
+                "  >",
+                "  errnos_counters <",
+                "    key: 13",
+                "    value: 3",
+                "  >",
+                "  errnos_counters <",
+                "    key: 98",
+                "    value: 1",
+                "  >",
+                "  errnos_counters <",
+                "    key: 110",
+                "    value: 3",
+                "  >",
+                "  errnos_counters <",
+                "    key: 111",
+                "    value: 1",
+                "  >",
+                "  ipv6_addr_count: 6",
+                "  latencies_ms: 23",
+                "  latencies_ms: 45",
+                "  latencies_ms: 56",
+                "  latencies_ms: 67",
+                "  latencies_ms: 110",
+                "  latencies_ms: 214",
+                "  latencies_ms: 523",
+                ">",
+                "time_ms: 0",
+                "transport: 0");
+        verifyConnectEvent(want, got);
+    }
+
+    Thread connectEventAction(int error, int latencyMs, String ipAddr) {
+        return new Thread(() -> {
+            try {
+                mNetdEventListenerService.onConnectEvent(100, error, latencyMs, ipAddr, 80, 1);
+            } catch (Exception e) {
+                fail(e.toString());
+            }
+        });
+    }
+
     void log(int netId, int[] latencies) {
         try {
             for (int l : latencies) {
@@ -174,7 +273,7 @@
         }
     }
 
-    void logAsync(int netId, int[] latencies) {
+    void logDnsAsync(int netId, int[] latencies) {
         new Thread() {
             public void run() {
                 log(netId, latencies);
@@ -182,15 +281,15 @@
         }.start();
     }
 
-    void verifyLoggedEvents(DnsEvent... expected) {
-        verifyLoggedEvents(0, expected);
+    void verifyLoggedDnsEvents(DnsEvent... expected) {
+        verifyLoggedDnsEvents(0, expected);
     }
 
-    void verifyLoggedEvents(int wait, DnsEvent... expectedEvents) {
-        verify(mLog, timeout(wait).times(expectedEvents.length)).log(mEvCaptor.capture());
-        for (DnsEvent got : mEvCaptor.getAllValues()) {
+    void verifyLoggedDnsEvents(int wait, DnsEvent... expectedEvents) {
+        verify(mLog, timeout(wait).times(expectedEvents.length)).log(mDnsEvCaptor.capture());
+        for (DnsEvent got : mDnsEvCaptor.getAllValues()) {
             OptionalInt index = IntStream.range(0, expectedEvents.length)
-                    .filter(i -> eventsEqual(expectedEvents[i], got))
+                    .filter(i -> dnsEventsEqual(expectedEvents[i], got))
                     .findFirst();
             // Don't match same expected event more than once.
             index.ifPresent(i -> expectedEvents[i] = null);
@@ -199,11 +298,30 @@
     }
 
     /** equality function for DnsEvent to avoid overriding equals() and hashCode(). */
-    static boolean eventsEqual(DnsEvent expected, DnsEvent got) {
+    static boolean dnsEventsEqual(DnsEvent expected, DnsEvent got) {
         return (expected == got) || ((expected != null) && (got != null)
                 && (expected.netId == got.netId)
                 && Arrays.equals(expected.eventTypes, got.eventTypes)
                 && Arrays.equals(expected.returnCodes, got.returnCodes)
                 && Arrays.equals(expected.latenciesMs, got.latenciesMs));
     }
+
+    static String joinLines(String ... elems) {
+        StringBuilder b = new StringBuilder();
+        for (String s : elems) {
+            b.append(s).append("\n");
+        }
+        return b.toString();
+    }
+
+    static void verifyConnectEvent(String expected, IpConnectivityEvent got) {
+        try {
+            Arrays.sort(got.connectStatistics.latenciesMs);
+            Arrays.sort(got.connectStatistics.errnosCounters,
+                    Comparator.comparingInt((p) -> p.key));
+            assertEquals(expected, got.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
 }