Allow network transport type override

Bug: b/112588045
Test: atest EthernetServiceTests --verbose
Test: kitchensink with RPi devices connected by USB -> Ethernet
      adapters. Edit config.xml to try different tranport
      overrides manually. Use dumpsys ethernet to show final
      final network scores.
Change-Id: I482e78a76d06c9c090aa6816db14bb346eb6528b
diff --git a/java/com/android/server/ethernet/EthernetNetworkFactory.java b/java/com/android/server/ethernet/EthernetNetworkFactory.java
index 54bcdc3..1f4f7fa 100644
--- a/java/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/java/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -16,9 +16,11 @@
 
 package com.android.server.ethernet;
 
-import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static com.android.internal.util.Preconditions.checkNotNull;
 
+import android.annotation.NonNull;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
@@ -37,10 +39,12 @@
 import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.SparseArray;
 
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.FileDescriptor;
+import java.lang.Math;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -239,6 +243,52 @@
         private NetworkAgent mNetworkAgent;
         private IpConfiguration mIpConfig;
 
+        /**
+         * An object to contain all transport type information, including base network score and
+         * the legacy transport type it maps to (if any)
+         */
+        private static class TransportInfo {
+            final int mLegacyType;
+            final int mScore;
+
+            private TransportInfo(int legacyType, int score) {
+                mLegacyType = legacyType;
+                mScore = score;
+            }
+        }
+
+        /**
+         * A map of TRANSPORT_* types to TransportInfo, making scoring and legacy type information
+         * available for each type an ethernet interface could propagate.
+         *
+         * Unfortunately, base scores for the various transports are not yet centrally located.
+         * They've been lifted from the corresponding NetworkFactory files in the meantime.
+         *
+         * Additionally, there are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types
+         * are set to TYPE_NONE to match the behavior of their own network factories.
+         */
+        private static final SparseArray<TransportInfo> sTransports = new SparseArray();
+        static {
+            // LowpanInterfaceTracker.NETWORK_SCORE
+            sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN,
+                    new TransportInfo(ConnectivityManager.TYPE_NONE, 30));
+            // WifiAwareDataPathStateManager.NETWORK_FACTORY_SCORE_AVAIL
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                    new TransportInfo(ConnectivityManager.TYPE_NONE, 1));
+            // EthernetNetworkFactory.NETWORK_SCORE
+            sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET,
+                    new TransportInfo(ConnectivityManager.TYPE_ETHERNET, 70));
+            // BluetoothTetheringNetworkFactory.NETWORK_SCORE
+            sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                    new TransportInfo(ConnectivityManager.TYPE_BLUETOOTH, 69));
+            // WifiNetworkFactory.SCORE_FILTER / NetworkAgent.WIFI_BASE_SCORE
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI,
+                    new TransportInfo(ConnectivityManager.TYPE_WIFI, 60));
+            // TelephonyNetworkFactory.TELEPHONY_NETWORK_SCORE
+            sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR,
+                    new TransportInfo(ConnectivityManager.TYPE_MOBILE, 50));
+        }
+
         long refCount = 0;
 
         private final IpClient.Callback mIpClientCallback = new IpClient.Callback() {
@@ -259,14 +309,23 @@
         };
 
         NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
-                NetworkCapabilities capabilities) {
+                @NonNull NetworkCapabilities capabilities) {
             name = ifaceName;
-            mCapabilities = capabilities;
+            mCapabilities = checkNotNull(capabilities);
             mHandler = handler;
             mContext = context;
+            int legacyType = ConnectivityManager.TYPE_NONE;
+            int[] transportTypes = mCapabilities.getTransportTypes();
+            if (transportTypes.length > 0) {
+                legacyType = getLegacyType(transportTypes[0]);
+            } else {
+                // Should never happen as transport is always one of ETHERNET or a valid override
+                Log.w(TAG, "There is no transport type associated with network interface '"
+                        + mLinkProperties.getInterfaceName() + "' -- Legacy type set to TYPE_NONE");
+            }
 
             mHwAddress = hwAddress;
-            mNetworkInfo = new NetworkInfo(TYPE_ETHERNET, 0, NETWORK_TYPE, "");
+            mNetworkInfo = new NetworkInfo(legacyType, 0, NETWORK_TYPE, "");
             mNetworkInfo.setExtraInfo(mHwAddress);
             mNetworkInfo.setIsAvailable(true);
         }
@@ -283,6 +342,47 @@
             return mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         }
 
+        /**
+         * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults
+         * to legacy TYPE_NONE if there is no known conversion
+         */
+        private static int getLegacyType(int transport) {
+            TransportInfo transportInfo = sTransports.get(transport, /* if dne */ null);
+            if (transportInfo != null) {
+                return transportInfo.mLegacyType;
+            }
+            return ConnectivityManager.TYPE_NONE;
+        }
+
+        /**
+         * Determines the network score based on the transport associated with the interface.
+         * Ethernet interfaces could propagate a transport types forward. Since we can't
+         * get more information about the statuses of the interfaces on the other end of the local
+         * interface, we'll best-effort assign the score as the base score of the assigned transport
+         * when the link is up. When the link is down, the score is set to zero.
+         *
+         * This function is called with the purpose of assigning and updating the network score of
+         * the member NetworkAgent.
+         */
+        private int getNetworkScore() {
+            // never set the network score below 0.
+            if (!mLinkUp) {
+                return 0;
+            }
+
+            int[] transportTypes = mCapabilities.getTransportTypes();
+            if (transportTypes.length < 1) {
+                Log.w(TAG, "There is no transport type associated with network interface '"
+                        + mLinkProperties.getInterfaceName() + "' -- Score set to zero");
+                return 0;
+            }
+            TransportInfo transportInfo = sTransports.get(transportTypes[0], /* if dne */ null);
+            if (transportInfo != null) {
+                return transportInfo.mScore;
+            }
+            return 0;
+        }
+
         private void start() {
             if (mIpClient != null) {
                 if (DBG) Log.d(TAG, "IpClient already started");
@@ -317,7 +417,7 @@
             // Create our NetworkAgent.
             mNetworkAgent = new NetworkAgent(mHandler.getLooper(), mContext,
                     NETWORK_TYPE, mNetworkInfo, mCapabilities, mLinkProperties,
-                    NETWORK_SCORE) {
+                    getNetworkScore()) {
                 public void unwanted() {
                     if (this == mNetworkAgent) {
                         stop();
@@ -390,8 +490,11 @@
             mNetworkAgent.sendNetworkCapabilities(mCapabilities);
             mNetworkAgent.sendNetworkInfo(mNetworkInfo);
             mNetworkAgent.sendLinkProperties(mLinkProperties);
-            // never set the network score below 0.
-            mNetworkAgent.sendNetworkScore(mLinkUp? NETWORK_SCORE : 0);
+
+            // As a note, getNetworkScore() is fairly expensive to calculate. This is fine for now
+            // since the agent isn't update frequently. Consider caching the score in the future if
+            // agent updating is required more often
+            mNetworkAgent.sendNetworkScore(getNetworkScore());
         }
 
         private void clear() {
@@ -433,7 +536,9 @@
                     + "up: " + mLinkUp + ", "
                     + "hwAddress: " + mHwAddress + ", "
                     + "networkInfo: " + mNetworkInfo + ", "
+                    + "networkCapabilities: " + mCapabilities + ", "
                     + "networkAgent: " + mNetworkAgent + ", "
+                    + "score: " + getNetworkScore() + ", "
                     + "ipClient: " + mIpClient + ","
                     + "linkProperties: " + mLinkProperties
                     + "}";
diff --git a/java/com/android/server/ethernet/EthernetTracker.java b/java/com/android/server/ethernet/EthernetTracker.java
index 00eedd5..099e054 100644
--- a/java/com/android/server/ethernet/EthernetTracker.java
+++ b/java/com/android/server/ethernet/EthernetTracker.java
@@ -294,12 +294,20 @@
         }
     }
 
+    /**
+     * Parses an Ethernet interface configuration
+     *
+     * @param configString represents an Ethernet configuration in the following format: {@code
+     * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]}
+     */
     private void parseEthernetConfig(String configString) {
-        String[] tokens = configString.split(";");
+        String[] tokens = configString.split(";", /* limit of tokens */ 4);
         String name = tokens[0];
         String capabilities = tokens.length > 1 ? tokens[1] : null;
+        String transport = tokens.length > 3 ? tokens[3] : null;
         NetworkCapabilities nc = createNetworkCapabilities(
-                !TextUtils.isEmpty(capabilities)  /* clear default capabilities */, capabilities);
+                !TextUtils.isEmpty(capabilities)  /* clear default capabilities */, capabilities,
+                transport);
         mNetworkCapabilities.put(name, nc);
 
         if (tokens.length > 2 && !TextUtils.isEmpty(tokens[2])) {
@@ -320,24 +328,76 @@
     }
 
     private static NetworkCapabilities createNetworkCapabilities(boolean clearDefaultCapabilities) {
-        return createNetworkCapabilities(clearDefaultCapabilities, null);
+        return createNetworkCapabilities(clearDefaultCapabilities, null, null);
     }
 
-    private static NetworkCapabilities createNetworkCapabilities(
-            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities) {
+    /**
+     * Parses a static list of network capabilities
+     *
+     * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
+     * @param commaSeparatedCapabilities A comma separated string list of integer encoded
+     *                                   NetworkCapability.NET_CAPABILITY_* values
+     * @param overrideTransport A string representing a single override transport type, must be one
+     *                                 of the NetworkCapability.TRANSPORT_* values. TRANSPORT_VPN is
+     *                                 not supported. Errors with input will cause the override to
+     *                                 be ignored.
+     */
+    @VisibleForTesting
+    static NetworkCapabilities createNetworkCapabilities(
+            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
+            @Nullable String overrideTransport) {
 
         NetworkCapabilities nc = new NetworkCapabilities();
         if (clearDefaultCapabilities) {
-            nc.clearAll();  // Remove default capabilities.
+            nc.clearAll();  // Remove default capabilities and transports
         }
-        nc.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+
+        // Determine the transport type. If someone has tried to define an override transport then
+        // attempt to add it. Since we can only have one override, all errors with it will
+        // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an
+        // override type. Wifi Aware and LoWPAN are currently unsupported as well.
+        int transport = NetworkCapabilities.TRANSPORT_ETHERNET;
+        if (!TextUtils.isEmpty(overrideTransport)) {
+            try {
+                int parsedTransport = Integer.valueOf(overrideTransport);
+                if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) {
+                    Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported -- "
+                            + "Defaulting to TRANSPORT_ETHERNET");
+                } else {
+                    transport = parsedTransport;
+                }
+            } catch (NumberFormatException nfe) {
+                Log.e(TAG, "Override transport type '" + overrideTransport + "' could not be parsed"
+                        + "-- Defaulting to TRANSPORT_ETHERNET");
+            }
+        }
+
+        // Apply the transport. If the user supplied a valid number that is not a valid transport
+        // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens
+        try {
+            nc.addTransportType(transport);
+        } catch (IllegalArgumentException iae) {
+            Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value"
+                    + " -- Defaulting to TRANSPORT_ETHERNET");
+            nc.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+        }
+
         nc.setLinkUpstreamBandwidthKbps(100 * 1000);
         nc.setLinkDownstreamBandwidthKbps(100 * 1000);
 
         if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
             for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
                 if (!TextUtils.isEmpty(strNetworkCapability)) {
-                    nc.addCapability(Integer.valueOf(strNetworkCapability));
+                    try {
+                        nc.addCapability(Integer.valueOf(strNetworkCapability));
+                    } catch (NumberFormatException nfe) {
+                        Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed");
+                    } catch (IllegalArgumentException iae) {
+                        Log.e(TAG, strNetworkCapability + " is not a valid "
+                                + "NetworkCapability.NET_CAPABILITY_* value");
+                    }
                 }
             }
         }
diff --git a/tests/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/java/com/android/server/ethernet/EthernetTrackerTest.java
index 70d316d..a56c9fe 100644
--- a/tests/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -23,6 +23,7 @@
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
 import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
 import android.net.StaticIpConfiguration;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -35,47 +36,55 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class EthernetTrackerTest {
-
+    /**
+     * Test: Creation of various valid static IP configurations
+     */
     @Test
     public void createStaticIpConfiguration() {
-        assertStaticConfiguration("", new StaticIpConfiguration());
+        // Empty gives default StaticIPConfiguration object
+        assertStaticConfiguration(new StaticIpConfiguration(), "");
 
+        // Setting only the IP address properly cascades and assumes defaults
         assertStaticConfiguration(
-                "ip=192.0.2.10/24",
-                new StaticIpConfigBuilder().setIp("192.0.2.10/24").build());
+                new StaticIpConfigBuilder().setIp("192.0.2.10/24").build(),
+                "ip=192.0.2.10/24");
 
+        // Setting other fields properly cascades them
         assertStaticConfiguration(
-                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android",
                 new StaticIpConfigBuilder()
                         .setIp("192.0.2.10/24")
                         .setDns(new String[] {"4.4.4.4", "8.8.8.8"})
                         .setGateway("192.0.2.1")
                         .setDomains("android")
-                        .build());
+                        .build(),
+                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android");
 
         // Verify order doesn't matter
         assertStaticConfiguration(
-                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ",
                 new StaticIpConfigBuilder()
                         .setIp("192.0.2.10/24")
                         .setDns(new String[] {"4.4.4.4", "8.8.8.8"})
                         .setGateway("192.0.2.1")
                         .setDomains("android")
-                        .build());
+                        .build(),
+                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ");
     }
 
+    /**
+     * Test: Attempt creation of various bad static IP configurations
+     */
     @Test
     public void createStaticIpConfiguration_Bad() {
-        assertFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
-        assertFails("ip=192.0.2.1");  // mask is missing
-        assertFails("ip=a.b.c");  // not a valid ip address
-        assertFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
-        assertFails("=");  // Key and value is empty
-        assertFails("ip=");  // Value is empty
-        assertFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
+        assertStaticConfigurationFails("ip=192.0.2.1");  // mask is missing
+        assertStaticConfigurationFails("ip=a.b.c");  // not a valid ip address
+        assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
+        assertStaticConfigurationFails("=");  // Key and value is empty
+        assertStaticConfigurationFails("ip=");  // Value is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
     }
 
-    private void assertFails(String config) {
+    private void assertStaticConfigurationFails(String config) {
         try {
             EthernetTracker.parseStaticIpConfiguration(config);
             fail("Expected to fail: " + config);
@@ -84,8 +93,8 @@
         }
     }
 
-    private void assertStaticConfiguration(String configAsString,
-            StaticIpConfiguration expectedStaticIpConfig) {
+    private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig,
+                String configAsString) {
         IpConfiguration expectedIpConfiguration = new IpConfiguration(IpAssignment.STATIC,
                 ProxySettings.NONE, expectedStaticIpConfig, null);
 
@@ -118,9 +127,178 @@
             return this;
         }
 
-
         StaticIpConfiguration build() {
             return new StaticIpConfiguration(config);
         }
     }
+
+    /**
+     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     */
+    @Test
+    public void createNetworkCapabilities() {
+
+        // Particularly common expected results
+        NetworkCapabilities defaultEthernetCleared = new NetworkCapabilitiesBuilder()
+                .clearAll()
+                .setLinkUpstreamBandwidthKbps(100000)
+                .setLinkDownstreamBandwidthKbps(100000)
+                .addTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .build();
+
+        NetworkCapabilities ethernetClearedWithCommonCaps = new NetworkCapabilitiesBuilder()
+                .clearAll()
+                .setLinkUpstreamBandwidthKbps(100000)
+                .setLinkDownstreamBandwidthKbps(100000)
+                .addTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(12)
+                .addCapability(13)
+                .addCapability(14)
+                .addCapability(15)
+                .build();
+
+        // Empty capabilities and transports lists with a "please clear defaults" should
+        // yield an empty capabilities set with TRANPORT_ETHERNET
+        assertNetworkCapabilities(defaultEthernetCleared, true, "", "");
+
+        // Empty capabilities and transports without the clear defaults flag should return the
+        // default capabilities set with TRANSPORT_ETHERNET
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build(),
+                false, "", "");
+
+        // A list of capabilities without the clear defaults flag should return the default
+        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(11)
+                        .addCapability(12)
+                        .build(),
+                false, "11,12", "");
+
+        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
+        // with a default TRANSPORT_ETHERNET since no overrides are specified
+        assertNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+
+        // Adding any invalid capabilities to the list will cause them to be ignored
+        assertNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
+        assertNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+
+        // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
+        // and apply only the override to the capabiltities object
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .clearAll()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(0)
+                        .build(),
+                true, "", "0");
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .clearAll()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(1)
+                        .build(),
+                true, "", "1");
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .clearAll()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(2)
+                        .build(),
+                true, "", "2");
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .clearAll()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransport(3)
+                        .build(),
+                true, "", "3");
+
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
+        assertNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+
+        // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+
+        // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+
+        // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
+        assertNetworkCapabilities(defaultEthernetCleared,true, "", "100");
+        assertNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+
+        // Ensure the adding of both capabilities and transports work
+        assertNetworkCapabilities(
+                new NetworkCapabilitiesBuilder()
+                        .clearAll()
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .addTransport(3)
+                        .build(),
+                true, "12,13,14,15", "3");
+
+        // Ensure order does not matter for capability list
+        assertNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+    }
+
+    private void assertNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
+            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
+        assertEquals(expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
+                        configTransports));
+    }
+
+    private static class NetworkCapabilitiesBuilder {
+        private final NetworkCapabilities nc = new NetworkCapabilities();
+
+        NetworkCapabilitiesBuilder clearAll(){
+            // This is THE ONLY one that doesn't return a reference to the object so I wrapped
+            // everything in a builder to keep things consistent and clean above. Fix if this
+            // ever changes
+            nc.clearAll();
+            return this;
+        }
+
+        NetworkCapabilitiesBuilder addCapability(int capability) {
+            nc.addCapability(capability);
+            return this;
+        }
+
+        NetworkCapabilitiesBuilder addTransport(int transport) {
+            nc.addTransportType(transport);
+            return this;
+        }
+
+        NetworkCapabilitiesBuilder setLinkUpstreamBandwidthKbps(int upKbps) {
+            nc.setLinkUpstreamBandwidthKbps(upKbps);
+            return this;
+        }
+
+        NetworkCapabilitiesBuilder setLinkDownstreamBandwidthKbps(int downKbps) {
+            nc.setLinkDownstreamBandwidthKbps(downKbps);
+            return this;
+        }
+
+        NetworkCapabilities build() {
+            return new NetworkCapabilities(nc);
+        }
+    }
 }