Snap for 7259092 from b2e801082912eb6dfbf814a40184796a5d94f48e to mainline-media-swcodec-release

Change-Id: I8d74fd1e8978d48832f660c53a25bb28258fad16
diff --git a/TEST_MAPPING b/TEST_MAPPING
index a2ed850..178026f 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -15,6 +15,26 @@
       "name": "NetworkStackIntegrationTests"
     }
   ],
+  "auto-postsubmit": [
+    // Test tag for automotive targets. These are only running in postsubmit so as to harden the
+    // automotive targets to avoid introducing additional test flake and build time. The plan for
+    // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
+    // Additionally, this tag is used in targeted test suites to limit resource usage on the test
+    // infra during the hardening phase.
+    // TODO: this tag to be removed once the above is no longer an issue.
+    {
+      "name": "NetworkStackTests"
+    },
+    {
+      "name": "NetworkStackNextTests"
+    },
+    {
+      "name": "NetworkStackHostTests"
+    },
+    {
+      "name": "NetworkStackIntegrationTests"
+    }
+  ],
   "mainline-presubmit": [
     // These are unit tests only, so they don't actually require any modules to be installed.
     // We must specify at least one module here or the tests won't run. Use the same set as CTS
diff --git a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
index 9e27666..8719e83 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
@@ -51,7 +51,7 @@
     }
 
     @Override
-    public String getVenueFriendlyName() {
+    public CharSequence getVenueFriendlyName() {
         // Not supported in API level 29
         return null;
     }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java
index e504495..3c13a56 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java
@@ -22,7 +22,6 @@
 import android.os.Handler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
@@ -45,7 +44,7 @@
      */
     @Override
     public void requestBackgroundNetwork(@NonNull NetworkRequest request,
-            @Nullable Handler handler, @NonNull NetworkCallback networkCallback)
+            @NonNull Handler handler, @NonNull NetworkCallback networkCallback)
             throws UnsupportedApiLevelException {
         // Not supported for API 29.
         throw new UnsupportedApiLevelException("Not supported in API 29.");
diff --git a/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..0c1d837
--- /dev/null
+++ b/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api29;
+
+import android.net.NetworkRequest;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+
+import java.util.Set;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 29.
+ */
+public class NetworkRequestShimImpl implements NetworkRequestShim {
+    protected NetworkRequestShimImpl() {}
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    public static NetworkRequestShim newInstance() {
+        return new NetworkRequestShimImpl();
+    }
+
+    @Override
+    public void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) throws UnsupportedApiLevelException {
+        // Not supported before API 31.
+        throw new UnsupportedApiLevelException("Not supported before API 31.");
+    }
+}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java
index ac62522..c1e3561 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java
@@ -23,7 +23,6 @@
 import android.os.Handler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.networkstack.apishim.common.ShimUtils;
@@ -55,7 +54,7 @@
      */
     @Override
     public void requestBackgroundNetwork(@NonNull NetworkRequest request,
-            @Nullable Handler handler, @NonNull NetworkCallback networkCallback)
+            @NonNull Handler handler, @NonNull NetworkCallback networkCallback)
             throws UnsupportedApiLevelException {
         // Not supported for API 30.
         throw new UnsupportedApiLevelException("Not supported in API 30.");
diff --git a/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..b65a556
--- /dev/null
+++ b/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api30;
+
+import android.os.Build;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 30.
+ */
+public class NetworkRequestShimImpl
+        extends com.android.networkstack.apishim.api29.NetworkRequestShimImpl {
+    protected NetworkRequestShimImpl() {
+        super();
+    }
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    public static NetworkRequestShim newInstance() {
+        if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) {
+            return com.android.networkstack.apishim.api29.NetworkRequestShimImpl
+                    .newInstance();
+        }
+        return new NetworkRequestShimImpl();
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java b/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
index 6e3eb19..5af7412 100644
--- a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
@@ -33,7 +33,7 @@
     }
 
     @Override
-    public String getVenueFriendlyName() {
+    public CharSequence getVenueFriendlyName() {
         return mData.getVenueFriendlyName();
     }
 
diff --git a/apishim/31/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java b/apishim/31/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java
index 0c3fa3e..a62a9db 100644
--- a/apishim/31/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java
@@ -24,7 +24,6 @@
 import android.os.Handler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.networkstack.apishim.common.ShimUtils;
@@ -57,7 +56,7 @@
      */
     @Override
     public void requestBackgroundNetwork(@NonNull NetworkRequest request,
-            @Nullable Handler handler, @NonNull NetworkCallback networkCallback) {
+            @NonNull Handler handler, @NonNull NetworkCallback networkCallback) {
         mCm.requestBackgroundNetwork(request, handler, networkCallback);
     }
 
diff --git a/apishim/31/com/android/networkstack/apishim/NetworkRequestShimImpl.java b/apishim/31/com/android/networkstack/apishim/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..b0d5827
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/NetworkRequestShimImpl.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim;
+
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
+import java.util.Set;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 31.
+ */
+public class NetworkRequestShimImpl
+        extends com.android.networkstack.apishim.api30.NetworkRequestShimImpl {
+    protected NetworkRequestShimImpl() {
+        super();
+    }
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    public static NetworkRequestShim newInstance() {
+        if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R)) {
+            return com.android.networkstack.apishim.api30.NetworkRequestShimImpl
+                    .newInstance();
+        }
+        return new NetworkRequestShimImpl();
+    }
+
+    @Override
+    public void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) {
+        builder.setUids(uids);
+    }
+}
diff --git a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
index 26ac9d4..13bf257 100644
--- a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
@@ -54,7 +54,7 @@
     /**
      * @see CaptivePortalData#getVenueFriendlyName()
      */
-    String getVenueFriendlyName();
+    CharSequence getVenueFriendlyName();
 
     /**
      * @see CaptivePortalData#getUserPortalUrlSource()
diff --git a/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java b/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java
index 2d60d97..beb5f1d 100644
--- a/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java
@@ -21,8 +21,6 @@
 import android.os.Handler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 
 /**
  * Interface used to access API methods in {@link android.net.ConnectivityManager}, with
@@ -36,7 +34,7 @@
 public interface ConnectivityManagerShim {
     /** See android.net.ConnectivityManager#requestBackgroundNetwork */
     void requestBackgroundNetwork(@NonNull NetworkRequest request,
-            @Nullable Handler handler, @NonNull NetworkCallback networkCallback)
+            @NonNull Handler handler, @NonNull NetworkCallback networkCallback)
             throws UnsupportedApiLevelException;
 
     /** See android.net.ConnectivityManager#registerSystemDefaultNetworkCallback */
diff --git a/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java b/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java
new file mode 100644
index 0000000..7b53efa
--- /dev/null
+++ b/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.common;
+
+import android.net.NetworkRequest;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+
+/**
+ * Interface used to access API methods in {@link android.net.NetworkRequest}, with
+ * appropriate fallbacks if the methods are not yet part of the released API.
+ */
+public interface NetworkRequestShim {
+    /**
+     * See android.net.NetworkRequest.Builder#setUids.
+     * Set the {@code uids} into {@code builder}.
+     */
+    void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) throws UnsupportedApiLevelException;
+}
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 527d895..7b54302 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -17,9 +17,9 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="notification_channel_name_connected" msgid="1795068343200033922">"Verificatie van captive portal"</string>
-    <string name="notification_channel_description_connected" msgid="7239184168268014518">"Er worden meldingen weergegeven als het apparaat is geverifieerd voor een captive portal-netwerk"</string>
+    <string name="notification_channel_description_connected" msgid="7239184168268014518">"Er worden meldingen getoond als het apparaat is geverifieerd voor een captive portal-netwerk"</string>
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"Netwerklocatie-informatie"</string>
-    <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Er worden meldingen weergegeven om aan te geven dat het netwerk een locatie-informatiepagina heeft"</string>
+    <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Er worden meldingen getoond om aan te geven dat het netwerk een locatie-informatiepagina heeft"</string>
     <string name="connected" msgid="4563643884927480998">"Verbonden"</string>
     <string name="tap_for_info" msgid="6849746325626883711">"Verbonden / Tik om de website te bekijken"</string>
     <string name="application_label" msgid="1322847171305285454">"Netwerkbeheer"</string>
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 5f77128..8e0e9d3 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -97,9 +97,9 @@
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.internal.util.TrafficStatsConstants;
 import com.android.internal.util.WakeupMessage;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.PacketReader;
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
@@ -611,7 +611,7 @@
 
     private boolean initUdpSocket() {
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_DHCP);
+                NetworkStackConstants.TAG_SYSTEM_DHCP);
         try {
             mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
             SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java
index 3acd76e..3465e72 100644
--- a/src/android/net/dhcp/DhcpServer.java
+++ b/src/android/net/dhcp/DhcpServer.java
@@ -33,12 +33,12 @@
 import static android.system.OsConstants.SO_BROADCAST;
 import static android.system.OsConstants.SO_REUSEADDR;
 
-import static com.android.internal.util.TrafficStatsConstants.TAG_SYSTEM_DHCP_SERVER;
 import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
 import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
 import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_DHCP_SERVER;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import static java.lang.Integer.toUnsignedLong;
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index a73e997..980c6d6 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -18,8 +18,14 @@
 
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
+import static android.net.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
 
+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.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
@@ -52,6 +58,7 @@
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo.InformationElement;
 import android.net.util.InterfaceParams;
+import android.net.util.NetworkStackUtils;
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.ConditionVariable;
@@ -61,6 +68,8 @@
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.stats.connectivity.DisconnectCode;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
@@ -79,16 +88,21 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.apishim.SocketUtilsShimImpl;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
+import com.android.networkstack.packets.NeighborAdvertisement;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.MalformedURLException;
+import java.net.SocketAddress;
+import java.net.SocketException;
 import java.net.URL;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -479,6 +493,8 @@
     private final MessageHandlingLogger mMsgStateLogger;
     private final IpConnectivityLog mMetricsLog;
     private final InterfaceController mInterfaceCtrl;
+    // Set of IPv6 addresses for which unsolicited gratuitous NA packets have been sent.
+    private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
 
     // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
     private final int mMinRdnssLifetimeSec;
@@ -564,6 +580,16 @@
         public IpConnectivityLog getIpConnectivityLog() {
             return new IpConnectivityLog();
         }
+
+        /**
+         * Return whether a feature guarded by a feature flag is enabled.
+         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
+         */
+        public boolean isFeatureEnabled(final Context context, final String name,
+                boolean defaultEnabled) {
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
+                    defaultEnabled);
+        }
     }
 
     public IpClient(Context context, String ifName, IIpClientCallbacks callback,
@@ -647,6 +673,21 @@
                 logMsg(msg);
             }
 
+            @Override
+            public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
+                super.onInterfaceAddressRemoved(address, iface);
+                if (!mInterfaceName.equals(iface)) return;
+                if (!address.isIpv6()) return;
+                final Inet6Address targetIp = (Inet6Address) address.getAddress();
+                if (mGratuitousNaTargetAddresses.contains(targetIp)) {
+                    mGratuitousNaTargetAddresses.remove(targetIp);
+
+                    final String msg = "Global IPv6 address: " + targetIp
+                            + " has removed from the set of gratuitous NA target address.";
+                    logMsg(msg);
+                }
+            }
+
             private void logMsg(String msg) {
                 Log.d(mTag, msg);
                 getHandler().post(() -> mLog.log("OBSERVED " + msg));
@@ -793,6 +834,11 @@
         mLinkObserver.shutdown();
     }
 
+    private boolean isGratuitousNaEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION,
+                false /* defaultEnabled */);
+    }
+
     @Override
     protected void onQuitting() {
         mCallback.onQuit();
@@ -1377,6 +1423,57 @@
         }
     }
 
+    private void sendGratuitousNA(final Inet6Address srcIp, final Inet6Address target) {
+        final int flags = 0; // R=0, S=0, O=0
+        final Inet6Address dstIp = IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+        // Ethernet multicast destination address: 33:33:00:00:00:02.
+        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
+        final ByteBuffer packet = NeighborAdvertisement.build(mInterfaceParams.macAddr, dstMac,
+                srcIp, dstIp, flags, target);
+        FileDescriptor sock = null;
+        try {
+            sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
+            final SocketAddress sockAddress =
+                    SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
+                            mInterfaceParams.index, dstMac.toByteArray());
+            Os.sendto(sock, packet.array(), 0 /* byteOffset */, packet.limit() /* byteCount */,
+                    0 /* flags */, sockAddress);
+        } catch (SocketException | ErrnoException e) {
+            logError("Fail to send Gratuitous Neighbor Advertisement", e);
+        } finally {
+            NetworkStackUtils.closeSocketQuietly(sock);
+        }
+    }
+
+    private Inet6Address getIpv6LinkLocalAddress(final LinkProperties newLp) {
+        Inet6Address src = null;
+        for (LinkAddress la : newLp.getLinkAddresses()) {
+            if (!la.isIpv6()) continue;
+            final Inet6Address ip = (Inet6Address) la.getAddress();
+            if (ip.isLinkLocalAddress()) return ip;
+        }
+        return null;
+    }
+
+    private void maybeSendGratuitousNAs(final LinkProperties newLp) {
+        if (!newLp.hasGlobalIpv6Address() || !isGratuitousNaEnabled()) return;
+
+        final Inet6Address src = getIpv6LinkLocalAddress(newLp);
+        if (src == null) return;
+        for (LinkAddress la : newLp.getLinkAddresses()) {
+            if (!la.isIpv6() || !la.isGlobalPreferred()) continue;
+            final Inet6Address targetIp = (Inet6Address) la.getAddress();
+            // Already sent gratuitous NA with this target global IPv6 address.
+            if (mGratuitousNaTargetAddresses.contains(targetIp)) continue;
+            if (DBG) {
+                Log.d(mTag, "send Gratuitous NA from " + src + ", target Address is "
+                            + targetIp);
+            }
+            sendGratuitousNA(src, targetIp);
+            mGratuitousNaTargetAddresses.add(targetIp);
+        }
+    }
+
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
@@ -1384,6 +1481,11 @@
             return true;
         }
 
+        // Check if new assigned IPv6 GUA is available in the LinkProperties now. If so, initiate
+        // gratuitous multicast unsolicited Neighbor Advertisements as soon as possible to inform
+        // first-hop routers that the new GUA host is goning to use.
+        maybeSendGratuitousNAs(newLp);
+
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
         mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
@@ -1662,6 +1764,7 @@
         public void enter() {
             stopAllIP();
             mHasDisabledIPv6OnProvLoss = false;
+            mGratuitousNaTargetAddresses.clear();
 
             mLinkObserver.clearInterfaceParams();
             resetLinkProperties();
diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index 5c11733..6e32833 100755
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -17,12 +17,16 @@
 package android.net.util;
 
 import android.content.Context;
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
 
 import com.android.net.module.util.DeviceConfigUtils;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.SocketException;
 
 /**
@@ -232,6 +236,12 @@
      */
     public static final String VALIDATION_METRICS_VERSION = "validation_metrics_version";
 
+    /**
+     * Experiment flag to enable sending gratuitous multicast unsolicited Neighbor Advertisements
+     * to propagate new assigned IPv6 GUA as quickly as possible.
+     */
+    public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
@@ -247,6 +257,21 @@
     }
 
     /**
+     * Convert IPv6 multicast address to ethernet multicast address in network order.
+     */
+    public static MacAddress ipv6MulticastToEthernetMulticast(@NonNull final Inet6Address addr) {
+        final byte[] etherMulticast = new byte[6];
+        final byte[] ipv6Multicast = addr.getAddress();
+        etherMulticast[0] = (byte) 0x33;
+        etherMulticast[1] = (byte) 0x33;
+        etherMulticast[2] = ipv6Multicast[12];
+        etherMulticast[3] = ipv6Multicast[13];
+        etherMulticast[4] = ipv6Multicast[14];
+        etherMulticast[5] = ipv6Multicast[15];
+        return MacAddress.fromBytes(etherMulticast);
+    }
+
+    /**
      * Attaches a socket filter that accepts DHCP packets to the given socket.
      */
     public static native void attachDhcpFilter(FileDescriptor fd) throws SocketException;
diff --git a/src/com/android/networkstack/NetworkStackNotifier.java b/src/com/android/networkstack/NetworkStackNotifier.java
index 0558d3a..acf3c95 100644
--- a/src/com/android/networkstack/NetworkStackNotifier.java
+++ b/src/com/android/networkstack/NetworkStackNotifier.java
@@ -237,8 +237,8 @@
 
             // If the venue friendly name is available (in Passpoint use-case), display it.
             // Otherwise, display the SSID.
-            final String friendlyName = capportData.getVenueFriendlyName();
-            final String venueDisplayName = TextUtils.isEmpty(friendlyName)
+            final CharSequence friendlyName = capportData.getVenueFriendlyName();
+            final CharSequence venueDisplayName = TextUtils.isEmpty(friendlyName)
                     ? getSsid(networkStatus) : friendlyName;
 
             builder = getNotificationBuilder(channel, networkStatus, res, venueDisplayName)
@@ -284,9 +284,9 @@
 
     private Notification.Builder getNotificationBuilder(@NonNull String channelId,
             @NonNull TrackedNetworkStatus networkStatus, @NonNull Resources res,
-            @NonNull String ssid) {
+            @NonNull CharSequence networkIdentifier) {
         return new Notification.Builder(mContext, channelId)
-                .setContentTitle(ssid)
+                .setContentTitle(networkIdentifier)
                 .setSmallIcon(R.drawable.icon_wifi);
     }
 
diff --git a/src/com/android/networkstack/util/DnsUtils.java b/src/com/android/networkstack/util/DnsUtils.java
index 83f2daf..622f56a 100644
--- a/src/com/android/networkstack/util/DnsUtils.java
+++ b/src/com/android/networkstack/util/DnsUtils.java
@@ -29,7 +29,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.internal.util.TrafficStatsConstants;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.server.connectivity.NetworkMonitor.DnsLogFunc;
 
 import java.net.InetAddress;
@@ -126,7 +126,7 @@
         // look at the tag at all. Given that this is a library, the tag should be passed in by the
         // caller.
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_PROBE);
+                NetworkStackConstants.TAG_SYSTEM_PROBE);
 
         if (type == TYPE_ADDRCONFIG) {
             dnsResolver.query(network, host, flag, r -> r.run(), null /* cancellationSignal */,
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index b4a3ae9..94be13e 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -161,8 +161,8 @@
 import com.android.internal.util.RingBufferIndices;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.internal.util.TrafficStatsConstants;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.networkstack.NetworkStackNotifier;
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
@@ -2463,7 +2463,7 @@
         String redirectUrl = null;
         final Stopwatch probeTimer = new Stopwatch().start();
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_PROBE);
+                NetworkStackConstants.TAG_SYSTEM_PROBE);
         try {
             // Follow redirects for PAC probes as such probes verify connectivity by fetching the
             // PAC proxy file, which may be configured behind a redirect.
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
index 9f29a2e..eb0a799 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
@@ -17,6 +17,7 @@
 package android.net.ip
 
 import android.net.ipmemorystore.NetworkAttributes
+import android.util.ArrayMap
 import org.mockito.Mockito.any
 import org.mockito.ArgumentCaptor
 import org.mockito.Mockito.eq
@@ -28,22 +29,20 @@
  * Tests for IpClient, run with signature permissions.
  */
 class IpClientIntegrationTest : IpClientIntegrationTestCommon() {
+    private val mEnabledFeatures = ArrayMap<String, Boolean>()
+
     override fun makeIIpClient(ifaceName: String, cb: IIpClientCallbacks): IIpClient {
         return mIpc.makeConnector()
     }
 
     override fun useNetworkStackSignature() = true
 
-    override fun setDhcpFeatures(
-        isDhcpLeaseCacheEnabled: Boolean,
-        isRapidCommitEnabled: Boolean,
-        isDhcpIpConflictDetectEnabled: Boolean,
-        isIPv6OnlyPreferredEnabled: Boolean
-    ) {
-        mDependencies.setDhcpLeaseCacheEnabled(isDhcpLeaseCacheEnabled)
-        mDependencies.setDhcpRapidCommitEnabled(isRapidCommitEnabled)
-        mDependencies.setDhcpIpConflictDetectEnabled(isDhcpIpConflictDetectEnabled)
-        mDependencies.setIPv6OnlyPreferredEnabled(isIPv6OnlyPreferredEnabled)
+    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
+        return mEnabledFeatures.get(name) ?: defaultEnabled
+    }
+
+    override fun setFeatureEnabled(name: String, enabled: Boolean) {
+        mEnabledFeatures.put(name, enabled)
     }
 
     override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes {
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
index 451437e..ae68362 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
@@ -39,11 +39,15 @@
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
 
 import static junit.framework.Assert.fail;
 
@@ -141,6 +145,7 @@
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.arp.ArpPacket;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
+import com.android.networkstack.packets.NeighborAdvertisement;
 import com.android.server.NetworkObserver;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -343,32 +348,12 @@
     };
 
     protected class Dependencies extends IpClient.Dependencies {
-        private boolean mIsDhcpLeaseCacheEnabled;
-        private boolean mIsDhcpRapidCommitEnabled;
-        private boolean mIsDhcpIpConflictDetectEnabled;
         // Can't use SparseIntArray, it doesn't have an easy way to know if a key is not present.
         private HashMap<String, Integer> mIntConfigProperties = new HashMap<>();
         private DhcpClient mDhcpClient;
         private boolean mIsHostnameConfigurationEnabled;
         private String mHostname;
         private boolean mIsInterfaceRecovered;
-        private boolean mIsIPv6OnlyPreferredEnabled;
-
-        public void setDhcpLeaseCacheEnabled(final boolean enable) {
-            mIsDhcpLeaseCacheEnabled = enable;
-        }
-
-        public void setDhcpRapidCommitEnabled(final boolean enable) {
-            mIsDhcpRapidCommitEnabled = enable;
-        }
-
-        public void setDhcpIpConflictDetectEnabled(final boolean enable) {
-            mIsDhcpIpConflictDetectEnabled = enable;
-        }
-
-        public void setIPv6OnlyPreferredEnabled(final boolean enable) {
-            mIsIPv6OnlyPreferredEnabled = enable;
-        }
 
         public void setHostnameConfiguration(final boolean enable, final String hostname) {
             mIsHostnameConfigurationEnabled = enable;
@@ -411,25 +396,19 @@
         }
 
         @Override
+        public boolean isFeatureEnabled(final Context context, final String name,
+                final boolean defaultEnabled) {
+            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled);
+        }
+
+        @Override
         public DhcpClient.Dependencies getDhcpClientDependencies(
                 NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
             return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
                 @Override
                 public boolean isFeatureEnabled(final Context context, final String name,
                         final boolean defaultEnabled) {
-                    switch (name) {
-                        case NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION:
-                            return mIsDhcpRapidCommitEnabled;
-                        case NetworkStackUtils.DHCP_INIT_REBOOT_VERSION:
-                            return mIsDhcpLeaseCacheEnabled;
-                        case NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION:
-                            return mIsDhcpIpConflictDetectEnabled;
-                        case NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION:
-                            return mIsIPv6OnlyPreferredEnabled;
-                        default:
-                            fail("Invalid experiment flag: " + name);
-                            return false;
-                    }
+                    return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled);
                 }
 
                 @Override
@@ -473,9 +452,9 @@
     protected abstract IIpClient makeIIpClient(
             @NonNull String ifaceName, @NonNull IIpClientCallbacks cb);
 
-    protected abstract void setDhcpFeatures(boolean isDhcpLeaseCacheEnabled,
-            boolean isRapidCommitEnabled, boolean isDhcpIpConflictDetectEnabled,
-            boolean isIPv6OnlyPreferredEnabled);
+    protected abstract void setFeatureEnabled(String name, boolean enabled);
+
+    protected abstract boolean isFeatureEnabled(String name, boolean defaultEnabled);
 
     protected abstract boolean useNetworkStackSignature();
 
@@ -490,6 +469,17 @@
                 && (mIsSignatureRequiredTest || !TestNetworkStackServiceClient.isSupported());
     }
 
+    protected void setDhcpFeatures(final boolean isDhcpLeaseCacheEnabled,
+            final boolean isRapidCommitEnabled, final boolean isDhcpIpConflictDetectEnabled,
+            final boolean isIPv6OnlyPreferredEnabled) {
+        setFeatureEnabled(NetworkStackUtils.DHCP_INIT_REBOOT_VERSION, isDhcpLeaseCacheEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION, isRapidCommitEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION,
+                isDhcpIpConflictDetectEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION,
+                isIPv6OnlyPreferredEnabled);
+    }
+
     @Before
     public void setUp() throws Exception {
         final Method testMethod = IpClientIntegrationTestCommon.class.getMethod(
@@ -704,6 +694,14 @@
         }
     }
 
+    private NeighborAdvertisement parseNeighborAdvertisementOrNull(final byte[] packet) {
+        try {
+            return NeighborAdvertisement.parse(packet, packet.length);
+        } catch (NeighborAdvertisement.ParseException e) {
+            return null;
+        }
+    }
+
     private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
             final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
             final String captivePortalUrl, final Integer ipv6OnlyWaitTime) {
@@ -1434,6 +1432,24 @@
                         == (byte) ICMPV6_ROUTER_SOLICITATION;
     }
 
+    private boolean isNeighborAdvertisement(final byte[] packetBytes) {
+        ByteBuffer packet = ByteBuffer.wrap(packetBytes);
+        return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
+                && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
+                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN)
+                        == (byte) ICMPV6_NEIGHBOR_ADVERTISEMENT;
+    }
+
+    private NeighborAdvertisement getNextNeighborAdvertisement() throws ParseException {
+        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
+                this::isNeighborAdvertisement);
+        if (packet == null) return null;
+
+        final NeighborAdvertisement na = parseNeighborAdvertisementOrNull(packet);
+        assertNotNull("Invalid neighbour advertisement received", na);
+        return na;
+    }
+
     private void waitForRouterSolicitation() throws ParseException {
         assertNotNull("No router solicitation received on interface within timeout",
                 mPacketReader.popPacket(PACKET_TIMEOUT_MS, this::isRouterSolicitation));
@@ -1464,7 +1480,7 @@
     private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
             throws Exception {
         return PrefixInformationOption.build(new IpPrefix(prefixString),
-                (byte) 0b11000000 /* L = 1, A = 1 */, valid, preferred);
+                (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), valid, preferred);
     }
 
     private static ByteBuffer buildRdnssOption(int lifetime, String... servers) throws Exception {
@@ -2760,4 +2776,50 @@
         assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
         assertFalse(packet.hasRequestedParam((byte) 42 /* NTP_SERVER */));
     }
+
+    private void assertGratuitousNa(final NeighborAdvertisement na) throws Exception {
+        final MacAddress etherMulticast =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
+        final LinkAddress target = new LinkAddress(na.naHdr.target, 64);
+
+        assertEquals(etherMulticast, na.ethHdr.dstMac);
+        assertEquals(ETH_P_IPV6, na.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, na.ipv6Hdr.nextHeader);
+        assertEquals(0xff, na.ipv6Hdr.hopLimit);
+        assertTrue(na.ipv6Hdr.srcIp.isLinkLocalAddress());
+        assertEquals(IPV6_ADDR_ALL_ROUTERS_MULTICAST, na.ipv6Hdr.dstIp);
+        assertEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, na.icmpv6Hdr.type);
+        assertEquals(0, na.icmpv6Hdr.code);
+        assertEquals(0, na.naHdr.flags);
+        assertTrue(target.isGlobalPreferred());
+    }
+
+    @Test
+    public void testGratuitousNa() throws Exception {
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION,
+                true /* isGratuitousNaEnabled */);
+        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, false));
+        startIpClientProvisioning(config);
+
+        final InOrder inOrder = inOrder(mCb);
+        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);
+
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        NeighborAdvertisement packet;
+        while ((packet = getNextNeighborAdvertisement()) != null) {
+            assertGratuitousNa(packet);
+            naList.add(packet);
+        }
+        assertEquals(2, naList.size()); // privacy address and stable privacy address
+    }
 }
diff --git a/tests/integration/src/android/net/ip/IpClientRootTest.kt b/tests/integration/src/android/net/ip/IpClientRootTest.kt
index ea2ec11..68d8aab 100644
--- a/tests/integration/src/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/src/android/net/ip/IpClientRootTest.kt
@@ -26,12 +26,12 @@
 import android.net.ipmemorystore.NetworkAttributes
 import android.net.ipmemorystore.Status
 import android.net.networkstack.TestNetworkStackServiceClient
-import android.net.util.NetworkStackUtils
 import android.os.Process
 import android.provider.DeviceConfig
 import android.util.ArrayMap
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.DeviceConfigUtils
 import java.lang.System.currentTimeMillis
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.CountDownLatch
@@ -194,37 +194,33 @@
         return ipClientCaptor.value
     }
 
-    override fun setDhcpFeatures(
-        isDhcpLeaseCacheEnabled: Boolean,
-        isRapidCommitEnabled: Boolean,
-        isDhcpIpConflictDetectEnabled: Boolean,
-        isIPv6OnlyPreferredEnabled: Boolean
-    ) {
+    override fun setFeatureEnabled(feature: String, enabled: Boolean) {
         automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
         try {
-            setFeatureEnabled(NetworkStackUtils.DHCP_INIT_REBOOT_VERSION, isDhcpLeaseCacheEnabled)
-            setFeatureEnabled(NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION, isRapidCommitEnabled)
-            setFeatureEnabled(NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION,
-                    isDhcpIpConflictDetectEnabled)
-            setFeatureEnabled(NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION,
-                    isIPv6OnlyPreferredEnabled)
+            // Do not use computeIfAbsent as it would overwrite null values (flag originally unset)
+            if (!originalFlagValues.containsKey(feature)) {
+                originalFlagValues[feature] =
+                        DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature)
+            }
+            // The feature is enabled if the flag is lower than the package version.
+            // Package versions follow a standard format with 9 digits.
+            // TODO: consider resetting flag values on reboot when set to special values like "1" or
+            // "999999999"
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature,
+                    if (enabled) "1" else "999999999", false)
         } finally {
             automation.dropShellPermissionIdentity()
         }
     }
 
-    private fun setFeatureEnabled(feature: String, enabled: Boolean) {
-        // Do not use computeIfAbsent as it would overwrite null values (flag originally unset)
-        if (!originalFlagValues.containsKey(feature)) {
-            originalFlagValues[feature] =
-                    DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature)
+    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        try {
+            return DeviceConfigUtils.isFeatureEnabled(mContext, DeviceConfig.NAMESPACE_CONNECTIVITY,
+                    name, defaultEnabled)
+        } finally {
+            automation.dropShellPermissionIdentity()
         }
-        // The feature is enabled if the flag is lower than the package version.
-        // Package versions follow a standard format with 9 digits.
-        // TODO: consider resetting flag values on reboot when set to special values like "1" or
-        // "999999999"
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature,
-                if (enabled) "1" else "999999999", false)
     }
 
     private class TestAttributesRetrievedListener : OnNetworkAttributesRetrievedListener {
diff --git a/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt b/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
deleted file mode 100644
index 9fb4d8c..0000000
--- a/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
+++ /dev/null
@@ -1,446 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.net.module.util
-
-import com.android.testutils.ConcurrentInterpreter
-import com.android.testutils.InterpretException
-import com.android.testutils.InterpretMatcher
-import com.android.testutils.SyntaxException
-import com.android.testutils.__FILE__
-import com.android.testutils.__LINE__
-import com.android.testutils.intArg
-import com.android.testutils.strArg
-import com.android.testutils.timeArg
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import java.util.concurrent.CyclicBarrier
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.system.measureTimeMillis
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertFalse
-import kotlin.test.assertNotEquals
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-val TEST_VALUES = listOf(4, 13, 52, 94, 41, 68, 11, 13, 51, 0, 91, 94, 33, 98, 14)
-const val ABSENT_VALUE = 2
-// Caution in changing these : some tests rely on the fact that TEST_TIMEOUT > 2 * SHORT_TIMEOUT
-// and LONG_TIMEOUT > 2 * TEST_TIMEOUT
-const val SHORT_TIMEOUT = 40L // ms
-const val TEST_TIMEOUT = 200L // ms
-const val LONG_TIMEOUT = 5000L // ms
-
-@RunWith(JUnit4::class)
-class TrackRecordTest {
-    @Test
-    fun testAddAndSizeAndGet() {
-        val repeats = 22 // arbitrary
-        val record = ArrayTrackRecord<Int>()
-        assertEquals(0, record.size)
-        repeat(repeats) { i -> record.add(i + 2) }
-        assertEquals(repeats, record.size)
-        record.add(2)
-        assertEquals(repeats + 1, record.size)
-
-        assertEquals(11, record[9])
-        assertEquals(11, record.getOrNull(9))
-        assertEquals(2, record[record.size - 1])
-        assertEquals(2, record.getOrNull(record.size - 1))
-
-        assertFailsWith<IndexOutOfBoundsException> { record[800] }
-        assertFailsWith<IndexOutOfBoundsException> { record[-1] }
-        assertFailsWith<IndexOutOfBoundsException> { record[repeats + 1] }
-        assertNull(record.getOrNull(800))
-        assertNull(record.getOrNull(-1))
-        assertNull(record.getOrNull(repeats + 1))
-        assertNull(record.getOrNull(800) { true })
-        assertNull(record.getOrNull(-1) { true })
-        assertNull(record.getOrNull(repeats + 1) { true })
-    }
-
-    @Test
-    fun testIndexOf() {
-        val record = ArrayTrackRecord<Int>()
-        TEST_VALUES.forEach { record.add(it) }
-        with(record) {
-            assertEquals(9, indexOf(0))
-            assertEquals(9, lastIndexOf(0))
-            assertEquals(1, indexOf(13))
-            assertEquals(7, lastIndexOf(13))
-            assertEquals(3, indexOf(94))
-            assertEquals(11, lastIndexOf(94))
-            assertEquals(-1, indexOf(ABSENT_VALUE))
-            assertEquals(-1, lastIndexOf(ABSENT_VALUE))
-        }
-    }
-
-    @Test
-    fun testContains() {
-        val record = ArrayTrackRecord<Int>()
-        TEST_VALUES.forEach { record.add(it) }
-        TEST_VALUES.forEach { assertTrue(record.contains(it)) }
-        assertFalse(record.contains(ABSENT_VALUE))
-        assertTrue(record.containsAll(TEST_VALUES))
-        assertTrue(record.containsAll(TEST_VALUES.sorted()))
-        assertTrue(record.containsAll(TEST_VALUES.sortedDescending()))
-        assertTrue(record.containsAll(TEST_VALUES.distinct()))
-        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2)))
-        assertTrue(record.containsAll(TEST_VALUES.subList(0, TEST_VALUES.size / 2).sorted()))
-        assertTrue(record.containsAll(listOf()))
-        assertFalse(record.containsAll(listOf(ABSENT_VALUE)))
-        assertFalse(record.containsAll(TEST_VALUES + listOf(ABSENT_VALUE)))
-    }
-
-    @Test
-    fun testEmpty() {
-        val record = ArrayTrackRecord<Int>()
-        assertTrue(record.isEmpty())
-        record.add(1)
-        assertFalse(record.isEmpty())
-    }
-
-    @Test
-    fun testIterate() {
-        val record = ArrayTrackRecord<Int>()
-        record.forEach { fail("Expected nothing to iterate") }
-        TEST_VALUES.forEach { record.add(it) }
-        // zip relies on the iterator (this calls extension function Iterable#zip(Iterable))
-        record.zip(TEST_VALUES).forEach { assertEquals(it.first, it.second) }
-        // Also test reverse iteration (to test hasPrevious() and friends)
-        record.reversed().zip(TEST_VALUES.reversed()).forEach { assertEquals(it.first, it.second) }
-    }
-
-    @Test
-    fun testIteratorIsSnapshot() {
-        val record = ArrayTrackRecord<Int>()
-        TEST_VALUES.forEach { record.add(it) }
-        val iterator = record.iterator()
-        val expectedSize = record.size
-        record.add(ABSENT_VALUE)
-        record.add(ABSENT_VALUE)
-        var measuredSize = 0
-        iterator.forEach {
-            ++measuredSize
-            assertNotEquals(ABSENT_VALUE, it)
-        }
-        assertEquals(expectedSize, measuredSize)
-    }
-
-    @Test
-    fun testSublist() {
-        val record = ArrayTrackRecord<Int>()
-        TEST_VALUES.forEach { record.add(it) }
-        assertEquals(record.subList(3, record.size - 3),
-                TEST_VALUES.subList(3, TEST_VALUES.size - 3))
-    }
-
-    fun testPollReturnsImmediately(record: TrackRecord<Int>) {
-        record.add(4)
-        val elapsed = measureTimeMillis { assertEquals(4, record.poll(LONG_TIMEOUT, 0)) }
-        // Should not have waited at all, in fact.
-        assertTrue(elapsed < LONG_TIMEOUT)
-        record.add(7)
-        record.add(9)
-        // Can poll multiple times for the same position, in whatever order
-        assertEquals(9, record.poll(0, 2))
-        assertEquals(7, record.poll(Long.MAX_VALUE, 1))
-        assertEquals(9, record.poll(0, 2))
-        assertEquals(4, record.poll(0, 0))
-        assertEquals(9, record.poll(0, 2) { it > 5 })
-        assertEquals(7, record.poll(0, 0) { it > 5 })
-    }
-
-    @Test
-    fun testPollReturnsImmediately() {
-        testPollReturnsImmediately(ArrayTrackRecord())
-        testPollReturnsImmediately(ArrayTrackRecord<Int>().newReadHead())
-    }
-
-    @Test
-    fun testPollTimesOut() {
-        val record = ArrayTrackRecord<Int>()
-        var delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0)) }
-        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
-        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
-        assertTrue(delay >= SHORT_TIMEOUT)
-    }
-
-    @Test
-    fun testConcurrentPollDisallowed() {
-        val failures = AtomicInteger(0)
-        val readHead = ArrayTrackRecord<Int>().newReadHead()
-        val barrier = CyclicBarrier(2)
-        Thread {
-            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
-            try {
-                readHead.poll(LONG_TIMEOUT)
-            } catch (e: ConcurrentModificationException) {
-                failures.incrementAndGet()
-                // Unblock the other thread
-                readHead.add(0)
-            }
-        }.start()
-        barrier.await() // barrier 1
-        try {
-            readHead.poll(LONG_TIMEOUT)
-        } catch (e: ConcurrentModificationException) {
-            failures.incrementAndGet()
-            // Unblock the other thread
-            readHead.add(0)
-        }
-        // One of the threads must have gotten an exception.
-        assertEquals(failures.get(), 1)
-    }
-
-    @Test
-    fun testPollWakesUp() {
-        val record = ArrayTrackRecord<Int>()
-        val barrier = CyclicBarrier(2)
-        Thread {
-            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
-            barrier.await() // barrier 2
-            Thread.sleep(SHORT_TIMEOUT * 2)
-            record.add(31)
-        }.start()
-        barrier.await() // barrier 1
-        // Should find the element in more than SHORT_TIMEOUT but less than TEST_TIMEOUT
-        var delay = measureTimeMillis {
-            barrier.await() // barrier 2
-            assertEquals(31, record.poll(TEST_TIMEOUT, 0))
-        }
-        assertTrue(delay in SHORT_TIMEOUT..TEST_TIMEOUT)
-        // Polling for an element already added in anothe thread (pos 0) : should return immediately
-        delay = measureTimeMillis { assertEquals(31, record.poll(TEST_TIMEOUT, 0)) }
-        assertTrue(delay < TEST_TIMEOUT, "Delay $delay > $TEST_TIMEOUT")
-        // Waiting for an element that never comes
-        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 1)) }
-        assertTrue(delay >= SHORT_TIMEOUT, "Delay $delay < $SHORT_TIMEOUT")
-        // Polling for an element that doesn't match what is already there
-        delay = measureTimeMillis { assertNull(record.poll(SHORT_TIMEOUT, 0) { it < 10 }) }
-        assertTrue(delay >= SHORT_TIMEOUT)
-    }
-
-    // Just make sure the interpreter actually throws an exception when the spec
-    // does not conform to the behavior. The interpreter is just a tool to test a
-    // tool used for a tool for test, let's not have hundreds of tests for it ;
-    // if it's broken one of the tests using it will break.
-    @Test
-    fun testInterpreter() {
-        val interpretLine = __LINE__ + 2
-        try {
-            TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-                add(4) | poll(1, 0) = 5
-            """)
-            fail("This spec should have thrown")
-        } catch (e: InterpretException) {
-            assertTrue(e.cause is AssertionError)
-            assertEquals(interpretLine + 1, e.stackTrace[0].lineNumber)
-            assertTrue(e.stackTrace[0].fileName.contains(__FILE__))
-            assertTrue(e.stackTrace[0].methodName.contains("testInterpreter"))
-            assertTrue(e.stackTrace[0].methodName.contains("thread1"))
-        }
-    }
-
-    @Test
-    fun testMultipleAdds() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
-            add(2)         |                |                |
-                           | add(4)         |                |
-                           |                | add(6)         |
-                           |                |                | add(8)
-            poll(0, 0) = 2 time 0..1 | poll(0, 0) = 2 | poll(0, 0) = 2 | poll(0, 0) = 2
-            poll(0, 1) = 4 time 0..1 | poll(0, 1) = 4 | poll(0, 1) = 4 | poll(0, 1) = 4
-            poll(0, 2) = 6 time 0..1 | poll(0, 2) = 6 | poll(0, 2) = 6 | poll(0, 2) = 6
-            poll(0, 3) = 8 time 0..1 | poll(0, 3) = 8 | poll(0, 3) = 8 | poll(0, 3) = 8
-        """)
-    }
-
-    @Test
-    fun testConcurrentAdds() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
-            add(2)             | add(4)             | add(6)             | add(8)
-            add(1)             | add(3)             | add(5)             | add(7)
-            poll(0, 1) is even | poll(0, 0) is even | poll(0, 3) is even | poll(0, 2) is even
-            poll(0, 5) is odd  | poll(0, 4) is odd  | poll(0, 7) is odd  | poll(0, 6) is odd
-        """)
-    }
-
-    @Test
-    fun testMultiplePoll() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
-            add(4)         | poll(1, 0) = 4
-                           | poll(0, 1) = null time 0..1
-                           | poll(1, 1) = null time 1..2
-            sleep; add(7)  | poll(2, 1) = 7 time 1..2
-            sleep; add(18) | poll(2, 2) = 18 time 1..2
-        """)
-    }
-
-    @Test
-    fun testMultiplePollWithPredicate() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = false, spec = """
-                     | poll(1, 0) = null          | poll(1, 0) = null
-            add(6)   | poll(1, 0) = 6             |
-            add(11)  | poll(1, 0) { > 20 } = null | poll(1, 0) { = 11 } = 11
-                     | poll(1, 0) { > 8 } = 11    |
-        """)
-    }
-
-    @Test
-    fun testMultipleReadHeads() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-                   | poll() = null | poll() = null | poll() = null
-            add(5) |               | poll() = 5    |
-                   | poll() = 5    |               |
-            add(8) | poll() = 8    | poll() = 8    |
-                   |               |               | poll() = 5
-                   |               |               | poll() = 8
-                   |               |               | poll() = null
-                   |               | poll() = null |
-        """)
-    }
-
-    @Test
-    fun testReadHeadPollWithPredicate() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-            add(5)  | poll() { < 0 } = null
-                    | poll() { > 5 } = null
-            add(10) |
-                    | poll() { = 5 } = null   // The "5" was skipped in the previous line
-            add(15) | poll() { > 8 } = 15     // The "10" was skipped in the previous line
-                    | poll(1, 0) { > 8 } = 10 // 10 is the first element after pos 0 matching > 8
-        """)
-    }
-
-    @Test
-    fun testPollImmediatelyAdvancesReadhead() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-            add(1)                  | add(2)              | add(3)   | add(4)
-            mark = 0                | poll(0) { > 3 } = 4 |          |
-            poll(0) { > 10 } = null |                     |          |
-            mark = 4                |                     |          |
-            poll() = null           |                     |          |
-        """)
-    }
-
-    @Test
-    fun testParallelReadHeads() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-            mark = 0   | mark = 0   | mark = 0   | mark = 0
-            add(2)     |            |            |
-                       | add(4)     |            |
-                       |            | add(6)     |
-                       |            |            | add(8)
-            poll() = 2 | poll() = 2 | poll() = 2 | poll() = 2
-            poll() = 4 | poll() = 4 | poll() = 4 | poll() = 4
-            poll() = 6 | poll() = 6 | poll() = 6 | mark = 2
-            poll() = 8 | poll() = 8 | mark = 3   | poll() = 6
-            mark = 4   | mark = 4   | poll() = 8 | poll() = 8
-        """)
-    }
-
-    @Test
-    fun testPeek() {
-        TRTInterpreter.interpretTestSpec(useReadHeads = true, spec = """
-            add(2)     |            |               |
-                       | add(4)     |               |
-                       |            | add(6)        |
-                       |            |               | add(8)
-            peek() = 2 | poll() = 2 | poll() = 2    | peek() = 2
-            peek() = 2 | peek() = 4 | poll() = 4    | peek() = 2
-            peek() = 2 | peek() = 4 | peek() = 6    | poll() = 2
-            peek() = 2 | mark = 1   | mark = 2      | poll() = 4
-            mark = 0   | peek() = 4 | peek() = 6    | peek() = 6
-            poll() = 2 | poll() = 4 | poll() = 6    | poll() = 6
-            poll() = 4 | mark = 2   | poll() = 8    | peek() = 8
-            peek() = 6 | peek() = 6 | peek() = null | mark = 3
-        """)
-    }
-}
-
-private object TRTInterpreter : ConcurrentInterpreter<TrackRecord<Int>>(interpretTable) {
-    fun interpretTestSpec(spec: String, useReadHeads: Boolean) = if (useReadHeads) {
-        interpretTestSpec(spec, initial = ArrayTrackRecord(),
-                threadTransform = { (it as ArrayTrackRecord).newReadHead() })
-    } else {
-        interpretTestSpec(spec, ArrayTrackRecord())
-    }
-}
-
-/*
- * Quick ref of supported expressions :
- * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
- * add(x) : calls and returns TrackRecord#add.
- * poll(time, pos) [{ predicate }] : calls and returns TrackRecord#poll(x time units, pos).
- *   Optionally, a predicate may be specified.
- * poll() [{ predicate }] : calls and returns ReadHead#poll(1 time unit). Optionally, a predicate
- *   may be specified.
- * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
- *   string "null" or an int. Returns Unit.
- * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
- *   y time units.
- * predicate must be one of "= x", "< x" or "> x".
- */
-private val interpretTable = listOf<InterpretMatcher<TrackRecord<Int>>>(
-    // Interpret "XXX is odd" : run XXX and assert its return value is odd ("even" works too)
-    Regex("(.*)\\s+is\\s+(even|odd)") to { i, t, r ->
-        i.interpret(r.strArg(1), t).also {
-            assertEquals((it as Int) % 2, if ("even" == r.strArg(2)) 0 else 1)
-        }
-    },
-    // Interpret "add(XXX)" as TrackRecord#add(int)
-    Regex("""add\((\d+)\)""") to { i, t, r ->
-        t.add(r.intArg(1))
-    },
-    // Interpret "poll(x, y)" as TrackRecord#poll(timeout = x * INTERPRET_TIME_UNIT, pos = y)
-    // Accepts an optional {} argument for the predicate (see makePredicate for syntax)
-    Regex("""poll\((\d+),\s*(\d+)\)\s*(\{.*\})?""") to { i, t, r ->
-        t.poll(r.timeArg(1), r.intArg(2), makePredicate(r.strArg(3)))
-    },
-    // ReadHead#poll. If this throws in the cast, the code is malformed and has passed "poll()"
-    // in a test that takes a TrackRecord that is not a ReadHead. It's technically possible to get
-    // the test code to not compile instead of throw, but it's vastly more complex and this will
-    // fail 100% at runtime any test that would not have compiled.
-    Regex("""poll\((\d+)?\)\s*(\{.*\})?""") to { i, t, r ->
-        (if (r.strArg(1).isEmpty()) i.interpretTimeUnit else r.timeArg(1)).let { time ->
-            (t as ArrayTrackRecord<Int>.ReadHead).poll(time, makePredicate(r.strArg(2)))
-        }
-    },
-    // ReadHead#mark. The same remarks apply as with ReadHead#poll.
-    Regex("mark") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).mark },
-    // ReadHead#peek. The same remarks apply as with ReadHead#poll.
-    Regex("peek\\(\\)") to { i, t, _ -> (t as ArrayTrackRecord<Int>.ReadHead).peek() }
-)
-
-// Parses a { = x } or { < x } or { > x } string and returns the corresponding predicate
-// Returns an always-true predicate for empty and null arguments
-private fun makePredicate(spec: String?): (Int) -> Boolean {
-    if (spec.isNullOrEmpty()) return { true }
-    val match = Regex("""\{\s*([<>=])\s*(\d+)\s*\}""").matchEntire(spec)
-            ?: throw SyntaxException("Predicate \"${spec}\"")
-    val arg = match.intArg(2)
-    return when (match.strArg(1)) {
-        ">" -> { i -> i > arg }
-        "<" -> { i -> i < arg }
-        "=" -> { i -> i == arg }
-        else -> throw RuntimeException("How did \"${spec}\" match this regexp ?")
-    }
-}