Snap for 9254005 from 4a583e9284f2c7955c2ca9d3a9c841987752f8f9 to mainline-cellbroadcast-release

Change-Id: I696ec9e99a726cce52403a27dbf5ea548dada252
diff --git a/Android.bp b/Android.bp
index a6cf966..0aba37c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -74,11 +74,18 @@
     ]
 }
 
+// Common defaults for NetworkStack integration tests, root tests and coverage tests
+// to keep tests always running against the same target sdk version with NetworkStack.
 java_defaults {
-    name: "NetworkStackReleaseApiLevel",
-    sdk_version: module_33_version,
+    name: "NetworkStackReleaseTargetSdk",
     min_sdk_version: "29",
     target_sdk_version: "33",
+}
+
+java_defaults {
+    name: "NetworkStackReleaseApiLevel",
+    defaults:["NetworkStackReleaseTargetSdk"],
+    sdk_version: module_33_version,
     libs: [
         "framework-connectivity",
         "framework-connectivity-t",
@@ -372,7 +379,7 @@
     ],
     out: ["NetworkStackJarJarRules.txt"],
     cmd: "$(location jarjar-rules-generator) " +
-        "--jars $(location :NetworkStackApiStableLib{.jar}) " +
+        "$(location :NetworkStackApiStableLib{.jar}) " +
         "--prefix com.android.networkstack " +
         "--excludes $(location jarjar-excludes.txt) " +
         "--output $(out)",
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1d0f234..c1bc9cf 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -8,6 +8,9 @@
     },
     {
       "name": "NetworkStackIntegrationTests"
+    },
+    {
+      "name": "NetworkStackRootTests"
     }
   ],
   "mainline-presubmit": [
diff --git a/apishim/29/com/android/networkstack/apishim/api29/VpnProfileStateShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/VpnProfileStateShimImpl.java
deleted file mode 100644
index 10eb8b9..0000000
--- a/apishim/29/com/android/networkstack/apishim/api29/VpnProfileStateShimImpl.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.networkstack.apishim.api29;
-
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.networkstack.apishim.common.VpnProfileStateShim;
-
-/** Implementation of {@link VpnProfileStateShim} for API 29. */
-@RequiresApi(Build.VERSION_CODES.Q)
-public class VpnProfileStateShimImpl implements VpnProfileStateShim {
-}
diff --git a/apishim/31/com/android/networkstack/apishim/api31/VpnProfileStateShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/VpnProfileStateShimImpl.java
deleted file mode 100644
index 0bdd157..0000000
--- a/apishim/31/com/android/networkstack/apishim/api31/VpnProfileStateShimImpl.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.networkstack.apishim.api31;
-
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.networkstack.apishim.common.VpnProfileStateShim;
-
-/** Implementation of {@link VpnProfileStateShim} for API 31. */
-@RequiresApi(Build.VERSION_CODES.S)
-public class VpnProfileStateShimImpl
-        extends com.android.networkstack.apishim.api29.VpnProfileStateShimImpl {
-}
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp
index d7f6d78..5ff0288 100644
--- a/jni/network_stack_utils_jni.cpp
+++ b/jni/network_stack_utils_jni.cpp
@@ -60,7 +60,7 @@
     return true;
 }
 
-static void network_stack_utils_addArpEntry(JNIEnv *env, jobject thiz, jbyteArray ethAddr,
+static void network_stack_utils_addArpEntry(JNIEnv *env, jclass clazz, jbyteArray ethAddr,
         jbyteArray ipv4Addr, jstring ifname, jobject javaFd) {
     arpreq req = {};
     sockaddr_in& netAddrStruct = *reinterpret_cast<sockaddr_in*>(&req.arp_pa);
@@ -99,7 +99,7 @@
     }
 }
 
-static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd) {
+static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jclass clazz, jobject javaFd) {
     static sock_filter filter_code[] = {
         // Check the protocol is UDP.
         BPF_STMT(BPF_LD  | BPF_B    | BPF_ABS, kIPv4Protocol),
@@ -107,7 +107,7 @@
 
         // Check this is not a fragment.
         BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 4, 0),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_MF | IP_OFFMASK, 4, 0),
 
         // Get the IP header length.
         BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
@@ -116,8 +116,10 @@
         BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 0, 1),
 
-        // Accept or reject.
+        // Accept.
         BPF_STMT(BPF_RET | BPF_K,              0xffff),
+
+        // Reject.
         BPF_STMT(BPF_RET | BPF_K,              0)
     };
     static const sock_fprog filter = {
@@ -131,7 +133,7 @@
     }
 }
 
-static void network_stack_utils_attachRaFilter(JNIEnv *env, jobject clazz, jobject javaFd,
+static void network_stack_utils_attachRaFilter(JNIEnv *env, jclass clazz, jobject javaFd,
         jint hardwareAddressType) {
     if (hardwareAddressType != ARPHRD_ETHER) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
@@ -148,8 +150,10 @@
         BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    ND_ROUTER_ADVERT, 0, 1),
 
-        // Accept or reject.
+        // Accept.
         BPF_STMT(BPF_RET | BPF_K,              0xffff),
+
+        // Reject.
         BPF_STMT(BPF_RET | BPF_K,              0)
     };
     static const sock_fprog filter = {
@@ -166,7 +170,7 @@
 
 // TODO: Move all this filter code into libnetutils.
 static void network_stack_utils_attachControlPacketFilter(
-        JNIEnv *env, jobject clazz, jobject javaFd, jint hardwareAddressType) {
+        JNIEnv *env, jclass clazz, jobject javaFd, jint hardwareAddressType) {
     if (hardwareAddressType != ARPHRD_ETHER) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
                 "attachControlPacketFilter only supports ARPHRD_ETHER");
@@ -223,8 +227,10 @@
         BPF_JUMP(BPF_JMP | BPF_JGE  | BPF_K,   ND_ROUTER_SOLICIT, 0, 2),
         BPF_JUMP(BPF_JMP | BPF_JGT  | BPF_K,   ND_NEIGHBOR_ADVERT, 1, 0),
 
-        // Accept or reject.
+        // Accept.
         BPF_STMT(BPF_RET | BPF_K,              0xffff),
+
+        // Reject.
         BPF_STMT(BPF_RET | BPF_K,              0)
     };
     static const sock_fprog filter = {
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index feda547..61a7149 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -21,6 +21,6 @@
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"Informații despre locația rețelei"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Notificări afișate pentru a indica faptul că o rețea are o pagină cu informații despre locație"</string>
     <string name="connected" msgid="4563643884927480998">"Conectată"</string>
-    <string name="tap_for_info" msgid="6849746325626883711">"Conectat / atingeți pentru a vedea site-ul"</string>
+    <string name="tap_for_info" msgid="6849746325626883711">"Conectat / atinge pentru a vedea site-ul"</string>
     <string name="application_label" msgid="1322847171305285454">"Manager de rețea"</string>
 </resources>
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 97c6990..694c4ee 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -35,9 +35,11 @@
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
 import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_CLEAR_ADDRESSES_ON_STOP_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DISABLE_ACCEPT_RA_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import android.annotation.SuppressLint;
@@ -116,6 +118,7 @@
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.networkstack.metrics.NetworkQuirkMetrics;
 import com.android.networkstack.packets.NeighborAdvertisement;
+import com.android.networkstack.packets.NeighborSolicitation;
 import com.android.networkstack.util.NetworkStackUtils;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -455,7 +458,7 @@
     private static final int CMD_START                            = 3;
     private static final int CMD_CONFIRM                          = 4;
     private static final int EVENT_PRE_DHCP_ACTION_COMPLETE       = 5;
-    // Triggered by NetlinkTracker to communicate netlink events.
+    // Triggered by IpClientLinkObserver to communicate netlink events.
     private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6;
     private static final int CMD_UPDATE_TCP_BUFFER_SIZES          = 7;
     private static final int CMD_UPDATE_HTTP_PROXY                = 8;
@@ -478,6 +481,16 @@
     private static final int CMD_ADDRESSES_CLEARED                = 100;
     private static final int CMD_JUMP_RUNNING_TO_STOPPING         = 101;
     private static final int CMD_JUMP_STOPPING_TO_STOPPED         = 102;
+    private static final int CMD_JUMP_STOPPING_TO_CLEAR_ADDRESSES_ON_STOP = 103;
+    private static final int EVENT_CLEAR_ADDRESSES_TIMEOUT        = 104;
+
+    // Used to time out the wait for IP addresses cleared. This timeout is
+    // necessary because netlink events will get lost if ENOBUFS happens,
+    // then RTM_DELADDR might never arrive, which results in never exiting
+    // ClearAddressesOnStopState.
+    @VisibleForTesting
+    static final String CONFIG_CLEAR_ADDRESSES_TIMEOUT = "ipclient_clear_addresses_timeout";
+    private static final int DEFAULT_CLEAR_ADDRESSES_TIMEOUT_MS = 2000;
 
     // IpClient shares a handler with DhcpClient: commands must not overlap
     public static final int DHCPCLIENT_CMD_BASE = 1000;
@@ -532,10 +545,11 @@
 
     private final State mStoppedState = new StoppedState();
     private final State mStoppingState = new StoppingState();
-    private final State mClearingIpAddressesState = new ClearingIpAddressesState();
     private final State mStartedState = new StartedState();
     private final State mRunningState = new RunningState();
     private final State mPreconnectingState = new PreconnectingState();
+    private final State mClearAddressesOnStopState = new ClearAddressesOnStopState();
+    private final State mClearAddressesOnStartState = new ClearAddressesOnStartState();
 
     private final String mTag;
     private final Context mContext;
@@ -557,6 +571,8 @@
     private final InterfaceController mInterfaceCtrl;
     // Set of IPv6 addresses for which unsolicited gratuitous NA packets have been sent.
     private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
+    // Set of IPv6 addresses from which multicast NS packets have been sent.
+    private final Set<Inet6Address> mMulticastNsSourceAddresses = new HashSet<>();
 
     // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
     private final int mMinRdnssLifetimeSec;
@@ -580,6 +596,7 @@
     private long mStartTimeMillis;
     private MacAddress mCurrentBssid;
     private boolean mHasDisabledIpv6OrAcceptRaOnProvLoss;
+    private boolean mClearAddressesOnStop;
 
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
@@ -745,15 +762,18 @@
                     }
 
                     @Override
-                    public void onIpv6AddressRemoved(final Inet6Address targetIp) {
-                        // The update of Gratuitous NA target addresses set should be only accessed
-                        // from the handler thread of IpClient StateMachine, keeping the behaviour
+                    public void onIpv6AddressRemoved(final Inet6Address address) {
+                        // The update of Gratuitous NA target addresses set or unsolicited
+                        // multicast NS source addresses set should be only accessed from the
+                        // handler thread of IpClient StateMachine, keeping the behaviour
                         // consistent with relying on the non-blocking NetworkObserver callbacks,
                         // see {@link registerObserverForNonblockingCallback}. This can be done
                         // by either sending a message to StateMachine or posting a handler.
                         getHandler().post(() -> {
-                            if (!mGratuitousNaTargetAddresses.contains(targetIp)) return;
-                            updateGratuitousNaTargetSet(targetIp, false /* remove address */);
+                            mLog.log("Remove IPv6 GUA " + address
+                                    + " from both Gratuituous NA and Multicast NS sets");
+                            mGratuitousNaTargetAddresses.remove(address);
+                            mMulticastNsSourceAddresses.remove(address);
                         });
                     }
 
@@ -891,10 +911,11 @@
         // CHECKSTYLE:OFF IndentationCheck
         addState(mStoppedState);
         addState(mStartedState);
+            addState(mClearAddressesOnStartState, mStartedState);
             addState(mPreconnectingState, mStartedState);
-            addState(mClearingIpAddressesState, mStartedState);
             addState(mRunningState, mStartedState);
         addState(mStoppingState);
+        addState(mClearAddressesOnStopState);
         // CHECKSTYLE:ON IndentationCheck
 
         setInitialState(mStoppedState);
@@ -922,6 +943,11 @@
                 false /* defaultEnabled */);
     }
 
+    private boolean isMulticastNsEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_MULTICAST_NS_VERSION,
+                false /* defaultEnabled */);
+    }
+
     @VisibleForTesting
     static MacAddress getInitialBssid(final Layer2Information layer2Info,
             final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
@@ -951,6 +977,11 @@
                 true /* defaultEnabled */);
     }
 
+    private boolean shouldClearAddressesOnStop() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_CLEAR_ADDRESSES_ON_STOP_VERSION,
+                false /* defaultEnabled */);
+    }
+
     @Override
     protected void onQuitting() {
         mCallback.onQuit();
@@ -1636,6 +1667,22 @@
         transmitPacket(packet, sockAddress, "Failed to send GARP");
     }
 
+    private void sendMulticastNs(final Inet6Address srcIp, final Inet6Address dstIp,
+            final Inet6Address targetIp) {
+        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
+        final ByteBuffer packet = NeighborSolicitation.build(mInterfaceParams.macAddr, dstMac,
+                srcIp, dstIp, targetIp);
+        final SocketAddress sockAddress =
+                SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
+                        mInterfaceParams.index, dstMac.toByteArray());
+
+        if (DBG) {
+            mLog.log("send multicast NS from " + srcIp.getHostAddress() + " to "
+                    + dstIp.getHostAddress() + " , target IP: " + targetIp.getHostAddress());
+        }
+        transmitPacket(packet, sockAddress, "Failed to send multicast Neighbor Solicitation");
+    }
+
     @Nullable
     private static Inet6Address getIpv6LinkLocalAddress(final LinkProperties newLp) {
         for (LinkAddress la : newLp.getLinkAddresses()) {
@@ -1646,16 +1693,6 @@
         return null;
     }
 
-    private void updateGratuitousNaTargetSet(@NonNull final Inet6Address targetIp, boolean add) {
-        if (add) {
-            mGratuitousNaTargetAddresses.add(targetIp);
-        } else {
-            mGratuitousNaTargetAddresses.remove(targetIp);
-        }
-        mLog.log((add ? "Add" : "Remove") + " global IPv6 address " + targetIp
-                + (add ? " to" : " from") + " the set of gratuitous NA target address.");
-    }
-
     private void maybeSendGratuitousNAs(final LinkProperties lp, boolean afterRoaming) {
         if (!lp.hasGlobalIpv6Address()) return;
 
@@ -1665,7 +1702,7 @@
         // TODO: add experiment with sending only one gratuitous NA packet instead of one
         // packet per address.
         for (LinkAddress la : lp.getLinkAddresses()) {
-            if (!la.isIpv6() || !la.isGlobalPreferred()) continue;
+            if (!NetworkStackUtils.isIPv6GUA(la)) continue;
             final Inet6Address targetIp = (Inet6Address) la.getAddress();
             // Already sent gratuitous NA with this target global IPv6 address. But for
             // the L2 roaming case, device should always (re)transmit Gratuitous NA for
@@ -1676,7 +1713,9 @@
                         + targetIp.getHostAddress() + (afterRoaming ? " after roaming" : ""));
             }
             sendGratuitousNA(srcIp, targetIp);
-            if (!afterRoaming) updateGratuitousNaTargetSet(targetIp, true /* add address */);
+            if (!afterRoaming) {
+                mGratuitousNaTargetAddresses.add(targetIp);
+            }
         }
     }
 
@@ -1693,6 +1732,39 @@
         }
     }
 
+    @Nullable
+    private static Inet6Address getIPv6DefaultGateway(final LinkProperties lp) {
+        for (RouteInfo r : lp.getRoutes()) {
+            // TODO: call {@link RouteInfo#isIPv6Default} directly after core networking modules
+            // are consolidated.
+            if (r.getType() == RTN_UNICAST && r.getDestination().getPrefixLength() == 0
+                    && r.getDestination().getAddress() instanceof Inet6Address) {
+                // Check if it's IPv6 default route, if yes, return the gateway address
+                // (i.e. default router's IPv6 link-local address)
+                return (Inet6Address) r.getGateway();
+            }
+        }
+        return null;
+    }
+
+    private void maybeSendMulticastNSes(final LinkProperties lp) {
+        if (!(lp.hasGlobalIpv6Address() && lp.hasIpv6DefaultRoute())) return;
+
+        // Get the default router's IPv6 link-local address.
+        final Inet6Address targetIp = getIPv6DefaultGateway(lp);
+        if (targetIp == null) return;
+        final Inet6Address dstIp = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(targetIp);
+        if (dstIp == null) return;
+
+        for (LinkAddress la : lp.getLinkAddresses()) {
+            if (!NetworkStackUtils.isIPv6GUA(la)) continue;
+            final Inet6Address srcIp = (Inet6Address) la.getAddress();
+            if (mMulticastNsSourceAddresses.contains(srcIp)) continue;
+            sendMulticastNs(srcIp, dstIp, targetIp);
+            mMulticastNsSourceAddresses.add(srcIp);
+        }
+    }
+
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
@@ -1707,6 +1779,17 @@
             maybeSendGratuitousNAs(newLp, false /* isGratuitousNaAfterRoaming */);
         }
 
+        // Sending multicast NS from each new assigned IPv6 GUAs to the solicited-node multicast
+        // address based on the default router's IPv6 link-local address should trigger default
+        // router response with NA, and update the neighbor cache entry immediately, that would
+        // help speed up the connection to an IPv6-only network.
+        //
+        // TODO: stop sending this multicast NS after deployment of RFC9131 in the field, leverage
+        // the gratuitous NA to update the first-hop router's neighbor cache entry.
+        if (isMulticastNsEnabled()) {
+            maybeSendMulticastNSes(newLp);
+        }
+
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
         mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
@@ -2004,9 +2087,15 @@
     class StoppedState extends State {
         @Override
         public void enter() {
+            // It's necessary to disable IPv6 stack at StoppedState#enter, which cleans up the
+            // IPv6 link-local address and default IPv6 link-local route(fe80::/64 -> ::) which
+            // are generated on interface creation. The IPv6 link-local address and route will
+            // come back later during provisioning, otherwise, there is no way to syncup the
+            // initial link-local address and route to mLinkProperties.
             stopAllIP();
             mHasDisabledIpv6OrAcceptRaOnProvLoss = false;
             mGratuitousNaTargetAddresses.clear();
+            mMulticastNsSourceAddresses.clear();
 
             resetLinkProperties();
             if (mStartTimeMillis > 0) {
@@ -2030,8 +2119,9 @@
                     break;
 
                 case CMD_START:
+                    mClearAddressesOnStop = shouldClearAddressesOnStop();
                     mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj;
-                    transitionTo(mClearingIpAddressesState);
+                    transitionTo(mClearAddressesOnStartState);
                     break;
 
                 case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
@@ -2078,7 +2168,10 @@
         public void enter() {
             if (mDhcpClient == null) {
                 // There's no DHCPv4 for which to wait; proceed to stopped.
-                deferMessage(obtainMessage(CMD_JUMP_STOPPING_TO_STOPPED));
+                deferMessage(obtainMessage(
+                        mClearAddressesOnStop
+                                ? CMD_JUMP_STOPPING_TO_CLEAR_ADDRESSES_ON_STOP
+                                : CMD_JUMP_STOPPING_TO_STOPPED));
             } else {
                 mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
                 mDhcpClient.doQuit();
@@ -2095,6 +2188,10 @@
                     transitionTo(mStoppedState);
                     break;
 
+                case CMD_JUMP_STOPPING_TO_CLEAR_ADDRESSES_ON_STOP:
+                    transitionTo(mClearAddressesOnStopState);
+                    break;
+
                 case CMD_STOP:
                     break;
 
@@ -2104,7 +2201,9 @@
 
                 case DhcpClient.CMD_ON_QUIT:
                     mDhcpClient = null;
-                    transitionTo(mStoppedState);
+                    transitionTo(mClearAddressesOnStop
+                            ? mClearAddressesOnStopState
+                            : mStoppedState);
                     break;
 
                 default:
@@ -2160,7 +2259,65 @@
                 isUsingPreconnection(), options));
     }
 
-    class ClearingIpAddressesState extends State {
+    abstract class ClearAddressesState extends State {
+        protected abstract State getTargetState();
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ADDRESSES_CLEARED:
+                    transitionTo(getTargetState());
+                    break;
+
+                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+                    handleLinkPropertiesUpdate(NO_CALLBACKS);
+                    if (readyToProceed()) {
+                        transitionTo(getTargetState());
+                    }
+                    break;
+
+                case EVENT_CLEAR_ADDRESSES_TIMEOUT:
+                    transitionTo(mStoppedState);
+                    break;
+
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        // Usually NetworkAgent will be unregistered along with stopping IpClient, then network will
+        // be destroyed and IPv6 relevant routes will be removed from interface, but IPv4 relevant
+        // routes won't be removed if any. Disabling IPv6 stack also results in the removal of all
+        // IPv6 addresses and relevant routes.
+        private boolean readyToProceed() {
+            return !mLinkProperties.hasIpv4Address() && !mLinkProperties.hasGlobalIpv6Address()
+                    && !mLinkProperties.hasIpv6DefaultRoute();
+        }
+
+        protected void ensureIpAddressesCleared() {
+            if (readyToProceed()) {
+                deferMessage(obtainMessage(CMD_ADDRESSES_CLEARED));
+            } else {
+                // Clear all IPv4 and IPv6 before proceeding to RunningState.
+                // Clean up any leftover state from an abnormal exit from
+                // tethering or during an IpClient restart.
+                stopAllIP();
+
+                // Set a timeout for waiting the RTM_DELADDR netlink events.
+                sendMessageDelayed(EVENT_CLEAR_ADDRESSES_TIMEOUT,
+                        mDependencies.getDeviceConfigPropertyInt(CONFIG_CLEAR_ADDRESSES_TIMEOUT,
+                                DEFAULT_CLEAR_ADDRESSES_TIMEOUT_MS));
+            }
+        }
+    }
+
+    class ClearAddressesOnStartState extends ClearAddressesState {
+        @Override
+        protected State getTargetState() {
+            return isUsingPreconnection() ? mPreconnectingState : mRunningState;
+        }
+
         @Override
         public void enter() {
             // Ensure that interface parameters are fetched on the handler thread so they are
@@ -2175,33 +2332,14 @@
             }
 
             mLinkObserver.setInterfaceParams(mInterfaceParams);
-
-            if (readyToProceed()) {
-                deferMessage(obtainMessage(CMD_ADDRESSES_CLEARED));
-            } else {
-                // Clear all IPv4 and IPv6 before proceeding to RunningState.
-                // Clean up any leftover state from an abnormal exit from
-                // tethering or during an IpClient restart.
-                stopAllIP();
-            }
-
             mCallback.setNeighborDiscoveryOffload(true);
+            ensureIpAddressesCleared();
         }
 
         @Override
         public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) return HANDLED;
             switch (msg.what) {
-                case CMD_ADDRESSES_CLEARED:
-                    transitionTo(isUsingPreconnection() ? mPreconnectingState : mRunningState);
-                    break;
-
-                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
-                    handleLinkPropertiesUpdate(NO_CALLBACKS);
-                    if (readyToProceed()) {
-                        transitionTo(isUsingPreconnection() ? mPreconnectingState : mRunningState);
-                    }
-                    break;
-
                 case CMD_STOP:
                 case EVENT_PROVISIONING_TIMEOUT:
                     // Fall through to StartedState.
@@ -2217,9 +2355,29 @@
             }
             return HANDLED;
         }
+    }
 
-        private boolean readyToProceed() {
-            return !mLinkProperties.hasIpv4Address() && !mLinkProperties.hasGlobalIpv6Address();
+    class ClearAddressesOnStopState extends ClearAddressesState {
+        @Override
+        protected State getTargetState() {
+            return mStoppedState;
+        }
+
+        @Override
+        public void enter() {
+            ensureIpAddressesCleared();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) return HANDLED;
+            switch (msg.what) {
+                default:
+                    // Any messages which are not processed in ClearAddressesState will be
+                    // deferred to StoppedState.
+                    deferMessage(msg);
+            }
+            return HANDLED;
         }
     }
 
@@ -2406,8 +2564,6 @@
                 mApfFilter.shutdown();
                 mApfFilter = null;
             }
-
-            resetLinkProperties();
         }
 
         private void enqueueJumpToStoppingState(final DisconnectCode code) {
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 08226ef..e73a913 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -60,6 +60,7 @@
 import com.android.net.module.util.netlink.StructNdOptRdnss;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
+import com.android.networkstack.util.NetworkStackUtils;
 import com.android.server.NetworkObserver;
 
 import java.net.Inet6Address;
@@ -120,7 +121,7 @@
         void update(boolean linkState);
 
         /**
-         * Called when an IPv6 address was removed from the interface.
+         * Called when an IPv6 global unicast address was removed from the interface.
          *
          * @param addr The removed IPv6 address.
          */
@@ -156,6 +157,7 @@
     private final IpClient.Dependencies mDependencies;
     private final String mClatInterfaceName;
     private final MyNetlinkMonitor mNetlinkMonitor;
+    private final boolean mNetlinkEventParsingEnabled;
 
     private boolean mClatInterfaceExists;
 
@@ -176,7 +178,7 @@
         mContext = context;
         mInterfaceName = iface;
         mClatInterfaceName = CLAT_PREFIX + iface;
-        mTag = "NetlinkTracker/" + mInterfaceName;
+        mTag = "IpClientLinkObserver/" + mInterfaceName;
         mCallback = callback;
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
@@ -186,6 +188,8 @@
         mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mDependencies = deps;
+        mNetlinkEventParsingEnabled = deps.isFeatureEnabled(context,
+                IPCLIENT_PARSE_NETLINK_EVENTS_VERSION, isAtLeastT() /* default value */);
         mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
         mHandler.post(() -> {
             if (!mNetlinkMonitor.start()) {
@@ -215,11 +219,6 @@
         }
     }
 
-    private boolean isNetlinkEventParsingEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_PARSE_NETLINK_EVENTS_VERSION,
-                isAtLeastT() /* default value */);
-    }
-
     private int getSocketReceiveBufferSize() {
         final int size = mDependencies.getDeviceConfigPropertyInt(CONFIG_SOCKET_RECV_BUFSIZE,
                 SOCKET_RECV_BUFSIZE /* default value */);
@@ -231,7 +230,7 @@
 
     @Override
     public void onInterfaceAdded(String iface) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         maybeLog("interfaceAdded", iface);
         if (mClatInterfaceName.equals(iface)) {
             mCallback.onClatInterfaceStateUpdate(true /* add interface */);
@@ -240,7 +239,7 @@
 
     @Override
     public void onInterfaceRemoved(String iface) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         maybeLog("interfaceRemoved", iface);
         if (mClatInterfaceName.equals(iface)) {
             mCallback.onClatInterfaceStateUpdate(false /* remove interface */);
@@ -251,7 +250,7 @@
 
     @Override
     public void onInterfaceLinkStateChanged(String iface, boolean state) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(iface)) return;
         maybeLog("interfaceLinkStateChanged", iface + (state ? " up" : " down"));
         updateInterfaceLinkStateChanged(state);
@@ -259,7 +258,7 @@
 
     @Override
     public void onInterfaceAddressUpdated(LinkAddress address, String iface) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(iface)) return;
         maybeLog("addressUpdated", iface, address);
         updateInterfaceAddress(address, true /* add address */);
@@ -267,7 +266,7 @@
 
     @Override
     public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(iface)) return;
         maybeLog("addressRemoved", iface, address);
         updateInterfaceAddress(address, false /* remove address */);
@@ -275,7 +274,7 @@
 
     @Override
     public void onRouteUpdated(RouteInfo route) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(route.getInterface())) return;
         maybeLog("routeUpdated", route);
         updateInterfaceRoute(route, true /* add route */);
@@ -283,7 +282,7 @@
 
     @Override
     public void onRouteRemoved(RouteInfo route) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(route.getInterface())) return;
         maybeLog("routeRemoved", route);
         updateInterfaceRoute(route, false /* remove route */);
@@ -291,7 +290,7 @@
 
     @Override
     public void onInterfaceDnsServerInfo(String iface, long lifetime, String[] addresses) {
-        if (isNetlinkEventParsingEnabled()) return;
+        if (mNetlinkEventParsingEnabled) return;
         if (!mInterfaceName.equals(iface)) return;
         updateInterfaceDnsServerInfo(lifetime, addresses);
     }
@@ -326,7 +325,7 @@
         }
         if (changed) {
             mCallback.update(linkState);
-            if (!add && address.isIpv6()) {
+            if (!add && NetworkStackUtils.isIPv6GUA(address)) {
                 final Inet6Address addr = (Inet6Address) address.getAddress();
                 mCallback.onIpv6AddressRemoved(addr);
             }
@@ -423,7 +422,7 @@
 
         MyNetlinkMonitor(Handler h, SharedLog log, String tag) {
             super(h, log, tag, OsConstants.NETLINK_ROUTE,
-                    !isNetlinkEventParsingEnabled()
+                    !mNetlinkEventParsingEnabled
                         ? NetlinkConstants.RTMGRP_ND_USEROPT
                         : (NetlinkConstants.RTMGRP_ND_USEROPT | NetlinkConstants.RTMGRP_LINK
                                 | NetlinkConstants.RTMGRP_IPV4_IFADDR
@@ -536,7 +535,7 @@
         }
 
         private void processRdnssOption(StructNdOptRdnss opt) {
-            if (!isNetlinkEventParsingEnabled()) return;
+            if (!mNetlinkEventParsingEnabled) return;
             final String[] addresses = new String[opt.servers.length];
             for (int i = 0; i < opt.servers.length; i++) {
                 addresses[i] = opt.servers[i].getHostAddress();
@@ -587,7 +586,7 @@
         }
 
         private void processRtNetlinkLinkMessage(RtNetlinkLinkMessage msg) {
-            if (!isNetlinkEventParsingEnabled()) return;
+            if (!mNetlinkEventParsingEnabled) return;
 
             // Check if receiving netlink link state update for clat interface.
             final String ifname = msg.getInterfaceName();
@@ -621,7 +620,7 @@
         }
 
         private void processRtNetlinkAddressMessage(RtNetlinkAddressMessage msg) {
-            if (!isNetlinkEventParsingEnabled()) return;
+            if (!mNetlinkEventParsingEnabled) return;
 
             final StructIfaddrMsg ifaddrMsg = msg.getIfaddrHeader();
             if (ifaddrMsg.index != mIfindex) return;
@@ -646,7 +645,7 @@
         }
 
         private void processRtNetlinkRouteMessage(RtNetlinkRouteMessage msg) {
-            if (!isNetlinkEventParsingEnabled()) return;
+            if (!mNetlinkEventParsingEnabled) return;
             if (msg.getInterfaceIndex() != mIfindex) return;
             // Ignore the unsupported route protocol and non-global unicast routes.
             if (!isSupportedRouteProtocol(msg)
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 00f6dfa..d16840d 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -232,6 +232,7 @@
     private int mInterSolicitIntervalMs;
     @NonNull
     private final Callback mCallback;
+    private final boolean mMulticastResolicitEnabled;
 
     public IpReachabilityMonitor(
             Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
@@ -253,6 +254,8 @@
         mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
         mCm = context.getSystemService(ConnectivityManager.class);
         mDependencies = dependencies;
+        mMulticastResolicitEnabled = dependencies.isFeatureEnabled(context,
+                IP_REACHABILITY_MCAST_RESOLICIT_VERSION, false /* defaultEnabled */);
         mMetricsLog = metricsLog;
         mNetd = netd;
         Preconditions.checkNotNull(mNetd);
@@ -261,7 +264,7 @@
         // In case the overylaid parameters specify an invalid configuration, set the parameters
         // to the hardcoded defaults first, then set them to the values used in the steady state.
         try {
-            int numResolicits = isMulticastResolicitEnabled()
+            int numResolicits = mMulticastResolicitEnabled
                     ? NUD_MCAST_RESOLICIT_NUM
                     : INVALID_NUD_MCAST_RESOLICIT_NUM;
             setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, numResolicits);
@@ -270,7 +273,7 @@
         }
         setNeighbourParametersForSteadyState();
 
-        mIpNeighborMonitor = mDependencies.makeIpNeighborMonitor(h, mLog,
+        mIpNeighborMonitor = dependencies.makeIpNeighborMonitor(h, mLog,
                 (NeighborEvent event) -> {
                     if (mInterfaceParams.index != event.ifindex) return;
                     if (!mNeighborWatchList.containsKey(event.ip)) return;
@@ -362,11 +365,6 @@
         return false;
     }
 
-    private boolean isMulticastResolicitEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
-                false /* defaultEnabled */);
-    }
-
     public void updateLinkProperties(LinkProperties lp) {
         if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
             // TODO: figure out whether / how to cope with interface changes.
@@ -406,7 +404,7 @@
 
     private void handleNeighborReachable(@Nullable final NeighborEvent prev,
             @NonNull final NeighborEvent event) {
-        if (isMulticastResolicitEnabled()
+        if (mMulticastResolicitEnabled
                 && hasDefaultRouterNeighborMacAddressChanged(prev, event)) {
             // This implies device has confirmed the neighbor's reachability from
             // other states(e.g., NUD_PROBE or NUD_STALE), checking if the mac
@@ -529,7 +527,7 @@
     private long getProbeWakeLockDuration() {
         final long gracePeriodMs = 500;
         final int numSolicits =
-                mNumSolicits + (isMulticastResolicitEnabled() ? NUD_MCAST_RESOLICIT_NUM : 0);
+                mNumSolicits + (mMulticastResolicitEnabled ? NUD_MCAST_RESOLICIT_NUM : 0);
         return (long) (numSolicits * mInterSolicitIntervalMs) + gracePeriodMs;
     }
 
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 2ec6841..6af742c 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -17,11 +17,14 @@
 package com.android.networkstack.util;
 
 import android.content.Context;
+import android.net.LinkAddress;
 import android.net.MacAddress;
 import android.net.util.SocketUtils;
 import android.system.ErrnoException;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.net.module.util.DeviceConfigUtils;
 
@@ -29,7 +32,9 @@
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.SocketException;
+import java.net.UnknownHostException;
 
 /**
  * Collection of utilities for the network stack.
@@ -221,6 +226,13 @@
     public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";
 
     /**
+     * Experiment flag to send multicast NS from the global IPv6 GUA to the solicited-node
+     * multicast address based on the default router's IPv6 link-local address, which helps
+     * flush the first-hop routers' neighbor cache entry for the global IPv6 GUA.
+     */
+    public static final String IPCLIENT_MULTICAST_NS_VERSION = "ipclient_multicast_ns_version";
+
+    /**
      * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for
      * all assigned IPv4 and IPv6 GUAs after completing L2 roaming.
      */
@@ -247,6 +259,13 @@
     public static final String IP_REACHABILITY_MCAST_RESOLICIT_VERSION =
             "ip_reachability_mcast_resolicit_version";
 
+    /**
+     * Experiment flag to wait for IP addresses cleared completely before transition to
+     * IpClient#StoppedState from IpClient#StoppingState.
+     */
+    public static final String IPCLIENT_CLEAR_ADDRESSES_ON_STOP_VERSION =
+            "ipclient_clear_addresses_on_stop_version";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
@@ -277,6 +296,36 @@
     }
 
     /**
+     * Convert IPv6 unicast or anycast address to solicited node multicast address
+     * per RFC4291 section 2.7.1.
+     */
+    @Nullable
+    public static Inet6Address ipv6AddressToSolicitedNodeMulticast(
+            @NonNull final Inet6Address addr) {
+        final byte[] address = new byte[16];
+        address[0] = (byte) 0xFF;
+        address[1] = (byte) 0x02;
+        address[11] = (byte) 0x01;
+        address[12] = (byte) 0xFF;
+        address[13] = addr.getAddress()[13];
+        address[14] = addr.getAddress()[14];
+        address[15] = addr.getAddress()[15];
+        try {
+            return (Inet6Address) InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid host IP address " + addr.getHostAddress(), e);
+            return null;
+        }
+    }
+
+    /**
+     * Check whether a link address is IPv6 global unicast address.
+     */
+    public static boolean isIPv6GUA(@NonNull final LinkAddress address) {
+        return address.isIpv6() && address.isGlobalPreferred();
+    }
+
+    /**
      * Attaches a socket filter that accepts DHCP packets to the given socket.
      */
     public static native void attachDhcpFilter(FileDescriptor fd) throws ErrnoException;
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index da6c289..61cb143 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -442,6 +442,10 @@
     private final String mCaptivePortalHttpsUrlFromSetting;
     private final String mCaptivePortalHttpUrlFromSetting;
     @Nullable
+    private final URL mTestCaptivePortalHttpsUrl;
+    @Nullable
+    private final URL mTestCaptivePortalHttpUrl;
+    @Nullable
     private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs;
 
     // The probing URLs may be updated after constructor if system notifies configuration changed.
@@ -613,6 +617,9 @@
                 mDependencies.getSetting(context, CAPTIVE_PORTAL_HTTPS_URL, null);
         mCaptivePortalHttpUrlFromSetting =
                 mDependencies.getSetting(context, CAPTIVE_PORTAL_HTTP_URL, null);
+        mTestCaptivePortalHttpsUrl =
+                getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL, validationLogs, deps);
+        mTestCaptivePortalHttpUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL, validationLogs, deps);
         mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
         mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled();
         mMetricsEnabled = deps.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
@@ -2065,8 +2072,9 @@
     }
 
     @Nullable
-    private URL getTestUrl(@NonNull String key) {
-        final String strExpiration = mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
+    private static URL getTestUrl(@NonNull String key, @NonNull SharedLog log,
+            @NonNull Dependencies deps) {
+        final String strExpiration = deps.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
                 TEST_URL_EXPIRATION_TIME, null);
         if (strExpiration == null) return null;
 
@@ -2074,23 +2082,23 @@
         try {
             expTime = Long.parseUnsignedLong(strExpiration);
         } catch (NumberFormatException e) {
-            loge("Invalid test URL expiration time format", e);
+            log.e("Invalid test URL expiration time format", e);
             return null;
         }
 
         final long now = System.currentTimeMillis();
         if (expTime < now || (expTime - now) > TEST_URL_EXPIRATION_MS) {
-            logw("Skipping test URL with expiration " + expTime + ", now " + now);
+            log.w("Skipping test URL with expiration " + expTime + ", now " + now);
             return null;
         }
 
-        final String strUrl = mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
+        final String strUrl = deps.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
                 key, null /* defaultValue */);
         if (!isValidTestUrl(strUrl)) {
-            logw("Skipping invalid test URL " + strUrl);
+            log.w("Skipping invalid test URL " + strUrl);
             return null;
         }
-        return makeURL(strUrl);
+        return makeURL(strUrl, log);
     }
 
     private String getCaptivePortalServerHttpsUrl(@NonNull Context context) {
@@ -2251,8 +2259,7 @@
     }
 
     private URL[] makeCaptivePortalHttpsUrls(@NonNull Context context) {
-        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
-        if (testUrl != null) return new URL[] { testUrl };
+        if (mTestCaptivePortalHttpsUrl != null) return new URL[] { mTestCaptivePortalHttpsUrl };
 
         final String firstUrl = getCaptivePortalServerHttpsUrl(context);
         try {
@@ -2272,8 +2279,7 @@
     }
 
     private URL[] makeCaptivePortalHttpUrls(@NonNull Context context) {
-        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
-        if (testUrl != null) return new URL[] { testUrl };
+        if (mTestCaptivePortalHttpUrl != null) return new URL[] { mTestCaptivePortalHttpUrl };
 
         final String firstUrl = getCaptivePortalServerHttpUrl(context);
         try {
@@ -3171,12 +3177,18 @@
         }
     }
 
-    private URL makeURL(String url) {
+    @Nullable
+    private URL makeURL(@Nullable String url) {
+        return makeURL(url, mValidationLogs);
+    }
+
+    @Nullable
+    private static URL makeURL(@Nullable String url, @NonNull SharedLog log) {
         if (url != null) {
             try {
                 return new URL(url);
             } catch (MalformedURLException e) {
-                validationLog("Bad URL: " + url);
+                log.w("Bad URL: " + url);
             }
         }
         return null;
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index f21cf33..fca588d 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -34,7 +34,10 @@
 
 java_defaults {
     name: "NetworkStackIntegrationTestsDefaults",
-    defaults: ["framework-connectivity-test-defaults"],
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "NetworkStackReleaseTargetSdk",
+    ],
     srcs: [
         "common/**/*.java",
         "common/**/*.kt",
@@ -70,13 +73,14 @@
 // Network stack integration tests.
 android_test {
     name: "NetworkStackIntegrationTests",
-    defaults: ["NetworkStackIntegrationTestsJniDefaults"],
+    defaults: [
+        "NetworkStackReleaseTargetSdk",
+        "NetworkStackIntegrationTestsJniDefaults",
+    ],
     static_libs: ["NetworkStackIntegrationTestsLib"],
     certificate: "networkstack",
     platform_apis: true,
     test_suites: ["device-tests"],
-    min_sdk_version: "29",
-    target_sdk_version: "30",
     jarjar_rules: ":NetworkStackJarJarRules",
     test_config_template: "AndroidTestTemplate_Integration.xml",
 }
@@ -129,11 +133,12 @@
     name: "NetworkStackCoverageTests",
     certificate: "networkstack",
     platform_apis: true,
-    min_sdk_version: "29",
-    target_sdk_version: "30",
     test_suites: ["device-tests", "mts-networking"],
     test_config: "AndroidTest_Coverage.xml",
-    defaults: ["NetworkStackIntegrationTestsJniDefaults"],
+    defaults: [
+        "NetworkStackReleaseTargetSdk",
+        "NetworkStackIntegrationTestsJniDefaults",
+    ],
     static_libs: [
         "modules-utils-native-coverage-listener",
         "NetworkStackTestsLib",
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 1a4dab1..34d4a07 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -642,6 +642,7 @@
         when(mCb.getInterfaceVersion()).thenReturn(IpClient.VERSION_ADDED_REACHABILITY_FAILURE);
 
         mDependencies.setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67);
+        mDependencies.setDeviceConfigProperty(IpClient.CONFIG_CLEAR_ADDRESSES_TIMEOUT, 50);
         mDependencies.setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10);
         mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10);
         mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MIN_MS, 10);
@@ -2186,7 +2187,7 @@
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testIpClientClearingIpAddressState() throws Exception {
+    public void testClearAddressesOnStartState() throws Exception {
         doIPv4OnlyProvisioningAndExitWithLeftAddress();
 
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -2207,7 +2208,7 @@
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testIpClientClearingIpAddressState_enablePreconnection() throws Exception {
+    public void testClearAddressesOnStartState_enablePreconnection() throws Exception {
         doIPv4OnlyProvisioningAndExitWithLeftAddress();
 
         // Enter ClearingIpAddressesState to clear the remaining IPv4 addresses and transition to
@@ -2812,7 +2813,7 @@
                 true /* shouldReplyNakOnRoam */);
     }
 
-    private void performDualStackProvisioning() throws Exception {
+    private LinkProperties performDualStackProvisioning() throws Exception {
         final InOrder inOrder = inOrder(mCb);
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
         final String dnsServer = "2001:4860:4860::64";
@@ -2838,9 +2839,10 @@
         assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
 
         reset(mCb);
+        return lp;
     }
 
-    private void doDualStackProvisioning(boolean shouldDisableAcceptRa) throws Exception {
+    private LinkProperties doDualStackProvisioning(boolean shouldDisableAcceptRa) throws Exception {
         when(mCm.shouldAvoidBadWifi()).thenReturn(true);
 
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -2855,7 +2857,7 @@
                 false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
         mIpc.startProvisioning(config);
 
-        performDualStackProvisioning();
+        return performDualStackProvisioning();
     }
 
     @Test @SignatureRequiredTest(reason = "signature perms are required due to mocked callabck")
@@ -3497,6 +3499,25 @@
         assertTrue(target.isGlobalPreferred());
     }
 
+    private void assertMulticastNsFromIpv6Gua(final NeighborSolicitation ns) throws Exception {
+        final Inet6Address solicitedNodeMulticast =
+                NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(ROUTER_LINK_LOCAL);
+        final MacAddress etherMulticast =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedNodeMulticast);
+
+        assertEquals(etherMulticast, ns.ethHdr.dstMac);
+        assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
+        assertEquals(0xff, ns.ipv6Hdr.hopLimit);
+
+        final LinkAddress srcIp = new LinkAddress(ns.ipv6Hdr.srcIp.getHostAddress() + "/64");
+        assertTrue(srcIp.isGlobalPreferred());
+        assertEquals(solicitedNodeMulticast, ns.ipv6Hdr.dstIp);
+        assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
+        assertEquals(0, ns.icmpv6Hdr.code);
+        assertEquals(ROUTER_LINK_LOCAL, ns.nsHdr.target);
+    }
+
     @Test
     public void testGratuitousNaForNewGlobalUnicastAddresses() throws Exception {
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -4003,4 +4024,52 @@
         assertFalse(hasRouteTo(lp, prefix));
         assertFalse(lp.hasIpv6DefaultRoute());
     }
+
+    @Test @SignatureRequiredTest(reason = "Need MANAGE_TEST_NETWORKS perm to create NetworkAgent")
+    public void testClearAddressesOnStopState() throws Exception {
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_CLEAR_ADDRESSES_ON_STOP_VERSION, true);
+        mNetworkAgentThread =
+                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
+        mNetworkAgentThread.start();
+
+        final LinkProperties lp = doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));
+
+        // Verify the link addresses and IPv6 routes get removed before transition to StoppedState..
+        mNetworkAgent.unregister();
+        mNetworkAgentThread.quitSafely();
+        mIIpClient.shutdown();
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.getAddresses().size() == 0
+                        && !x.hasIpv6DefaultRoute()
+                        && x.getDnsServers().size() == 0));
+        awaitIpClientShutdown();
+    }
+
+    @Test
+    public void testMulticastNsFromIPv6Gua() throws Exception {
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION,
+                true /* isUnsolicitedNsEnabled */);
+        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION, false));
+        startIpClientProvisioning(config);
+
+        doIpv6OnlyProvisioning();
+
+        final List<NeighborSolicitation> nsList = new ArrayList<>();
+        NeighborSolicitation packet;
+        while ((packet = getNextNeighborSolicitation()) != null) {
+            // Filter out the NSes used for duplicate address detetction, whose target address
+            // is the global IPv6 address inside these NSes.
+            if (packet.nsHdr.target.isLinkLocalAddress()) {
+                assertMulticastNsFromIpv6Gua(packet);
+                nsList.add(packet);
+            }
+        }
+        assertEquals(2, nsList.size()); // from privacy address and stable privacy address
+    }
 }
diff --git a/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt b/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
index 9f5e5e7..9c40eff 100644
--- a/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
+++ b/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
@@ -31,9 +31,10 @@
  */
 class TestNetworkStackServiceClient private constructor() : NetworkStackClientBase() {
     companion object {
+        private val testNetworkStackServiceAction = "android.net.INetworkStackConnector.Test"
         private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
         private val networkStackVersion by lazy {
-            val component = getNetworkStackComponent(INetworkStackConnector::class.java.name)
+            val component = getNetworkStackComponent(testNetworkStackServiceAction)
             val info = context.packageManager.getPackageInfo(component.packageName, 0 /* flags */)
             info.longVersionCode
         }
@@ -61,16 +62,22 @@
         }
     }
 
+    // Inner class defined in subclass cannot access the protected methods of parent class directly,
+    // so have a method outside of the inner class: serviceConnection and call this method instead.
+    private fun onNetworkStackConnected(service: IBinder) {
+        onNetworkStackConnected(INetworkStackConnector.Stub.asInterface(service))
+    }
+
     private val serviceConnection = object : ServiceConnection {
         override fun onServiceConnected(name: ComponentName, service: IBinder) {
-            onNetworkStackConnected(INetworkStackConnector.Stub.asInterface(service))
+            onNetworkStackConnected(service)
         }
 
         override fun onServiceDisconnected(name: ComponentName) = Unit
     }
 
     private fun init() {
-        val bindIntent = Intent(INetworkStackConnector::class.java.name + ".Test")
+        val bindIntent = Intent(testNetworkStackServiceAction)
         bindIntent.component = getNetworkStackComponent(bindIntent.action)
         context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)
     }
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index 10b55d5..3800752 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -261,6 +261,8 @@
         }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any())
         doReturn(mIpReachabilityMonitorMetrics)
                 .`when`(dependencies).getIpReachabilityMonitorMetrics()
+        doReturn(true).`when`(dependencies).isFeatureEnabled(anyObject(),
+                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION), anyBoolean())
 
         val monitorFuture = CompletableFuture<IpReachabilityMonitor>()
         // IpReachabilityMonitor needs to be started from the handler thread
@@ -348,9 +350,6 @@
         newLp: LinkProperties,
         neighbor: InetAddress
     ) {
-        doReturn(true).`when`(dependencies).isFeatureEnabled(anyObject(),
-                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION), anyBoolean())
-
         reachabilityMonitor.updateLinkProperties(newLp)
 
         neighborMonitor.enqueuePacket(makeNewNeighMessage(neighbor, NUD_REACHABLE,
diff --git a/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt b/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt
deleted file mode 100644
index 5ca20d8..0000000
--- a/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt
+++ /dev/null
@@ -1,375 +0,0 @@
-package android.net.testutils
-
-import android.annotation.SuppressLint
-import android.net.LinkAddress
-import android.net.LinkProperties
-import android.net.Network
-import android.net.NetworkCapabilities
-import com.android.testutils.ConcurrentInterpreter
-import com.android.testutils.InterpretMatcher
-import com.android.testutils.RecorderCallback.CallbackEntry
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.AVAILABLE
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.BLOCKED_STATUS
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LINK_PROPERTIES_CHANGED
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOSING
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOST
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
-import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
-import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
-import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.intArg
-import com.android.testutils.strArg
-import com.android.testutils.timeArg
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import kotlin.reflect.KClass
-import kotlin.test.assertEquals
-import kotlin.test.assertFails
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-const val SHORT_TIMEOUT_MS = 20L
-const val DEFAULT_LINGER_DELAY_MS = 30000
-const val NOT_METERED = NetworkCapabilities.NET_CAPABILITY_NOT_METERED
-const val WIFI = NetworkCapabilities.TRANSPORT_WIFI
-const val CELLULAR = NetworkCapabilities.TRANSPORT_CELLULAR
-const val TEST_INTERFACE_NAME = "testInterfaceName"
-
-@RunWith(JUnit4::class)
-@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
-class TestableNetworkCallbackTest {
-    private lateinit var mCallback: TestableNetworkCallback
-
-    private fun makeHasNetwork(netId: Int) = object : TestableNetworkCallback.HasNetwork {
-        override val network: Network = Network(netId)
-    }
-
-    @Before
-    fun setUp() {
-        mCallback = TestableNetworkCallback()
-    }
-
-    @Test
-    fun testLastAvailableNetwork() {
-        // Make sure there is no last available network at first, then the last available network
-        // is returned after onAvailable is called.
-        val net2097 = Network(2097)
-        assertNull(mCallback.lastAvailableNetwork)
-        mCallback.onAvailable(net2097)
-        assertEquals(mCallback.lastAvailableNetwork, net2097)
-
-        // Make sure calling onCapsChanged/onLinkPropertiesChanged don't affect the last available
-        // network.
-        mCallback.onCapabilitiesChanged(net2097, NetworkCapabilities())
-        mCallback.onLinkPropertiesChanged(net2097, LinkProperties())
-        assertEquals(mCallback.lastAvailableNetwork, net2097)
-
-        // Make sure onLost clears the last available network.
-        mCallback.onLost(net2097)
-        assertNull(mCallback.lastAvailableNetwork)
-
-        // Do the same but with a different network after onLost : make sure the last available
-        // network is the new one, not the original one.
-        val net2098 = Network(2098)
-        mCallback.onAvailable(net2098)
-        mCallback.onCapabilitiesChanged(net2098, NetworkCapabilities())
-        mCallback.onLinkPropertiesChanged(net2098, LinkProperties())
-        assertEquals(mCallback.lastAvailableNetwork, net2098)
-
-        // Make sure onAvailable changes the last available network even if onLost was not called.
-        val net2099 = Network(2099)
-        mCallback.onAvailable(net2099)
-        assertEquals(mCallback.lastAvailableNetwork, net2099)
-
-        // For legacy reasons, lastAvailableNetwork is null as soon as any is lost, not necessarily
-        // the last available one. Check that behavior.
-        mCallback.onLost(net2098)
-        assertNull(mCallback.lastAvailableNetwork)
-
-        // Make sure that losing the really last available one still results in null.
-        mCallback.onLost(net2099)
-        assertNull(mCallback.lastAvailableNetwork)
-
-        // Make sure multiple onAvailable in a row then onLost still results in null.
-        mCallback.onAvailable(net2097)
-        mCallback.onAvailable(net2098)
-        mCallback.onAvailable(net2099)
-        mCallback.onLost(net2097)
-        assertNull(mCallback.lastAvailableNetwork)
-    }
-
-    @Test
-    fun testAssertNoCallback() {
-        mCallback.assertNoCallback(SHORT_TIMEOUT_MS)
-        mCallback.onAvailable(Network(100))
-        assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) }
-    }
-
-    @Test
-    fun testAssertNoCallbackThat() {
-        val net = Network(101)
-        mCallback.assertNoCallbackThat { it is Available }
-        mCallback.onAvailable(net)
-        // Expect no blocked status change. Receive other callback does not fail the test.
-        mCallback.assertNoCallbackThat { it is BlockedStatus }
-        mCallback.onBlockedStatusChanged(net, true)
-        assertFails { mCallback.assertNoCallbackThat { it is BlockedStatus } }
-        mCallback.onBlockedStatusChanged(net, false)
-        mCallback.onCapabilitiesChanged(net, NetworkCapabilities())
-        assertFails { mCallback.assertNoCallbackThat { it is CapabilitiesChanged } }
-    }
-
-    @Test
-    fun testCapabilitiesWithAndWithout() {
-        val net = Network(101)
-        val matcher = makeHasNetwork(101)
-        val meteredNc = NetworkCapabilities()
-        val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED)
-        // Check that expecting caps (with or without) fails when no callback has been received.
-        assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
-        assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
-
-        // Add NOT_METERED and check that With succeeds and Without fails.
-        mCallback.onCapabilitiesChanged(net, unmeteredNc)
-        mCallback.expectCapabilitiesWith(NOT_METERED, matcher)
-        mCallback.onCapabilitiesChanged(net, unmeteredNc)
-        assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
-
-        // Don't add NOT_METERED and check that With fails and Without succeeds.
-        mCallback.onCapabilitiesChanged(net, meteredNc)
-        assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
-        mCallback.onCapabilitiesChanged(net, meteredNc)
-        mCallback.expectCapabilitiesWithout(NOT_METERED, matcher)
-    }
-
-    @Test
-    fun testExpectCallbackThat() {
-        val net = Network(193)
-        val netCaps = NetworkCapabilities().addTransportType(CELLULAR)
-        // Check that expecting callbackThat anything fails when no callback has been received.
-        assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { true } }
-
-        // Basic test for true and false
-        mCallback.onAvailable(net)
-        mCallback.expectCallbackThat { true }
-        mCallback.onAvailable(net)
-        assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { false } }
-
-        // Try a positive and a negative case
-        mCallback.onBlockedStatusChanged(net, true)
-        mCallback.expectCallbackThat { cb -> cb is BlockedStatus && cb.blocked }
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { cb ->
-            cb is CapabilitiesChanged && cb.caps.hasTransport(WIFI)
-        } }
-    }
-
-    @Test
-    fun testCapabilitiesThat() {
-        val net = Network(101)
-        val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI)
-        // Check that expecting capabilitiesThat anything fails when no callback has been received.
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } }
-
-        // Basic test for true and false
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        mCallback.expectCapabilitiesThat(net) { true }
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } }
-
-        // Try a positive and a negative case
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        mCallback.expectCapabilitiesThat(net) { caps ->
-            caps.hasCapability(NOT_METERED) &&
-                    caps.hasTransport(WIFI) &&
-                    !caps.hasTransport(CELLULAR)
-        }
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps ->
-            caps.hasTransport(CELLULAR)
-        } }
-
-        // Try a matching callback on the wrong network
-        mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } }
-    }
-
-    @Test
-    fun testLinkPropertiesThat() {
-        val net = Network(112)
-        val linkAddress = LinkAddress("fe80::ace:d00d/64")
-        val mtu = 1984
-        val linkProps = LinkProperties().apply {
-            this.mtu = mtu
-            interfaceName = TEST_INTERFACE_NAME
-            addLinkAddress(linkAddress)
-        }
-
-        // Check that expecting linkPropsThat anything fails when no callback has been received.
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } }
-
-        // Basic test for true and false
-        mCallback.onLinkPropertiesChanged(net, linkProps)
-        mCallback.expectLinkPropertiesThat(net) { true }
-        mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } }
-
-        // Try a positive and negative case
-        mCallback.onLinkPropertiesChanged(net, linkProps)
-        mCallback.expectLinkPropertiesThat(net) { lp ->
-            lp.interfaceName == TEST_INTERFACE_NAME &&
-                    lp.linkAddresses.contains(linkAddress) &&
-                    lp.mtu == mtu
-        }
-        mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp ->
-            lp.interfaceName != TEST_INTERFACE_NAME
-        } }
-
-        // Try a matching callback on the wrong network
-        mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp ->
-            lp.interfaceName == TEST_INTERFACE_NAME
-        } }
-    }
-
-    @Test
-    fun testExpectCallback() {
-        val net = Network(103)
-        // Test expectCallback fails when nothing was sent.
-        assertFails { mCallback.expectCallback<BlockedStatus>(net, SHORT_TIMEOUT_MS) }
-
-        // Test onAvailable is seen and can be expected
-        mCallback.onAvailable(net)
-        mCallback.expectCallback<Available>(net, SHORT_TIMEOUT_MS)
-
-        // Test onAvailable won't return calls with a different network
-        mCallback.onAvailable(Network(106))
-        assertFails { mCallback.expectCallback<Available>(net, SHORT_TIMEOUT_MS) }
-
-        // Test onAvailable won't return calls with a different callback
-        mCallback.onAvailable(net)
-        assertFails { mCallback.expectCallback<BlockedStatus>(net, SHORT_TIMEOUT_MS) }
-    }
-
-    @Test
-    fun testPollForNextCallback() {
-        assertFails { mCallback.pollForNextCallback(SHORT_TIMEOUT_MS) }
-        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
-                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
-            sleep; onAvailable(133)    | poll(2) = Available(133) time 1..4
-                                       | poll(1) fails
-            onCapabilitiesChanged(108) | poll(1) = CapabilitiesChanged(108) time 0..3
-            onBlockedStatus(199)       | poll(1) = BlockedStatus(199) time 0..3
-        """)
-    }
-
-    @Test
-    fun testEventuallyExpect() {
-        // TODO: Current test does not verify the inline one. Also verify the behavior after
-        // aligning two eventuallyExpect()
-        val net1 = Network(100)
-        val net2 = Network(101)
-        mCallback.onAvailable(net1)
-        mCallback.onCapabilitiesChanged(net1, NetworkCapabilities())
-        mCallback.onLinkPropertiesChanged(net1, LinkProperties())
-        mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) {
-            net1.equals(it.network)
-        }
-        // No further new callback. Expect no callback.
-        assertFails { mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) }
-
-        // Verify no predicate set.
-        mCallback.onAvailable(net2)
-        mCallback.onLinkPropertiesChanged(net2, LinkProperties())
-        mCallback.onBlockedStatusChanged(net1, false)
-        mCallback.eventuallyExpect(BLOCKED_STATUS) { net1.equals(it.network) }
-        // Verify no callback received if the callback does not happen.
-        assertFails { mCallback.eventuallyExpect(LOSING) }
-    }
-
-    @Test
-    fun testEventuallyExpectOnMultiThreads() {
-        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
-                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
-                onAvailable(100)                   | eventually(CapabilitiesChanged(100), 1) fails
-                sleep ; onCapabilitiesChanged(100) | eventually(CapabilitiesChanged(100), 2)
-                onAvailable(101) ; onBlockedStatus(101) | eventually(BlockedStatus(100), 2) fails
-                onSuspended(100) ; sleep ; onLost(100)  | eventually(Lost(100), 2)
-        """)
-    }
-}
-
-private object TNCInterpreter : ConcurrentInterpreter<TestableNetworkCallback>(interpretTable)
-
-val EntryList = CallbackEntry::class.sealedSubclasses.map { it.simpleName }.joinToString("|")
-private fun callbackEntryFromString(name: String): KClass<out CallbackEntry> {
-    return CallbackEntry::class.sealedSubclasses.first { it.simpleName == name }
-}
-
-@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
-private val interpretTable = listOf<InterpretMatcher<TestableNetworkCallback>>(
-    // Interpret "Available(xx)" as "call to onAvailable with netId xx", and likewise for
-    // all callback types. This is implemented above by enumerating the subclasses of
-    // CallbackEntry and reading their simpleName.
-    Regex("""(.*)\s+=\s+($EntryList)\((\d+)\)""") to { i, cb, t ->
-        val record = i.interpret(t.strArg(1), cb)
-        assertTrue(callbackEntryFromString(t.strArg(2)).isInstance(record))
-        // Strictly speaking testing for is CallbackEntry is useless as it's been tested above
-        // but the compiler can't figure things out from the isInstance call. It does understand
-        // from the assertTrue(is CallbackEntry) that this is true, which allows to access
-        // the 'network' member below.
-        assertTrue(record is CallbackEntry)
-        assertEquals(record.network.netId, t.intArg(3))
-    },
-    // Interpret "onAvailable(xx)" as calling "onAvailable" with a netId of xx, and likewise for
-    // all callback types. NetworkCapabilities and LinkProperties just get an empty object
-    // as their argument. Losing gets the default linger timer. Blocked gets false.
-    Regex("""on($EntryList)\((\d+)\)""") to { i, cb, t ->
-        val net = Network(t.intArg(2))
-        when (t.strArg(1)) {
-            "Available" -> cb.onAvailable(net)
-            // PreCheck not used in tests. Add it here if it becomes useful.
-            "CapabilitiesChanged" -> cb.onCapabilitiesChanged(net, NetworkCapabilities())
-            "LinkPropertiesChanged" -> cb.onLinkPropertiesChanged(net, LinkProperties())
-            "Suspended" -> cb.onNetworkSuspended(net)
-            "Resumed" -> cb.onNetworkResumed(net)
-            "Losing" -> cb.onLosing(net, DEFAULT_LINGER_DELAY_MS)
-            "Lost" -> cb.onLost(net)
-            "Unavailable" -> cb.onUnavailable()
-            "BlockedStatus" -> cb.onBlockedStatusChanged(net, false)
-            else -> fail("Unknown callback type")
-        }
-    },
-    Regex("""poll\((\d+)\)""") to { i, cb, t ->
-        cb.pollForNextCallback(t.timeArg(1))
-    },
-    // Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects
-    // CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and
-    // likewise for all callback types.
-    Regex("""eventually\(($EntryList)\((\d+)\),\s+(\d+)\)""") to { i, cb, t ->
-        val net = Network(t.intArg(2))
-        val timeout = t.timeArg(3)
-        when (t.strArg(1)) {
-            "Available" -> cb.eventuallyExpect(AVAILABLE, timeout) { net == it.network }
-            "Suspended" -> cb.eventuallyExpect(SUSPENDED, timeout) { net == it.network }
-            "Resumed" -> cb.eventuallyExpect(RESUMED, timeout) { net == it.network }
-            "Losing" -> cb.eventuallyExpect(LOSING, timeout) { net == it.network }
-            "Lost" -> cb.eventuallyExpect(LOST, timeout) { net == it.network }
-            "Unavailable" -> cb.eventuallyExpect(UNAVAILABLE, timeout) { net == it.network }
-            "BlockedStatus" -> cb.eventuallyExpect(BLOCKED_STATUS, timeout) { net == it.network }
-            "CapabilitiesChanged" ->
-                cb.eventuallyExpect(NETWORK_CAPS_UPDATED, timeout) { net == it.network }
-            "LinkPropertiesChanged" ->
-                cb.eventuallyExpect(LINK_PROPERTIES_CHANGED, timeout) { net == it.network }
-            else -> fail("Unknown callback type")
-        }
-    }
-)