Merge "Adjust visibility rules for migration"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index c6a17fe..c7a4bd5 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -149,6 +149,7 @@
     name: "net-utils-services-common-srcs",
     srcs: [
         "device/android/net/NetworkFactory.java",
+        "device/com/android/net/module/util/CollectionUtils.java",
     ],
     visibility: [
         "//frameworks/base/services/net",
diff --git a/staticlibs/device/com/android/net/module/util/CollectionUtils.java b/staticlibs/device/com/android/net/module/util/CollectionUtils.java
new file mode 100644
index 0000000..74f738d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/CollectionUtils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Utilities for {@link Collection} and arrays.
+ */
+public final class CollectionUtils {
+    private CollectionUtils() {}
+
+    /**
+     * @return True if the array is null or 0-length.
+     */
+    public static <T> boolean isEmpty(@Nullable T[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Returns an int array from the given Integer list.
+     */
+    @NonNull
+    public static int[] toIntArray(@NonNull Collection<Integer> list) {
+        int[] array = new int[list.size()];
+        int i = 0;
+        for (Integer item : list) {
+            array[i] = item;
+            i++;
+        }
+        return array;
+    }
+
+    /**
+     * Returns a long array from the given long list.
+     */
+    @NonNull
+    public static long[] toLongArray(@NonNull Collection<Long> list) {
+        long[] array = new long[list.size()];
+        int i = 0;
+        for (Long item : list) {
+            array[i] = item;
+            i++;
+        }
+        return array;
+    }
+
+    /**
+     * @return True if there exists at least one element in the sparse array for which
+     * condition {@code predicate}
+     */
+    public static <T> boolean any(@NonNull SparseArray<T> array, @NonNull Predicate<T> predicate) {
+        for (int i = 0; i < array.size(); ++i) {
+            if (predicate.test(array.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the array contains the specified value.
+     */
+    public static boolean contains(@Nullable int[] array, int value) {
+        if (array == null) return false;
+        for (int element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the array contains the specified value.
+     */
+    public static <T> boolean contains(@Nullable T[] array, @Nullable T value) {
+        if (array == null) return false;
+        for (T element : array) {
+            if (Objects.equals(element, value)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
new file mode 100644
index 0000000..271cc6e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import androidx.annotation.BoolRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Utilities for modules to query {@link DeviceConfig} and flags.
+ */
+public final class DeviceConfigUtils {
+    private DeviceConfigUtils() {}
+
+    private static final String TAG = DeviceConfigUtils.class.getSimpleName();
+
+    @VisibleForTesting
+    protected static volatile long sPackageVersion = -1;
+    private static long getPackageVersion(@NonNull final Context context)
+            throws PackageManager.NameNotFoundException {
+        // sPackageVersion may be set by another thread just after this check, but querying the
+        // package version several times on rare occasions is fine.
+        if (sPackageVersion >= 0) {
+            return sPackageVersion;
+        }
+        final long version = context.getPackageManager().getPackageInfo(
+                context.getPackageName(), 0).getLongVersionCode();
+        sPackageVersion = version;
+        return version;
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or has no valid value.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    @Nullable
+    public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
+            @Nullable String defaultValue) {
+        String value = DeviceConfig.getProperty(namespace, name);
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
+            int defaultValue) {
+        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
+        try {
+            return (value != null) ? Integer.parseInt(value) : defaultValue;
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     *
+     * Flags like timeouts should use this method and set an appropriate min/max range: if invalid
+     * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range
+     * protects against this kind of breakage.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param minimumValue The minimum value of a property.
+     * @param maximumValue The maximum value of a property.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists or the fetched value is
+     *         not in the provided range.
+     */
+    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
+            int minimumValue, int maximumValue, int defaultValue) {
+        int value = getDeviceConfigPropertyInt(namespace, name, defaultValue);
+        if (value < minimumValue || value > maximumValue) return defaultValue;
+        return value;
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultValue The value to return if the property does not exist or its value is null.
+     * @return the corresponding value, or defaultValue if none exists.
+     */
+    public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace,
+            @NonNull String name, boolean defaultValue) {
+        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
+        return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    /**
+     * Check whether or not one specific experimental feature for a particular namespace from
+     * {@link DeviceConfig} is enabled by comparing module package version
+     * with current version of property. If this property version is valid, the corresponding
+     * experimental feature would be enabled, otherwise disabled.
+     *
+     * This is useful to ensure that if a module install is rolled back, flags are not left fully
+     * rolled out on a version where they have not been well tested.
+     * @param context The global context information about an app environment.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+            @NonNull String name) {
+        return isFeatureEnabled(context, namespace, name, false /* defaultEnabled */);
+    }
+
+    /**
+     * Check whether or not one specific experimental feature for a particular namespace from
+     * {@link DeviceConfig} is enabled by comparing module package version
+     * with current version of property. If this property version is valid, the corresponding
+     * experimental feature would be enabled, otherwise disabled.
+     *
+     * This is useful to ensure that if a module install is rolled back, flags are not left fully
+     * rolled out on a version where they have not been well tested.
+     * @param context The global context information about an app environment.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultEnabled The value to return if the property does not exist or its value is
+     *                       null.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+            @NonNull String name, boolean defaultEnabled) {
+        try {
+            final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
+                    0 /* default value */);
+            final long packageVersion = getPackageVersion(context);
+            return (propertyVersion == 0 && defaultEnabled)
+                    || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Could not find the package name", e);
+            return false;
+        }
+    }
+
+    /**
+     * Gets boolean config from resources.
+     */
+    public static boolean getResBooleanConfig(@NonNull final Context context,
+            @BoolRes int configResource, final boolean defaultValue) {
+        final Resources res = context.getResources();
+        try {
+            return res.getBoolean(configResource);
+        } catch (Resources.NotFoundException e) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/NetworkCapabilitiesUtils.java b/staticlibs/device/com/android/net/module/util/NetworkCapabilitiesUtils.java
new file mode 100644
index 0000000..dd5a481
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utilities to examine {@link android.net.NetworkCapabilities}.
+ */
+public final class NetworkCapabilitiesUtils {
+    // Transports considered to classify networks in UI, in order of which transport should be
+    // surfaced when there are multiple transports. Transports not in this list do not have
+    // an ordering preference (in practice they will have a deterministic order based on the
+    // transport int itself).
+    private static final int[] DISPLAY_TRANSPORT_PRIORITIES = new int[] {
+        // Users think of their VPNs as VPNs, not as any of the underlying nets
+        TRANSPORT_VPN,
+        // If the network has cell, prefer showing that because it's usually metered.
+        TRANSPORT_CELLULAR,
+        // If the network has WiFi aware, prefer showing that as it's a more specific use case.
+        // Ethernet can masquerade as other transports, where the device uses ethernet to connect to
+        // a box providing cell or wifi. Today this is represented by only the masqueraded type for
+        // backward compatibility, but these networks should morally have Ethernet & the masqueraded
+        // type. Because of this, prefer other transports instead of Ethernet.
+        TRANSPORT_WIFI_AWARE,
+        TRANSPORT_BLUETOOTH,
+        TRANSPORT_WIFI,
+        TRANSPORT_ETHERNET
+
+        // Notably, TRANSPORT_TEST is not in this list as any network that has TRANSPORT_TEST and
+        // one of the above transports should be counted as that transport, to keep tests as
+        // realistic as possible.
+    };
+
+    /**
+     * Get a transport that can be used to classify a network when displaying its info to users.
+     *
+     * While networks can have multiple transports, users generally think of them as "wifi",
+     * "mobile data", "vpn" and expect them to be classified as such in UI such as settings.
+     * @param transports Non-empty array of transports on a network
+     * @return A single transport
+     * @throws IllegalArgumentException The array is empty
+     */
+    public static int getDisplayTransport(@NonNull int[] transports) {
+        for (int transport : DISPLAY_TRANSPORT_PRIORITIES) {
+            if (CollectionUtils.contains(transports, transport)) {
+                return transport;
+            }
+        }
+
+        if (transports.length < 1) {
+            // All NetworkCapabilities representing a network have at least one transport, so an
+            // empty transport array would be created by the caller instead of extracted from
+            // NetworkCapabilities.
+            throw new IllegalArgumentException("No transport in the provided array");
+        }
+        return transports[0];
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index bc8761a..6d8d301 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util;
 
 import android.annotation.NonNull;
+import android.net.MacAddress;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -110,6 +111,7 @@
         UBE63,     // unsigned long(MSB: 0) in network order, size = 8 bytes
         UBE64,     // unsigned long in network order,  size = 8 bytes
         ByteArray, // byte array with predefined length
+        EUI48,     // a 48-bits long MAC address
     }
 
     /**
@@ -179,6 +181,9 @@
                             + annotation.arraysize());
                 }
                 return;
+            case EUI48:
+                if (fieldType == MacAddress.class) return;
+                break;
             default:
                 throw new IllegalArgumentException("Unknown type" + annotation.type());
         }
@@ -213,6 +218,9 @@
             case ByteArray:
                 length = annotation.arraysize();
                 break;
+            case EUI48:
+                length = 6;
+                break;
             default:
                 throw new IllegalArgumentException("Unknown type" + annotation.type());
         }
@@ -373,6 +381,11 @@
                 buf.get(array);
                 value = array;
                 break;
+            case EUI48:
+                final byte[] macAddress = new byte[6];
+                buf.get(macAddress);
+                value = MacAddress.fromBytes(macAddress);
+                break;
             default:
                 throw new IllegalArgumentException("Unknown type:" + fieldInfo.annotation.type());
         }
@@ -442,6 +455,10 @@
             case ByteArray:
                 output.put((byte[]) value);
                 break;
+            case EUI48:
+                final byte[] macAddress = ((MacAddress) value).toByteArray();
+                output.put(macAddress);
+                break;
             default:
                 throw new IllegalArgumentException("Unknown type:" + fieldInfo.annotation.type());
         }
diff --git a/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java b/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java
new file mode 100644
index 0000000..382912b
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/ConnectivityUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+
+import android.annotation.Nullable;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * Various utilities used in connectivity code.
+ */
+public final class ConnectivityUtils {
+    private ConnectivityUtils() {}
+
+
+    /**
+     * Return IP address and port in a string format.
+     */
+    public static String addressAndPortToString(InetAddress address, int port) {
+        return String.format(
+                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
+                        address.getHostAddress(), port);
+    }
+
+    /**
+     * Return true if the provided address is non-null and an IPv6 Unique Local Address (RFC4193).
+     */
+    public static boolean isIPv6ULA(@Nullable InetAddress addr) {
+        return addr instanceof Inet6Address
+                && ((addr.getAddress()[0] & 0xfe) == 0xfc);
+    }
+
+    /**
+     * Returns the {@code int} nearest in value to {@code value}.
+     *
+     * @param value any {@code long} value
+     * @return the same value cast to {@code int} if it is in the range of the {@code int}
+     * type, {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if
+     * it is too small
+     */
+    public static int saturatedCast(long value) {
+        if (value > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        if (value < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        return (int) value;
+    }
+}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 537340e..c00202e 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -9,6 +9,7 @@
     static_libs: [
         "net-utils-framework-common",
         "androidx.test.rules",
+        "mockito-target-extended-minus-junit4",
         "net-utils-device-common",
         "net-tests-utils-host-device-common",
     ],
@@ -29,6 +30,11 @@
     static_libs: [
         "NetworkStaticLibTestsLib",
     ],
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
 }
diff --git a/staticlibs/tests/unit/AndroidManifest.xml b/staticlibs/tests/unit/AndroidManifest.xml
index c747c8a..84a20a2 100644
--- a/staticlibs/tests/unit/AndroidManifest.xml
+++ b/staticlibs/tests/unit/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ConnectivityUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ConnectivityUtilsTest.java
new file mode 100644
index 0000000..8af0196
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ConnectivityUtilsTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.InetAddresses.parseNumericAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for ConnectivityUtils */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityUtilsTest {
+    @Test
+    public void testIsIPv6ULA() {
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00::")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00::1")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fc00:1234::5678")));
+        assertTrue(isIPv6ULA(parseNumericAddress("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        assertFalse(isIPv6ULA(parseNumericAddress("fe00::")));
+        assertFalse(isIPv6ULA(parseNumericAddress("2480:1248::123:456")));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
new file mode 100644
index 0000000..982dbe7
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+
+/**
+ * Tests for DeviceConfigUtils.
+ *
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DeviceConfigUtilsTest {
+    private static final String TEST_NAME_SPACE = "connectivity";
+    private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
+    private static final int TEST_FLAG_VALUE = 28;
+    private static final String TEST_FLAG_VALUE_STRING = "28";
+    private static final int TEST_DEFAULT_FLAG_VALUE = 0;
+    private static final int TEST_MAX_FLAG_VALUE = 1000;
+    private static final int TEST_MIN_FLAG_VALUE = 100;
+    private static final long TEST_PACKAGE_VERSION = 290000000;
+    private static final String TEST_PACKAGE_NAME = "test.package.name";
+    private MockitoSession mSession;
+
+    @Mock private Context mContext;
+    @Mock private PackageManager mPm;
+    @Mock private PackageInfo mPi;
+    @Mock private Resources mResources;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
+
+        final PackageInfo pi = new PackageInfo();
+        pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+
+        doReturn(mPm).when(mContext).getPackageManager();
+        doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
+        doReturn(pi).when(mPm).getPackageInfo(anyString(), anyInt());
+        doReturn(mResources).when(mContext).getResources();
+    }
+
+    @After
+    public void tearDown() {
+        mSession.finishMocking();
+        DeviceConfigUtils.sPackageVersion = -1L;
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_Null() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NotNull() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NormalValue() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_NullValue() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_OverMaximumValue() {
+        doReturn(Integer.toString(TEST_MAX_FLAG_VALUE + 10)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_EqualsMaximumValue() {
+        doReturn(Integer.toString(TEST_MAX_FLAG_VALUE)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_MAX_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_BelowMinimumValue() {
+        doReturn(Integer.toString(TEST_MIN_FLAG_VALUE - 10)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_DEFAULT_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyInt_EqualsMinimumValue() {
+        doReturn(Integer.toString(TEST_MIN_FLAG_VALUE)).when(() -> DeviceConfig.getProperty(
+                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertEquals(TEST_MIN_FLAG_VALUE, DeviceConfigUtils.getDeviceConfigPropertyInt(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
+                TEST_MAX_FLAG_VALUE /* maximum value */,
+                TEST_DEFAULT_FLAG_VALUE /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyBoolean_Null() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertFalse(DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                false /* default value */));
+    }
+
+    @Test
+    public void testGetDeviceConfigPropertyBoolean_NotNull() {
+        doReturn("true").when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertTrue(DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
+                false /* default value */));
+    }
+
+    @Test
+    public void testFeatureIsEnabled() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
+    }
+
+    @Test
+    public void testFeatureIsNotEnabled() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
+    }
+
+    @Test
+    public void testFeatureIsEnabledWithException() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
+    }
+
+    @Test
+    public void testFeatureIsEnabledCaching() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
+
+        // Package info is only queried once
+        verify(mContext, times(1)).getPackageManager();
+        verify(mContext, times(1)).getPackageName();
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testGetResBooleanConfig() {
+        final int someResId = 1234;
+        doReturn(true).when(mResources).getBoolean(someResId);
+        assertTrue(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+        doReturn(false).when(mResources).getBoolean(someResId);
+        assertFalse(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+        doThrow(new Resources.NotFoundException()).when(mResources).getBoolean(someResId);
+        assertFalse(DeviceConfigUtils.getResBooleanConfig(mContext, someResId, false));
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
new file mode 100644
index 0000000..e94d132
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.lang.IllegalArgumentException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkCapabilitiesUtilsTest {
+
+    @Test
+    fun testGetAccountingTransport() {
+        assertEquals(TRANSPORT_WIFI, getDisplayTransport(intArrayOf(TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_CELLULAR, getDisplayTransport(intArrayOf(TRANSPORT_CELLULAR)))
+        assertEquals(TRANSPORT_BLUETOOTH, getDisplayTransport(intArrayOf(TRANSPORT_BLUETOOTH)))
+        assertEquals(TRANSPORT_ETHERNET, getDisplayTransport(intArrayOf(TRANSPORT_ETHERNET)))
+        assertEquals(TRANSPORT_WIFI_AWARE, getDisplayTransport(intArrayOf(TRANSPORT_WIFI_AWARE)))
+
+        assertEquals(TRANSPORT_VPN, getDisplayTransport(
+                intArrayOf(TRANSPORT_VPN, TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_VPN, getDisplayTransport(
+                intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_VPN)))
+
+        assertEquals(TRANSPORT_WIFI, getDisplayTransport(
+                intArrayOf(TRANSPORT_ETHERNET, TRANSPORT_WIFI)))
+        assertEquals(TRANSPORT_ETHERNET, getDisplayTransport(
+                intArrayOf(TRANSPORT_ETHERNET, TRANSPORT_TEST)))
+
+        assertFailsWith(IllegalArgumentException::class) {
+            getDisplayTransport(intArrayOf())
+        }
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index 1b03e75..47b34b9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.IpPrefix;
+import android.net.MacAddress;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -663,4 +664,37 @@
     private ByteBuffer toByteBuffer(final String hexString) {
         return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
     }
+
+    static class MacAddressMessage extends Struct {
+        @Field(order = 0, type = Type.EUI48) final MacAddress mMac1;
+        @Field(order = 1, type = Type.EUI48) final MacAddress mMac2;
+
+        MacAddressMessage(final MacAddress mac1, final MacAddress mac2) {
+            this.mMac1 = mac1;
+            this.mMac2 = mac2;
+        }
+    }
+
+    @Test
+    public void testMacAddressType() {
+        final MacAddressMessage msg = doParsingMessageTest("001122334455" + "ffffffffffff",
+                MacAddressMessage.class, ByteOrder.BIG_ENDIAN);
+
+        assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.mMac1);
+        assertEquals(MacAddress.fromString("ff:ff:ff:ff:ff:ff"), msg.mMac2);
+
+        assertEquals(12, Struct.getSize(MacAddressMessage.class));
+        assertArrayEquals(toByteBuffer("001122334455" + "ffffffffffff").array(),
+                msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    static class BadMacAddressType extends Struct {
+        @Field(order = 0, type = Type.EUI48) byte[] mMac;
+    }
+
+    @Test
+    public void testIncorrectType_EUI48WithByteArray() {
+        assertThrows(IllegalArgumentException.class,
+                () -> Struct.parse(BadMacAddressType.class, toByteBuffer("ffffffffffff")));
+    }
 }