Merge "Fix the mStopTimeNs not be reset in Stopwatch#Restart"
diff --git a/common/moduleutils/src/android/net/shared/NetdUtils.java b/common/moduleutils/src/android/net/shared/NetdUtils.java
index 0137bb7..5fa29c9 100644
--- a/common/moduleutils/src/android/net/shared/NetdUtils.java
+++ b/common/moduleutils/src/android/net/shared/NetdUtils.java
@@ -17,6 +17,7 @@
 package android.net.shared;
 
 import static android.net.RouteInfo.RTN_UNICAST;
+import static android.system.OsConstants.EBUSY;
 
 import android.net.INetd;
 import android.net.IpPrefix;
@@ -24,6 +25,8 @@
 import android.net.TetherConfigParcel;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -33,6 +36,8 @@
  * @hide
  */
 public class NetdUtils {
+    private static final String TAG = NetdUtils.class.getSimpleName();
+
     /** Start tethering. */
     public static void tetherStart(final INetd netd, final boolean usingLegacyDnsProxy,
             final String[] dhcpRange) throws RemoteException, ServiceSpecificException {
@@ -45,14 +50,46 @@
     /** Setup interface for tethering. */
     public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
             throws RemoteException, ServiceSpecificException {
-        netd.tetherInterfaceAdd(iface);
+        tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+    }
 
-        netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+    /** 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));
         RouteUtils.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 {
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 1591f43..c4f46ae 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -389,6 +389,9 @@
     private static final int CMD_COMPLETE_PRECONNECTION = 16;
     private static final int CMD_UPDATE_L2INFORMATION = 17;
 
+    private static final int ARG_LINKPROP_CHANGED_LINKSTATE_DOWN = 0;
+    private static final int ARG_LINKPROP_CHANGED_LINKSTATE_UP = 1;
+
     // Internal commands to use instead of trying to call transitionTo() inside
     // a given State's enter() method. Calling transitionTo() from enter/exit
     // encounters a Log.wtf() that can cause trouble on eng builds.
@@ -596,7 +599,9 @@
         mLinkObserver = new IpClientLinkObserver(
                 mContext, getHandler(),
                 mInterfaceName,
-                () -> sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED),
+                (ifaceUp) -> sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED, ifaceUp
+                        ? ARG_LINKPROP_CHANGED_LINKSTATE_UP
+                        : ARG_LINKPROP_CHANGED_LINKSTATE_DOWN),
                 config, mLog) {
             @Override
             public void onInterfaceAdded(String iface) {
@@ -2053,12 +2058,14 @@
 
                 case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                     // EVENT_NETLINK_LINKPROPERTIES_CHANGED message will be received in both of
-                    // provisioning loss and normal user termination case (e.g. turn off wifi or
-                    // switch to another wifi ssid), hence, checking current interface change
-                    // status (down or up) would help distinguish.
-                    final boolean ifUp = (msg.arg1 != 0);
+                    // provisioning loss and normal user termination cases (e.g. turn off wifi or
+                    // switch to another wifi ssid), hence, checking the current interface link
+                    // state (down or up) helps distinguish the two cases: if the link state is
+                    // down, provisioning is only lost because the link is being torn down (for
+                    // example when turning off wifi), so treat it as a normal termination.
                     if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
-                        transitionToStoppingState(ifUp ? DisconnectCode.DC_PROVISIONING_FAIL
+                        final boolean linkStateUp = (msg.arg1 == ARG_LINKPROP_CHANGED_LINKSTATE_UP);
+                        transitionToStoppingState(linkStateUp ? DisconnectCode.DC_PROVISIONING_FAIL
                                 : DisconnectCode.DC_NORMAL_TERMINATION);
                     }
                     break;
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index dcbca94..46b3844 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -89,8 +89,13 @@
     public interface Callback {
         /**
          * Called when some properties of the link were updated.
+         *
+         * @param linkState Whether the interface link state is up as per the latest
+         *                  {@link #onInterfaceLinkStateChanged(String, boolean)} callback. This
+         *                  should only be used for metrics purposes, as it could be inconsistent
+         *                  with {@link #getLinkProperties()} in particular.
          */
-        void update();
+        void update(boolean linkState);
     }
 
     /** Configuration parameters for IpClientLinkObserver. */
@@ -105,6 +110,7 @@
     private final String mInterfaceName;
     private final Callback mCallback;
     private final LinkProperties mLinkProperties;
+    private boolean mInterfaceLinkState;
     private DnsServerRepository mDnsServerRepository;
     private final AlarmManager mAlarmManager;
     private final Configuration mConfig;
@@ -121,6 +127,7 @@
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
         mConfig = config;
+        mInterfaceLinkState = true; // Assume up by default
         mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
@@ -149,7 +156,15 @@
             // interface-specific messages (e.g., RTM_DELADDR) will not reach us, because the netd
             // code that parses them will not be able to resolve the ifindex to an interface name.
             clearLinkProperties();
-            mCallback.update();
+            mCallback.update(getInterfaceLinkState());
+        }
+    }
+
+    @Override
+    public void onInterfaceLinkStateChanged(String iface, boolean state) {
+        if (mInterfaceName.equals(iface)) {
+            maybeLog("interfaceLinkStateChanged", iface + (state ? " up" : " down"));
+            setInterfaceLinkState(state);
         }
     }
 
@@ -162,7 +177,7 @@
                 changed = mLinkProperties.addLinkAddress(address);
             }
             if (changed) {
-                mCallback.update();
+                mCallback.update(getInterfaceLinkState());
             }
         }
     }
@@ -176,7 +191,7 @@
                 changed = mLinkProperties.removeLinkAddress(address);
             }
             if (changed) {
-                mCallback.update();
+                mCallback.update(getInterfaceLinkState());
             }
         }
     }
@@ -190,7 +205,7 @@
                 changed = mLinkProperties.addRoute(route);
             }
             if (changed) {
-                mCallback.update();
+                mCallback.update(getInterfaceLinkState());
             }
         }
     }
@@ -204,7 +219,7 @@
                 changed = mLinkProperties.removeRoute(route);
             }
             if (changed) {
-                mCallback.update();
+                mCallback.update(getInterfaceLinkState());
             }
         }
     }
@@ -218,7 +233,7 @@
                 synchronized (this) {
                     mDnsServerRepository.setDnsServersOn(mLinkProperties);
                 }
-                mCallback.update();
+                mCallback.update(getInterfaceLinkState());
             }
         }
     }
@@ -243,6 +258,14 @@
         mLinkProperties.setInterfaceName(mInterfaceName);
     }
 
+    private synchronized boolean getInterfaceLinkState() {
+        return mInterfaceLinkState;
+    }
+
+    private synchronized void setInterfaceLinkState(boolean state) {
+        mInterfaceLinkState = state;
+    }
+
     /** Notifies this object of new interface parameters. */
     public void setInterfaceParams(InterfaceParams params) {
         mNetlinkMonitor.setIfindex(params.index);
@@ -355,7 +378,7 @@
                 cancelPref64Alarm();
             }
 
-            mCallback.update();
+            mCallback.update(getInterfaceLinkState());
         }
 
         private void processPref64Option(StructNdOptPref64 opt, final long now) {
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 63aedab..5fdd219 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -411,6 +411,7 @@
     private String mPrivateDnsProviderHostname = "";
 
     private final Context mContext;
+    private final Context mCustomizedContext;
     private final INetworkMonitorCallbacks mCallback;
     private final int mCallbackVersion;
     private final Network mCleartextDnsNetwork;
@@ -548,6 +549,7 @@
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         mNotifier = serviceManager.getNotifier();
+        mCustomizedContext = getCustomizedContextOrDefault();
 
         // CHECKSTYLE:OFF IndentationCheck
         addState(mDefaultState);
@@ -1824,10 +1826,10 @@
     private String getCaptivePortalServerHttpsUrl() {
         final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
         if (isValidTestUrl(testUrl)) return testUrl;
-        final Context targetContext = getCustomizedContextOrDefault();
-        return getSettingFromResource(targetContext,
+        return getSettingFromResource(mCustomizedContext,
                 R.string.config_captive_portal_https_url, CAPTIVE_PORTAL_HTTPS_URL,
-                targetContext.getResources().getString(R.string.default_captive_portal_https_url));
+                mCustomizedContext.getResources().getString(
+                R.string.default_captive_portal_https_url));
     }
 
     private static boolean isValidTestUrl(@Nullable String url) {
@@ -1917,10 +1919,10 @@
     public String getCaptivePortalServerHttpUrl() {
         final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
         if (isValidTestUrl(testUrl)) return testUrl;
-        final Context targetContext = getCustomizedContextOrDefault();
-        return getSettingFromResource(targetContext,
+        return getSettingFromResource(mCustomizedContext,
                 R.string.config_captive_portal_http_url, CAPTIVE_PORTAL_HTTP_URL,
-                targetContext.getResources().getString(R.string.default_captive_portal_http_url));
+                mCustomizedContext.getResources().getString(
+                R.string.default_captive_portal_http_url));
     }
 
     private int getConsecutiveDnsTimeoutThreshold() {
@@ -2078,7 +2080,7 @@
      */
     private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
             @ArrayRes int defaultResId, @NonNull Function<String, T> resourceConverter) {
-        final Resources res = getCustomizedContextOrDefault().getResources();
+        final Resources res = mCustomizedContext.getResources();
         return getProbeUrlArrayConfig(providerValue, configResId, res.getStringArray(defaultResId),
                 resourceConverter);
     }
@@ -2096,7 +2098,7 @@
      */
     private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
             String[] defaultConfig, @NonNull Function<String, T> resourceConverter) {
-        final Resources res = getCustomizedContextOrDefault().getResources();
+        final Resources res = mCustomizedContext.getResources();
         String[] configValue = res.getStringArray(configResId);
 
         if (configValue.length == 0) {
diff --git a/tests/unit/src/android/net/shared/NetdUtilsTest.java b/tests/unit/src/android/net/shared/NetdUtilsTest.java
new file mode 100644
index 0000000..f3ef53a
--- /dev/null
+++ b/tests/unit/src/android/net/shared/NetdUtilsTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2016 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 android.net.shared;
+
+import static android.net.INetd.LOCAL_NET_ID;
+import static android.system.OsConstants.EBUSY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.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 android.net.INetd;
+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.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class NetdUtilsTest {
+    private static final String IFACE_NAME = "testnet1";
+    private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+
+    @Mock private INetd mNetd;
+
+    @Before public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    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_NAME);
+    }
+
+    class Counter {
+        private int mValue = 0;
+
+        private void count() {
+            mValue++;
+        }
+
+        private boolean isCounterReached(int target) {
+            return mValue >= target;
+        }
+    }
+
+    private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+        setNetworkAddInterfaceOutcome(null, expectedTries);
+
+        NetdUtils.tetherInterface(mNetd, IFACE_NAME, TEST_IPPREFIX);
+        verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE_NAME);
+        verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE_NAME), any(), any());
+        verifyNoMoreInteractions(mNetd);
+        reset(mNetd);
+    }
+
+    @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_NAME, 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_NAME, 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_NAME);
+        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE_NAME);
+        verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+        verifyNoMoreInteractions(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);
+    }
+}
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 4b20015..bbba171 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -927,6 +927,18 @@
         return info;
     }
 
+    private void setupNoSimCardNeighborMcc() throws Exception {
+        // Enable using neighbor resource by camping mcc feature.
+        doReturn(true).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
+        final List<CellInfo> cellList = new ArrayList<CellInfo>();
+        final int testMcc = 460;
+        cellList.add(makeTestCellInfoGsm(Integer.toString(testMcc)));
+        doReturn(cellList).when(mTelephony).getAllCellInfo();
+        final Configuration config = mResources.getConfiguration();
+        config.mcc = testMcc;
+        doReturn(mMccContext).when(mContext).createConfigurationContext(eq(config));
+    }
+
     @Test
     public void testMakeFallbackUrls() throws Exception {
         final WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
@@ -950,21 +962,28 @@
         assertEquals("http://testUrl1.com", urls[0].toString());
         assertEquals("http://testUrl2.com", urls[1].toString());
 
-        // Value is expected to be replaced by location resource.
-        doReturn(true).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
-
-        final List<CellInfo> cellList = new ArrayList<CellInfo>();
-        final int testMcc = 460;
-        cellList.add(makeTestCellInfoGsm(Integer.toString(testMcc)));
-        doReturn(cellList).when(mTelephony).getAllCellInfo();
-        final Configuration config = mResources.getConfiguration();
-        config.mcc = testMcc;
-        doReturn(mMccContext).when(mContext).createConfigurationContext(eq(config));
+        // Even though the using neighbor resource by camping mcc feature is enabled, the
+        // customized context has been assigned and won't change. So calling
+        // makeCaptivePortalFallbackUrls() still gets the original value.
+        setupNoSimCardNeighborMcc();
         doReturn(new String[] {"http://testUrl3.com"}).when(mMccResource)
                 .getStringArray(R.array.config_captive_portal_fallback_urls);
         urls = wnm.makeCaptivePortalFallbackUrls();
+        assertEquals(urls.length, 2);
+        assertEquals("http://testUrl1.com", urls[0].toString());
+        assertEquals("http://testUrl2.com", urls[1].toString());
+    }
+
+    @Test
+    public void testMakeFallbackUrlsWithCustomizedContext() throws Exception {
+        // Value is expected to be replaced by location resource.
+        setupNoSimCardNeighborMcc();
+        doReturn(new String[] {"http://testUrl.com"}).when(mMccResource)
+                .getStringArray(R.array.config_captive_portal_fallback_urls);
+        final WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        final URL[] urls = wnm.makeCaptivePortalFallbackUrls();
         assertEquals(urls.length, 1);
-        assertEquals("http://testUrl3.com", urls[0].toString());
+        assertEquals("http://testUrl.com", urls[0].toString());
     }
 
     private static CellIdentityGsm makeCellIdentityGsm(int lac, int cid, int arfcn, int bsic,