Merge "Move static utils to the static library" am: 0fdd516a82 am: f13acd2190 am: e0788758af am: d94a280ac0

Original change: https://android-review.googlesource.com/c/platform/frameworks/libs/net/+/1849358

Change-Id: I6e7e1bff7f6ca70ac723e0d46db4debe444dfddc
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index 8c79d2a..5cc011e 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -16,6 +16,7 @@
         "//frameworks/base/services:__subpackages__",
         "//frameworks/base/packages:__subpackages__",
         "//frameworks/libs/net/client-libs/tests:__subpackages__",
+        "//frameworks/libs/net/common:__subpackages__",
     ],
     libs: ["androidx.annotation_annotation"],
     static_libs: [
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
index 1f31912..e78e336 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -18,21 +18,39 @@
 
 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.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.List;
 
 /**
  * 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.
      */
@@ -102,4 +120,142 @@
                 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;
+    }
+
+    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/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index 306b1bb..dedda7e 100644
--- 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
@@ -16,15 +16,31 @@
 
 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.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;
@@ -43,6 +59,7 @@
     @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 {
@@ -99,5 +116,106 @@
         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);
+    }
+}