Merge client-libs/ from platform/frameworks/libs/net/ to staticlibs/client-libs/
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
new file mode 100644
index 0000000..cabfa06
--- /dev/null
+++ b/staticlibs/client-libs/Android.bp
@@ -0,0 +1,28 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "netd-client",
+    srcs: ["netd/**/*"],
+    sdk_version: "system_current",
+    min_sdk_version: "29",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+        "com.android.wifi"
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+        "//frameworks/base/services:__subpackages__",
+        "//frameworks/base/packages:__subpackages__",
+        "//frameworks/libs/net/client-libs/tests:__subpackages__",
+        "//frameworks/libs/net/common:__subpackages__",
+        "//packages/modules/Wifi/service:__subpackages__"
+    ],
+    libs: ["androidx.annotation_annotation"],
+    static_libs: [
+        "netd_aidl_interface-lateststable-java",
+        "netd_event_listener_interface-lateststable-java"
+    ]
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
new file mode 100644
index 0000000..4180732
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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.metrics.INetdEventListener;
+
+/**
+ * Base {@link INetdEventListener} that provides no-op implementations which can
+ * be overridden.
+ */
+public class BaseNetdEventListener extends INetdEventListener.Stub {
+
+    @Override
+    public void onDnsEvent(int netId, int eventType, int returnCode,
+            int latencyMs, String hostname, String[] ipAddresses,
+            int ipAddressesCount, int uid) { }
+
+    @Override
+    public void onPrivateDnsValidationEvent(int netId, String ipAddress,
+            String hostname, boolean validated) { }
+
+    @Override
+    public void onConnectEvent(int netId, int error, int latencyMs,
+            String ipAddr, int port, int uid) { }
+
+    @Override
+    public void onWakeupEvent(String prefix, int uid, int ethertype,
+            int ipNextHeader, byte[] dstHw, String srcIp, String dstIp,
+            int srcPort, int dstPort, long timestampNs) { }
+
+    @Override
+    public void onTcpSocketStatsEvent(int[] networkIds, int[] sentPackets,
+            int[] lostPackets, int[] rttUs, int[] sentAckDiffMs) { }
+
+    @Override
+    public void onNat64PrefixEvent(int netId, boolean added,
+            String prefixString, int prefixLength) { }
+
+    @Override
+    public int getInterfaceVersion() {
+        return INetdEventListener.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return INetdEventListener.HASH;
+    }
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java
new file mode 100644
index 0000000..526dd8b
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/BaseNetdUnsolicitedEventListener.java
@@ -0,0 +1,75 @@
+/*
+ * 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 android.net.INetdUnsolicitedEventListener;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base {@link INetdUnsolicitedEventListener} that provides no-op implementations which can be
+ * overridden.
+ */
+public class BaseNetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+
+    @Override
+    public void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs,
+            int uid) { }
+
+    @Override
+    public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceDnsServerInfo(@NonNull String ifName, long lifetimeS,
+            @NonNull String[] servers) { }
+
+    @Override
+    public void onInterfaceAddressUpdated(@NonNull String addr, String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAddressRemoved(@NonNull String addr, @NonNull String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAdded(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceRemoved(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onInterfaceLinkStateChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onRouteChanged(boolean updated, @NonNull String route, @NonNull String gateway,
+            @NonNull String ifName) { }
+
+    @Override
+    public void onStrictCleartextDetected(int uid, @NonNull String hex) { }
+
+    @Override
+    public int getInterfaceVersion() {
+        return INetdUnsolicitedEventListener.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return INetdUnsolicitedEventListener.HASH;
+    }
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
new file mode 100644
index 0000000..98fda56
--- /dev/null
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2021 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.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.system.OsConstants.EBUSY;
+
+import android.annotation.SuppressLint;
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.RouteInfo;
+import android.net.TetherConfigParcel;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Collection of utilities for netd.
+ */
+public class NetdUtils {
+    private static final String TAG = NetdUtils.class.getSimpleName();
+
+    /** Used to modify the specified route. */
+    public enum ModifyOperation {
+        ADD,
+        REMOVE,
+    }
+
+    /**
+     * Get InterfaceConfigurationParcel from netd.
+     */
+    public static InterfaceConfigurationParcel getInterfaceConfigParcel(@NonNull INetd netd,
+            @NonNull String iface) {
+        try {
+            return netd.interfaceGetCfg(iface);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static void validateFlag(String flag) {
+        if (flag.indexOf(' ') >= 0) {
+            throw new IllegalArgumentException("flag contains space: " + flag);
+        }
+    }
+
+    /**
+     * Check whether the InterfaceConfigurationParcel contains the target flag or not.
+     *
+     * @param config The InterfaceConfigurationParcel instance.
+     * @param flag Target flag string to be checked.
+     */
+    public static boolean hasFlag(@NonNull final InterfaceConfigurationParcel config,
+            @NonNull final String flag) {
+        validateFlag(flag);
+        final Set<String> flagList = new HashSet<String>(Arrays.asList(config.flags));
+        return flagList.contains(flag);
+    }
+
+    @VisibleForTesting
+    protected static String[] removeAndAddFlags(@NonNull String[] flags, @NonNull String remove,
+            @NonNull String add) {
+        final ArrayList<String> result = new ArrayList<>();
+        try {
+            // Validate the add flag first, so that the for-loop can be ignore once the format of
+            // add flag is invalid.
+            validateFlag(add);
+            for (String flag : flags) {
+                // Simply ignore both of remove and add flags first, then add the add flag after
+                // exiting the loop to prevent adding the duplicate flag.
+                if (remove.equals(flag) || add.equals(flag)) continue;
+                result.add(flag);
+            }
+            result.add(add);
+            return result.toArray(new String[result.size()]);
+        } catch (IllegalArgumentException iae) {
+            throw new IllegalStateException("Invalid InterfaceConfigurationParcel", iae);
+        }
+    }
+
+    /**
+     * Set interface configuration to netd by passing InterfaceConfigurationParcel.
+     */
+    public static void setInterfaceConfig(INetd netd, InterfaceConfigurationParcel configParcel) {
+        try {
+            netd.interfaceSetCfg(configParcel);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Set the given interface up.
+     */
+    public static void setInterfaceUp(INetd netd, String iface) {
+        final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
+        configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_DOWN /* remove */,
+                IF_STATE_UP /* add */);
+        setInterfaceConfig(netd, configParcel);
+    }
+
+    /**
+     * Set the given interface down.
+     */
+    public static void setInterfaceDown(INetd netd, String iface) {
+        final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
+        configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_UP /* remove */,
+                IF_STATE_DOWN /* add */);
+        setInterfaceConfig(netd, configParcel);
+    }
+
+    /** Start tethering. */
+    public static void tetherStart(final INetd netd, final boolean usingLegacyDnsProxy,
+            final String[] dhcpRange) throws RemoteException, ServiceSpecificException {
+        final TetherConfigParcel config = new TetherConfigParcel();
+        config.usingLegacyDnsProxy = usingLegacyDnsProxy;
+        config.dhcpRanges = dhcpRange;
+        netd.tetherStartWithConfiguration(config);
+    }
+
+    /** Setup interface for tethering. */
+    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
+            throws RemoteException, ServiceSpecificException {
+        tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+    }
+
+    /** Setup interface with configurable retries for tethering. */
+    public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
+            int maxAttempts, int pollingIntervalMs)
+            throws RemoteException, ServiceSpecificException {
+        netd.tetherInterfaceAdd(iface);
+        networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+        List<RouteInfo> routes = new ArrayList<>();
+        routes.add(new RouteInfo(dest, null, iface, RTN_UNICAST));
+        addRoutesToLocalNetwork(netd, iface, routes);
+    }
+
+    /**
+     * Retry Netd#networkAddInterface for EBUSY error code.
+     * If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
+     * There can be a race where puts the interface into the local network but interface is still
+     * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
+     * See b/158269544 for detail.
+     */
+    private static void networkAddInterface(final INetd netd, final String iface,
+            int maxAttempts, int pollingIntervalMs)
+            throws ServiceSpecificException, RemoteException {
+        for (int i = 1; i <= maxAttempts; i++) {
+            try {
+                netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+                return;
+            } catch (ServiceSpecificException e) {
+                if (e.errorCode == EBUSY && i < maxAttempts) {
+                    SystemClock.sleep(pollingIntervalMs);
+                    continue;
+                }
+
+                Log.e(TAG, "Retry Netd#networkAddInterface failure: " + e);
+                throw e;
+            }
+        }
+    }
+
+    /** Reset interface for tethering. */
+    public static void untetherInterface(final INetd netd, String iface)
+            throws RemoteException, ServiceSpecificException {
+        try {
+            netd.tetherInterfaceRemove(iface);
+        } finally {
+            netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+        }
+    }
+
+    /** Add |routes| to local network. */
+    public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+            final List<RouteInfo> routes) {
+
+        for (RouteInfo route : routes) {
+            if (!route.isDefaultRoute()) {
+                modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+            }
+        }
+
+        // IPv6 link local should be activated always.
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
+    }
+
+    /** Remove routes from local network. */
+    public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+        int failures = 0;
+
+        for (RouteInfo route : routes) {
+            try {
+                modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+            } catch (IllegalStateException e) {
+                failures++;
+            }
+        }
+
+        return failures;
+    }
+
+    @SuppressLint("NewApi")
+    private static String findNextHop(final RouteInfo route) {
+        final String nextHop;
+        switch (route.getType()) {
+            case RTN_UNICAST:
+                if (route.hasGateway()) {
+                    nextHop = route.getGateway().getHostAddress();
+                } else {
+                    nextHop = INetd.NEXTHOP_NONE;
+                }
+                break;
+            case RTN_UNREACHABLE:
+                nextHop = INetd.NEXTHOP_UNREACHABLE;
+                break;
+            case RTN_THROW:
+                nextHop = INetd.NEXTHOP_THROW;
+                break;
+            default:
+                nextHop = INetd.NEXTHOP_NONE;
+                break;
+        }
+        return nextHop;
+    }
+
+    /** Add or remove |route|. */
+    public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+            final RouteInfo route) {
+        final String ifName = route.getInterface();
+        final String dst = route.getDestination().toString();
+        final String nextHop = findNextHop(route);
+
+        try {
+            switch(op) {
+                case ADD:
+                    netd.networkAddRoute(netId, ifName, dst, nextHop);
+                    break;
+                case REMOVE:
+                    netd.networkRemoveRoute(netId, ifName, dst, nextHop);
+                    break;
+                default:
+                    throw new IllegalStateException("Unsupported modify operation:" + op);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
new file mode 100644
index 0000000..220a6c1
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -0,0 +1,44 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "NetdStaticLibTestsLib",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    min_sdk_version: "29",
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-extended-minus-junit4",
+        "net-tests-utils-host-device-common",
+        "netd-client",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    visibility: [
+        // Visible for Tethering and NetworkStack integration test and link NetdStaticLibTestsLib
+        // there, so that the tests under client-libs can also be run when running tethering and
+        // NetworkStack MTS.
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+        "//packages/modules/NetworkStack/tests/integration",
+    ]
+}
+
+android_test {
+    name: "NetdStaticLibTests",
+    certificate: "platform",
+    static_libs: [
+        "NetdStaticLibTestsLib",
+    ],
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    test_suites: ["device-tests"],
+}
diff --git a/staticlibs/client-libs/tests/unit/AndroidManifest.xml b/staticlibs/client-libs/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a07d3d
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.clientlibs.tests">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.clientlibs.tests"
+        android:label="Netd Static Library Tests" />
+</manifest>
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
new file mode 100644
index 0000000..5069672
--- /dev/null
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2021 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.INetd.LOCAL_NET_ID;
+import static android.system.OsConstants.EBUSY;
+
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetdUtilsTest {
+    @Mock private INetd mNetd;
+
+    private static final String IFACE = "TEST_IFACE";
+    private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private void setupFlagsForInterfaceConfiguration(String[] flags) throws Exception {
+        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
+        config.flags = flags;
+        when(mNetd.interfaceGetCfg(eq(IFACE))).thenReturn(config);
+    }
+
+    private void verifyMethodsAndArgumentsOfSetInterface(boolean ifaceUp) throws Exception {
+        final String[] flagsContainDownAndUp = new String[] {"flagA", "down", "flagB", "up"};
+        final String[] flagsForInterfaceDown = new String[] {"flagA", "down", "flagB"};
+        final String[] flagsForInterfaceUp = new String[] {"flagA", "up", "flagB"};
+        final String[] expectedFinalFlags;
+        setupFlagsForInterfaceConfiguration(flagsContainDownAndUp);
+        if (ifaceUp) {
+            // "down" flag will be removed from flagsContainDownAndUp when interface is up. Set
+            // expectedFinalFlags to flagsForInterfaceUp.
+            expectedFinalFlags = flagsForInterfaceUp;
+            NetdUtils.setInterfaceUp(mNetd, IFACE);
+        } else {
+            // "up" flag will be removed from flagsContainDownAndUp when interface is down. Set
+            // expectedFinalFlags to flagsForInterfaceDown.
+            expectedFinalFlags = flagsForInterfaceDown;
+            NetdUtils.setInterfaceDown(mNetd, IFACE);
+        }
+        verify(mNetd).interfaceSetCfg(
+                argThat(config ->
+                        // Check if actual flags are the same as expected flags.
+                        // TODO: Have a function in MiscAsserts to check if two arrays are the same.
+                        CollectionUtils.all(Arrays.asList(expectedFinalFlags),
+                                flag -> Arrays.asList(config.flags).contains(flag))
+                        && CollectionUtils.all(Arrays.asList(config.flags),
+                                flag -> Arrays.asList(expectedFinalFlags).contains(flag))));
+    }
+
+    @Test
+    public void testSetInterfaceUp() throws Exception {
+        verifyMethodsAndArgumentsOfSetInterface(true /* ifaceUp */);
+    }
+
+    @Test
+    public void testSetInterfaceDown() throws Exception {
+        verifyMethodsAndArgumentsOfSetInterface(false /* ifaceUp */);
+    }
+
+    @Test
+    public void testRemoveAndAddFlags() throws Exception {
+        final String[] flags = new String[] {"flagA", "down", "flagB"};
+        // Add an invalid flag and expect to get an IllegalStateException.
+        assertThrows(IllegalStateException.class,
+                () -> NetdUtils.removeAndAddFlags(flags, "down" /* remove */, "u p" /* add */));
+    }
+
+    private void setNetworkAddInterfaceOutcome(final Exception cause, int numLoops)
+            throws Exception {
+        // This cannot be an int because local variables referenced from a lambda expression must
+        // be final or effectively final.
+        final Counter myCounter = new Counter();
+        doAnswer((invocation) -> {
+            myCounter.count();
+            if (myCounter.isCounterReached(numLoops)) {
+                if (cause == null) return null;
+
+                throw cause;
+            }
+
+            throw new ServiceSpecificException(EBUSY);
+        }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+    }
+
+    class Counter {
+        private int mValue = 0;
+
+        private void count() {
+            mValue++;
+        }
+
+        private boolean isCounterReached(int target) {
+            return mValue >= target;
+        }
+    }
+
+    @Test
+    public void testTetherInterfaceSuccessful() throws Exception {
+        // Expect #networkAddInterface successful at first tries.
+        verifyTetherInterfaceSucceeds(1);
+
+        // Expect #networkAddInterface successful after 10 tries.
+        verifyTetherInterfaceSucceeds(10);
+    }
+
+    private void runTetherInterfaceWithServiceSpecificException(int expectedTries,
+            int expectedCode) throws Exception {
+        setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
+
+        try {
+            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            fail("Expect throw ServiceSpecificException");
+        } catch (ServiceSpecificException e) {
+            assertEquals(e.errorCode, expectedCode);
+        }
+
+        verifyNetworkAddInterfaceFails(expectedTries);
+        reset(mNetd);
+    }
+
+    private void runTetherInterfaceWithRemoteException(int expectedTries) throws Exception {
+        setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
+
+        try {
+            NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+            fail("Expect throw RemoteException");
+        } catch (RemoteException e) { }
+
+        verifyNetworkAddInterfaceFails(expectedTries);
+        reset(mNetd);
+    }
+
+    private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
+        verify(mNetd).tetherInterfaceAdd(IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+        verifyNoMoreInteractions(mNetd);
+    }
+
+    private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+        setNetworkAddInterfaceOutcome(null, expectedTries);
+
+        NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+        verify(mNetd).tetherInterfaceAdd(IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+        verifyNoMoreInteractions(mNetd);
+        reset(mNetd);
+    }
+
+    @Test
+    public void testTetherInterfaceFailOnNetworkAddInterface() throws Exception {
+        // Test throwing ServiceSpecificException with EBUSY failure.
+        runTetherInterfaceWithServiceSpecificException(20, EBUSY);
+
+        // Test throwing ServiceSpecificException with unexpectedError.
+        final int unexpectedError = 999;
+        runTetherInterfaceWithServiceSpecificException(1, unexpectedError);
+
+        // Test throwing ServiceSpecificException with unexpectedError after 7 tries.
+        runTetherInterfaceWithServiceSpecificException(7, unexpectedError);
+
+        // Test throwing RemoteException.
+        runTetherInterfaceWithRemoteException(1);
+
+        // Test throwing RemoteException after 3 tries.
+        runTetherInterfaceWithRemoteException(3);
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertTrue(NetdUtils.hasFlag(p, "up"));
+        assertTrue(NetdUtils.hasFlag(p, "running"));
+        assertTrue(NetdUtils.hasFlag(p, "broadcast"));
+        assertTrue(NetdUtils.hasFlag(p, "multicast"));
+        assertFalse(NetdUtils.hasFlag(p, "down"));
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag_flagContainsSpace() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertThrows(IllegalArgumentException.class, () -> NetdUtils.hasFlag(p, "up "));
+    }
+
+    @Test
+    public void testNetdUtilsHasFlag_UppercaseString() throws Exception {
+        final String[] flags = new String[] {"up", "broadcast", "running", "multicast"};
+        setupFlagsForInterfaceConfiguration(flags);
+
+        // Set interface up.
+        NetdUtils.setInterfaceUp(mNetd, IFACE);
+        final ArgumentCaptor<InterfaceConfigurationParcel> arg =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+        verify(mNetd, times(1)).interfaceSetCfg(arg.capture());
+
+        final InterfaceConfigurationParcel p = arg.getValue();
+        assertFalse(NetdUtils.hasFlag(p, "UP"));
+        assertFalse(NetdUtils.hasFlag(p, "BROADCAST"));
+        assertFalse(NetdUtils.hasFlag(p, "RUNNING"));
+        assertFalse(NetdUtils.hasFlag(p, "MULTICAST"));
+    }
+}