Merge "Increase tcp polling interval on data stall detection" into rvc-dev
diff --git a/Android.bp b/Android.bp
index 04b1062..9f572ad 100644
--- a/Android.bp
+++ b/Android.bp
@@ -87,7 +87,7 @@
     libs: ["unsupportedappusage"],
     static_libs: [
         "androidx.annotation_annotation",
-        "netd_aidl_interface-V3-java",
+        "netd_aidl_interface-java",
         "netlink-client",
         "networkstack-client",
         "datastallprotosnano",
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 5564503..4404273 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -503,9 +503,9 @@
      * check whether or not to support caching the last lease info and INIT-REBOOT state.
      *
      * INIT-REBOOT state is supported on Android R by default if there is no experiment flag set to
-     * disable this feature explicitly, meanwhile we still hope to be able to control this feature
-     * on/off by pushing experiment flag for A/B testing and metrics collection on both of Android
-     * Q and R version, however it's disbled on Android Q by default.
+     * disable this feature explicitly, meanwhile turning this feature on/off by pushing experiment
+     * flag makes it possible to do A/B test and metrics collection on both of Android Q and R, but
+     * it's disabled on Android Q by default.
      */
     public boolean isDhcpLeaseCacheEnabled() {
         final boolean defaultEnabled =
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 018d6ab..629d216 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -482,6 +482,7 @@
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
     private MacAddress mCurrentBssid;
+    private boolean mHasDisabledIPv6OnProvLoss;
 
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
@@ -1137,9 +1138,9 @@
         // Note that we can still be disconnected by IpReachabilityMonitor
         // if the IPv6 default gateway (but not the IPv6 DNS servers; see
         // accompanying code in IpReachabilityMonitor) is unreachable.
-        final boolean ignoreIPv6ProvisioningLoss =
-                mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
-                && !mCm.shouldAvoidBadWifi();
+        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledIPv6OnProvLoss
+                || (mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
+                        && !mCm.shouldAvoidBadWifi());
 
         // Additionally:
         //
@@ -1163,7 +1164,23 @@
         // IPv6 default route then also consider the loss of that default route
         // to be a loss of provisioning. See b/27962810.
         if (oldLp.hasGlobalIpv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
-            delta = PROV_CHANGE_LOST_PROVISIONING;
+            // Although link properties have lost IPv6 default route in this case, if IPv4 is still
+            // working with appropriate routes and DNS servers, we can keep the current connection
+            // without disconnecting from the network, just disable IPv6 on that given network until
+            // to the next provisioning. Disabling IPv6 will result in all IPv6 connectivity torn
+            // down and all IPv6 sockets being closed, the non-routable IPv6 DNS servers will be
+            // stripped out, so applications will be able to reconnect immediately over IPv4. See
+            // b/131781810.
+            if (newLp.isIpv4Provisioned()) {
+                mInterfaceCtrl.disableIPv6();
+                mHasDisabledIPv6OnProvLoss = true;
+                delta = PROV_CHANGE_STILL_PROVISIONED;
+                if (DBG) {
+                    mLog.log("Disable IPv6 stack completely when the default router has gone");
+                }
+            } else {
+                delta = PROV_CHANGE_LOST_PROVISIONING;
+            }
         }
 
         return delta;
@@ -1591,6 +1608,7 @@
         @Override
         public void enter() {
             stopAllIP();
+            mHasDisabledIPv6OnProvLoss = false;
 
             mLinkObserver.clearInterfaceParams();
             resetLinkProperties();
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index d0f62b2..8493816 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -792,6 +792,11 @@
                     return HANDLED;
                 case CMD_FORCE_REEVALUATION:
                 case CMD_CAPTIVE_PORTAL_RECHECK:
+                    if (getCurrentState() == mDefaultState) {
+                        // Before receiving CMD_NETWORK_CONNECTED (when still in mDefaultState),
+                        // requests to reevaluate are not valid: drop them.
+                        return HANDLED;
+                    }
                     String msg = "Forcing reevaluation for UID " + message.arg1;
                     final DnsStallDetector dsd = getDnsStallDetector();
                     if (dsd != null) {
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
index c6eb631..53297fe 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
@@ -171,6 +171,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -994,7 +995,7 @@
             assertEquals(5, packetList.size());
             assertArpProbe(packetList.get(0));
             assertArpAnnounce(packetList.get(3));
-
+        } else {
             verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
             assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
                     TEST_DEFAULT_MTU);
@@ -1245,11 +1246,11 @@
         fail("No router solicitation received on interface within timeout");
     }
 
-    private void sendBasicRouterAdvertisement(boolean waitForRs) throws Exception {
+    private void sendRouterAdvertisement(boolean waitForRs, short lifetime) throws Exception {
         final String dnsServer = "2001:4860:4860::64";
         final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
         ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
-        ByteBuffer ra = buildRaPacket(pio, rdnss);
+        ByteBuffer ra = buildRaPacket(lifetime, pio, rdnss);
 
         if (waitForRs) {
             waitForRouterSolicitation();
@@ -1258,6 +1259,14 @@
         mPacketReader.sendResponse(ra);
     }
 
+    private void sendBasicRouterAdvertisement(boolean waitForRs) throws Exception {
+        sendRouterAdvertisement(waitForRs, (short) 1800);
+    }
+
+    private void sendRouterAdvertisementWithZeroLifetime() throws Exception {
+        sendRouterAdvertisement(false /* waitForRs */, (short) 0);
+    }
+
     // TODO: move this and the following method to a common location and use them in ApfTest.
     private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
             throws Exception {
@@ -1319,7 +1328,8 @@
         return checksumAdjust(checksum, (short) IPPROTO_TCP, (short) IPPROTO_ICMPV6);
     }
 
-    private static ByteBuffer buildRaPacket(ByteBuffer... options) throws Exception {
+    private static ByteBuffer buildRaPacket(short lifetime, ByteBuffer... options)
+            throws Exception {
         final MacAddress srcMac = MacAddress.fromString("33:33:00:00:00:01");
         final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06");
         final byte[] routerLinkLocal = InetAddresses.parseNumericAddress("fe80::1").getAddress();
@@ -1347,7 +1357,7 @@
         packet.putShort((short) 0);                      // Checksum, TBD
         packet.put((byte) 0);                            // Hop limit, unspecified
         packet.put((byte) 0);                            // M=0, O=0
-        packet.putShort((short) 1800);                   // Router lifetime
+        packet.putShort(lifetime);                       // Router lifetime
         packet.putInt(0);                                // Reachable time, unspecified
         packet.putInt(100);                              // Retrans time 100ms.
 
@@ -1367,6 +1377,10 @@
         return packet;
     }
 
+    private static ByteBuffer buildRaPacket(ByteBuffer... options) throws Exception {
+        return buildRaPacket((short) 1800, options);
+    }
+
     private void disableIpv6ProvisioningDelays() throws Exception {
         // Speed up the test by disabling DAD and removing router_solicitation_delay.
         // We don't need to restore the default value because the interface is removed in tearDown.
@@ -2153,4 +2167,73 @@
         doDhcpRoamingTest(true /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                 TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, true /* expectRoaming */);
     }
+
+    private void doDualStackProvisioning() throws Exception {
+        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
+
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .build();
+        // Accelerate DHCP handshake to shorten test duration, not strictly necessary.
+        mDependencies.setDhcpRapidCommitEnabled(true);
+        mIpc.startProvisioning(config);
+
+        final InOrder inOrder = inOrder(mCb);
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+        final String dnsServer = "2001:4860:4860::64";
+        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
+        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
+        final ByteBuffer ra = buildRaPacket(pio, rdnss);
+
+        doIpv6OnlyProvisioning(inOrder, ra);
+
+        // Start IPv4 provisioning and wait until entire provisioning completes.
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(x -> {
+            if (!x.isIpv4Provisioned() || !x.isIpv6Provisioned()) return false;
+            lpFuture.complete(x);
+            return true;
+        }));
+
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
+        assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
+
+        reset(mCb);
+    }
+
+    @Test
+    public void testIgnoreIpv6ProvisioningLoss() throws Exception {
+        doDualStackProvisioning();
+
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+
+        // Send RA with 0-lifetime and wait until all IPv6-related default route and DNS servers
+        // have been removed, then verify if there is IPv4-only info left in the LinkProperties.
+        sendRouterAdvertisementWithZeroLifetime();
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
+                argThat(x -> {
+                    final boolean isOnlyIPv4Provisioned = (x.getLinkAddresses().size() == 1
+                            && x.getDnsServers().size() == 1
+                            && x.getAddresses().get(0) instanceof Inet4Address
+                            && x.getDnsServers().get(0) instanceof Inet4Address);
+
+                    if (!isOnlyIPv4Provisioned) return false;
+                    lpFuture.complete(x);
+                    return true;
+                }));
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
+        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
+    }
+
+    @Test
+    public void testDualStackProvisioning() throws Exception {
+        doDualStackProvisioning();
+
+        verify(mCb, never()).onProvisioningFailure(any());
+    }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 2422b77..00fe17c 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -69,6 +69,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.atLeastOnce;
@@ -1206,6 +1207,32 @@
     }
 
     @Test
+    public void testIsCaptivePortal_NoRevalidationBeforeNetworkConnected() throws Exception {
+        assumeTrue(CaptivePortalDataShimImpl.isSupported());
+
+        final NetworkMonitor nm = makeCellMeteredNetworkMonitor();
+
+        final LinkProperties lp = makeCapportLPs();
+
+        // LinkProperties changed, but NM should not revalidate before notifyNetworkConnected
+        nm.notifyLinkPropertiesChanged(lp);
+        verify(mHttpConnection, after(100).never()).getResponseCode();
+        verify(mHttpsConnection, never()).getResponseCode();
+        verify(mCapportApiConnection, never()).getResponseCode();
+
+        setValidProbes();
+        setApiContent(mCapportApiConnection, "{'captive': true, "
+                + "'user-portal-url': '" + TEST_LOGIN_URL + "'}");
+
+        // After notifyNetworkConnected, validation uses the capport API contents
+        nm.notifyNetworkConnected(lp, CELL_METERED_CAPABILITIES);
+        verifyNetworkTested(VALIDATION_RESULT_PORTAL, 0 /* probesSucceeded */, TEST_LOGIN_URL);
+
+        verify(mHttpConnection, never()).getResponseCode();
+        verify(mCapportApiConnection).getResponseCode();
+    }
+
+    @Test
     public void testIsCaptivePortal_CapportApiNotPortalNotValidated() throws Exception {
         assumeTrue(CaptivePortalDataShimImpl.isSupported());
         setSslException(mHttpsConnection);