Snap for 7277594 from 445f742223d8d27cff0e732bf7cd44aa0d26d32a to mainline-documentsui-release

Change-Id: Ib6d26afeb432e23fa19b4603d570d8a917ffbc6f
diff --git a/TEST_MAPPING b/TEST_MAPPING
index b840d01..490fb5d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -14,7 +14,7 @@
       "path": "frameworks/base/packages/Tethering"
     },
     {
-      "path": "frameworks/opt/net/wifi"
+      "path": "packages/modules/Wifi/framework"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/common/Android.bp b/common/Android.bp
index 03296e7..74aafd3 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -59,6 +59,7 @@
         "//packages/modules/NetworkStack:__subpackages__",
         "//packages/modules/CaptivePortalLogin",
         "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/libs/net/common/testutils:__subpackages__",
   ],
   static_libs: [
       "net-utils-framework-common",
@@ -68,25 +69,6 @@
   ],
 }
 
-java_library {
-  // Consider using net-tests-utils instead if writing device code.
-  // That library has a lot more useful tools into it for users that
-  // work on Android and includes this lib.
-  name: "net-tests-utils-host-device-common",
-  srcs: [
-      "hostdevice/**/*.java",
-      "hostdevice/**/*.kt",
-  ],
-  host_supported: true,
-  visibility: [
-      "//frameworks/libs/net/common/tests:__subpackages__",
-      "//frameworks/libs/net/client-libs/tests:__subpackages__",
-  ],
-  static_libs: [
-      "kotlin-test"
-  ]
-}
-
 java_defaults {
     name: "lib_mockito_extended",
     static_libs: [
@@ -98,25 +80,6 @@
     ],
 }
 
-java_library {
-    name: "net-tests-utils",
-    srcs: [
-        "devicetests/**/*.java",
-        "devicetests/**/*.kt",
-    ],
-    defaults: ["lib_mockito_extended"],
-    libs: [
-        "androidx.annotation_annotation",
-    ],
-    static_libs: [
-        "androidx.test.ext.junit",
-        "kotlin-reflect",
-        "libnanohttpd",
-        "net-tests-utils-host-device-common",
-        "net-utils-device-common",
-    ],
-}
-
 filegroup {
     name: "net-utils-framework-common-srcs",
     srcs: ["framework/**/*.java"],
diff --git a/common/device/com/android/net/module/util/Ipv6Utils.java b/common/device/com/android/net/module/util/Ipv6Utils.java
index 73c2431..fe7c89b 100644
--- a/common/device/com/android/net/module/util/Ipv6Utils.java
+++ b/common/device/com/android/net/module/util/Ipv6Utils.java
@@ -20,7 +20,9 @@
 
 import static com.android.net.module.util.IpUtils.icmpv6Checksum;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
 
@@ -30,6 +32,7 @@
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.NaHeader;
+import com.android.net.module.util.structs.NsHeader;
 import com.android.net.module.util.structs.RaHeader;
 import com.android.net.module.util.structs.RsHeader;
 
@@ -123,6 +126,19 @@
     }
 
     /**
+     * Build an ICMPv6 Neighbor Solicitation packet from the required specified parameters.
+     */
+    public static ByteBuffer buildNsPacket(final MacAddress srcMac, final MacAddress dstMac,
+            final Inet6Address srcIp, final Inet6Address dstIp,
+            final Inet6Address target, final ByteBuffer... options) {
+        final NsHeader nsHeader = new NsHeader(target);
+        final ByteBuffer[] payload = buildIcmpv6Payload(
+                ByteBuffer.wrap(nsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_NEIGHBOR_SOLICITATION /* type */, (byte) 0 /* code */, payload);
+    }
+
+    /**
      * Build an ICMPv6 Router Solicitation packet from the required specified parameters.
      */
     public static ByteBuffer buildRsPacket(final MacAddress srcMac, final MacAddress dstMac,
@@ -133,4 +149,14 @@
         return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
                 (byte) ICMPV6_ROUTER_SOLICITATION /* type */, (byte) 0 /* code */, payload);
     }
+
+    /**
+     * Build an ICMPv6 Echo Request packet from the required specified parameters.
+     */
+    public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
+            final MacAddress dstMac, final Inet6Address srcIp, final Inet6Address dstIp) {
+        final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+        return buildIcmpv6Packet(srcMac, dstMac, srcIp, dstIp,
+                (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */, (byte) 0 /* code */, payload);
+    }
 }
diff --git a/common/device/com/android/net/module/util/structs/NaHeader.java b/common/device/com/android/net/module/util/structs/NaHeader.java
index 571d67b..90c078e 100644
--- a/common/device/com/android/net/module/util/structs/NaHeader.java
+++ b/common/device/com/android/net/module/util/structs/NaHeader.java
@@ -26,8 +26,8 @@
  * ICMPv6 Neighbor Advertisement header, follow {@link Icmpv6Header}, as per
  * https://tools.ietf.org/html/rfc4861. This does not contain any option.
  *
- * 0                   1                   2                   3
- * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  0                   1                   2                   3
+ *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |     Type      |     Code      |          Checksum             |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
diff --git a/common/device/com/android/net/module/util/structs/NsHeader.java b/common/device/com/android/net/module/util/structs/NsHeader.java
new file mode 100644
index 0000000..6e0aa50
--- /dev/null
+++ b/common/device/com/android/net/module/util/structs/NsHeader.java
@@ -0,0 +1,57 @@
+/*
+ * 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/**
+ * ICMPv6 Neighbor Solicitation header, follow {@link Icmpv6Header}, as per
+ * https://tools.ietf.org/html/rfc4861. This does not contain any option.
+ *
+ *  0                   1                   2                   3
+ *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |     Code      |          Checksum             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Reserved                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                       Target Address                          +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |   Options ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+public class NsHeader extends Struct {
+    @Field(order = 0, type = Type.S32)
+    public int reserved; // 32 Reserved bits.
+    @Field(order = 1, type = Type.Ipv6Address)
+    public Inet6Address target;
+
+    public NsHeader(final Inet6Address target) {
+        this.reserved = 0;
+        this.target = target;
+    }
+}
diff --git a/common/framework/com/android/net/module/util/CollectionUtils.java b/common/framework/com/android/net/module/util/CollectionUtils.java
index 2223443..4fce8f5 100644
--- a/common/framework/com/android/net/module/util/CollectionUtils.java
+++ b/common/framework/com/android/net/module/util/CollectionUtils.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Objects;
 import java.util.function.Predicate;
@@ -148,4 +149,21 @@
         }
         return -1;
     }
+
+    /**
+     * Returns a new collection of elements that match the passed predicate.
+     * @param source the elements to filter.
+     * @param test the predicate to test for.
+     * @return a new collection containing only the source elements that satisfy the predicate.
+     */
+    @NonNull public static <T> ArrayList<T> filter(@NonNull final Collection<T> source,
+            @NonNull final Predicate<T> test) {
+        final ArrayList<T> matches = new ArrayList<>();
+        for (final T e : source) {
+            if (test.test(e)) {
+                matches.add(e);
+            }
+        }
+        return matches;
+    }
 }
diff --git a/common/framework/com/android/net/module/util/Inet4AddressUtils.java b/common/framework/com/android/net/module/util/Inet4AddressUtils.java
index a1d34a0..87f43d5 100644
--- a/common/framework/com/android/net/module/util/Inet4AddressUtils.java
+++ b/common/framework/com/android/net/module/util/Inet4AddressUtils.java
@@ -163,4 +163,30 @@
             throws IllegalArgumentException {
         return intToInet4AddressHTH(prefixLengthToV4NetmaskIntHTH(prefixLength));
     }
+
+    /**
+     * Trim leading zeros from IPv4 address strings
+     * Non-v4 addresses and host names remain unchanged.
+     * For example, 192.168.000.010 -> 192.168.0.10
+     * @param addr a string representing an ip address
+     * @return a string properly trimmed
+     */
+    public static String trimAddressZeros(String addr) {
+        if (addr == null) return null;
+        String[] octets = addr.split("\\.");
+        if (octets.length != 4) return addr;
+        StringBuilder builder = new StringBuilder(16);
+        String result = null;
+        for (int i = 0; i < 4; i++) {
+            try {
+                if (octets[i].length() > 3) return addr;
+                builder.append(Integer.parseInt(octets[i]));
+            } catch (NumberFormatException e) {
+                return addr;
+            }
+            if (i < 3) builder.append('.');
+        }
+        result = builder.toString();
+        return result;
+    }
 }
diff --git a/common/framework/com/android/net/module/util/InetAddressUtils.java b/common/framework/com/android/net/module/util/InetAddressUtils.java
index 6300328..31d0729 100644
--- a/common/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/common/framework/com/android/net/module/util/InetAddressUtils.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
@@ -27,6 +28,8 @@
  */
 public class InetAddressUtils {
 
+    private static final int INET6_ADDR_LENGTH = 16;
+
     /**
      * Writes an InetAddress to a parcel. The address may be null. This is likely faster than
      * calling writeSerializable.
@@ -35,6 +38,13 @@
     public static void parcelInetAddress(Parcel parcel, InetAddress address, int flags) {
         byte[] addressArray = (address != null) ? address.getAddress() : null;
         parcel.writeByteArray(addressArray);
+        if (address instanceof Inet6Address) {
+            final Inet6Address v6Address = (Inet6Address) address;
+            final boolean hasScopeId = v6Address.getScopeId() != 0;
+            parcel.writeBoolean(hasScopeId);
+            if (hasScopeId) parcel.writeInt(v6Address.getScopeId());
+        }
+
     }
 
     /**
@@ -47,7 +57,14 @@
         if (addressArray == null) {
             return null;
         }
+
         try {
+            if (addressArray.length == INET6_ADDR_LENGTH) {
+                final boolean hasScopeId = in.readBoolean();
+                final int scopeId = hasScopeId ? in.readInt() : 0;
+                return Inet6Address.getByAddress(null /* host */, addressArray, scopeId);
+            }
+
             return InetAddress.getByAddress(addressArray);
         } catch (UnknownHostException e) {
             return null;
diff --git a/common/framework/com/android/net/module/util/LocationPermissionChecker.java b/common/framework/com/android/net/module/util/LocationPermissionChecker.java
new file mode 100644
index 0000000..e4ce9e8
--- /dev/null
+++ b/common/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2020 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 android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.NetworkStack;
+import android.os.Binder;
+import android.os.Build;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * The methods used for location permission and location mode checking.
+ *
+ * @hide
+ */
+public class LocationPermissionChecker {
+
+    private static final String TAG = "LocationPermissionChecker";
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"LOCATION_PERMISSION_CHECK_STATUS_"}, value = {
+        SUCCEEDED,
+        ERROR_LOCATION_MODE_OFF,
+        ERROR_LOCATION_PERMISSION_MISSING,
+    })
+    public @interface LocationPermissionCheckStatus{}
+
+    // The location permission check succeeded.
+    public static final int SUCCEEDED = 0;
+    // The location mode turns off for the caller.
+    public static final int ERROR_LOCATION_MODE_OFF = 1;
+    // The location permission isn't granted for the caller.
+    public static final int ERROR_LOCATION_PERMISSION_MISSING = 2;
+
+    private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
+
+    public LocationPermissionChecker(Context context) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            throw new UnsupportedOperationException("This utility is not supported before R");
+        }
+
+        mContext = context;
+        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+    }
+
+    /**
+     * Check location permission granted by the caller.
+     *
+     * This API check if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     *
+     * @return {@code true} returns if the caller has location permission and the location mode is
+     *         enabled.
+     */
+    public boolean checkLocationPermission(String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        return checkLocationPermissionInternal(pkgName, featureId, uid, message) == SUCCEEDED;
+    }
+
+    /**
+     * Check location permission granted by the caller.
+     *
+     * This API check if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     * Compared with {@link #checkLocationPermission(String, String, int, String)}, this API returns
+     * the detail information about the checking result, including the reason why it's failed and
+     * logs the error for the caller.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     *
+     * @return {@link LocationPermissionCheckStatus} the result of the location permission check.
+     */
+    public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
+            String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
+        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
+        switch (result) {
+            case ERROR_LOCATION_MODE_OFF:
+                Log.e(TAG, "Location mode is disabled for the device");
+                break;
+            case ERROR_LOCATION_PERMISSION_MISSING:
+                Log.e(TAG, "UID " + uid + " has no location permission");
+                break;
+        }
+        return result;
+    }
+
+    /**
+     * Enforce the caller has location permission.
+     *
+     * This API determines if the location mode enabled for the caller and the caller has
+     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
+     * SecurityException is thrown if the caller has no permission or the location mode is disabled.
+     *
+     * @param pkgName package name of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     */
+    public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
+            @Nullable String message) throws SecurityException {
+        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
+
+        switch (result) {
+            case ERROR_LOCATION_MODE_OFF:
+                throw new SecurityException("Location mode is disabled for the device");
+            case ERROR_LOCATION_PERMISSION_MISSING:
+                throw new SecurityException("UID " + uid + " has no location permission");
+        }
+    }
+
+    private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        checkPackage(uid, pkgName);
+
+        // Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
+        // are granted a bypass.
+        if (checkNetworkSettingsPermission(uid) || checkNetworkSetupWizardPermission(uid)
+                || checkNetworkStackPermission(uid) || checkMainlineNetworkStackPermission(uid)) {
+            return SUCCEEDED;
+        }
+
+        // Location mode must be enabled
+        if (!isLocationModeEnabled()) {
+            return ERROR_LOCATION_MODE_OFF;
+        }
+
+        // LocationAccess by App: caller must have Coarse/Fine Location permission to have access to
+        // location information.
+        if (!checkCallersLocationPermission(pkgName, featureId, uid,
+                true /* coarseForTargetSdkLessThanQ */, message)) {
+            return ERROR_LOCATION_PERMISSION_MISSING;
+        }
+        return SUCCEEDED;
+    }
+
+    /**
+     * Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION or
+     * android.Manifest.permission.ACCESS_COARSE_LOCATION (depending on config/targetSDK level)
+     * and a corresponding app op is allowed for this package and uid.
+     *
+     * @param pkgName PackageName of the application requesting access
+     * @param featureId The feature in the package
+     * @param uid The uid of the package
+     * @param coarseForTargetSdkLessThanQ If true and the targetSDK < Q then will check for COARSE
+     *                                    else (false or targetSDK >= Q) then will check for FINE
+     * @param message A message describing why the permission was checked. Only needed if this is
+     *                not inside of a two-way binder call from the data receiver
+     */
+    public boolean checkCallersLocationPermission(String pkgName, @Nullable String featureId,
+            int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {
+
+        boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);
+
+        String permissionType = Manifest.permission.ACCESS_FINE_LOCATION;
+        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
+            // Having FINE permission implies having COARSE permission (but not the reverse)
+            permissionType = Manifest.permission.ACCESS_COARSE_LOCATION;
+        }
+        if (getUidPermission(permissionType, uid) == PackageManager.PERMISSION_DENIED) {
+            return false;
+        }
+
+        // Always checking FINE - even if will not enforce. This will record the request for FINE
+        // so that a location request by the app is surfaced to the user.
+        boolean isFineLocationAllowed = noteAppOpAllowed(
+                AppOpsManager.OPSTR_FINE_LOCATION, pkgName, featureId, uid, message);
+        if (isFineLocationAllowed) {
+            return true;
+        }
+        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
+            return noteAppOpAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, pkgName, featureId, uid,
+                    message);
+        }
+        return false;
+    }
+
+    /**
+     * Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
+     */
+    public boolean isLocationModeEnabled() {
+        final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
+        try {
+            return LocationManager.isLocationEnabledForUser(UserHandle.of(
+                    getCurrentUser()));
+        } catch (Exception e) {
+            Log.e(TAG, "Failure to get location mode via API, falling back to settings", e);
+            return false;
+        }
+    }
+
+    private boolean isTargetSdkLessThan(String packageName, int versionCode, int callingUid) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            if (mContext.getPackageManager().getApplicationInfoAsUser(
+                    packageName, 0,
+                    UserHandle.getUserHandleForUid(callingUid)).targetSdkVersion
+                    < versionCode) {
+                return true;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // In case of exception, assume unknown app (more strict checking)
+            // Note: This case will never happen since checkPackage is
+            // called to verify validity before checking App's version.
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return false;
+    }
+
+    private boolean noteAppOpAllowed(String op, String pkgName, @Nullable String featureId,
+            int uid, @Nullable String message) {
+        return mAppOpsManager.noteOp(op, uid, pkgName, featureId, message)
+                == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private void checkPackage(int uid, String pkgName)
+            throws SecurityException {
+        if (pkgName == null) {
+            throw new SecurityException("Checking UID " + uid + " but Package Name is Null");
+        }
+        mAppOpsManager.checkPackage(uid, pkgName);
+    }
+
+    @VisibleForTesting
+    protected int getCurrentUser() {
+        return ActivityManager.getCurrentUser();
+    }
+
+    private int getUidPermission(String permissionType, int uid) {
+        // We don't care about pid, pass in -1
+        return mContext.checkPermission(permissionType, -1, uid);
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_SETTINGS permission.
+     */
+    public boolean checkNetworkSettingsPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
+     */
+    public boolean checkNetworkSetupWizardPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds NETWORK_STACK permission.
+     */
+    public boolean checkNetworkStackPermission(int uid) {
+        return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
+     */
+    public boolean checkMainlineNetworkStackPermission(int uid) {
+        return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+}
diff --git a/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
index 3de78c6..4c7d675 100644
--- a/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
+++ b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -16,6 +16,20 @@
 
 package com.android.net.module.util;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MCX;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -24,7 +38,9 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 
 import android.annotation.NonNull;
+import android.net.NetworkCapabilities;
 
+import com.android.internal.annotations.VisibleForTesting;
 
 /**
  * Utilities to examine {@link android.net.NetworkCapabilities}.
@@ -55,6 +71,83 @@
     };
 
     /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_OEM_PRIVATE = 26;
+
+    /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27;
+
+    /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+
+    /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_ENTERPRISE = 29;
+
+    /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_VSIM
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_VSIM = 30;
+
+    /**
+     * See android.net.NetworkCapabilities.NET_CAPABILITY_BIP
+     * TODO: Use API constant when all downstream branches are S-based
+     */
+    public static final int NET_CAPABILITY_BIP = 31;
+
+
+    /**
+     * Capabilities that suggest that a network is restricted.
+     * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted},
+      * and {@code FORCE_RESTRICTED_CAPABILITIES}.
+     */
+    @VisibleForTesting
+    static final long RESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_BIP)
+            | (1 << NET_CAPABILITY_CBS)
+            | (1 << NET_CAPABILITY_DUN)
+            | (1 << NET_CAPABILITY_EIMS)
+            | (1 << NET_CAPABILITY_ENTERPRISE)
+            | (1 << NET_CAPABILITY_FOTA)
+            | (1 << NET_CAPABILITY_IA)
+            | (1 << NET_CAPABILITY_IMS)
+            | (1 << NET_CAPABILITY_MCX)
+            | (1 << NET_CAPABILITY_RCS)
+            | (1 << NET_CAPABILITY_VEHICLE_INTERNAL)
+            | (1 << NET_CAPABILITY_VSIM)
+            | (1 << NET_CAPABILITY_XCAP);
+
+    /**
+     * Capabilities that force network to be restricted.
+     * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
+     */
+    private static final long FORCE_RESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_OEM_PAID)
+            | (1 << NET_CAPABILITY_OEM_PRIVATE);
+
+    /**
+     * Capabilities that suggest that a network is unrestricted.
+     * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
+     */
+    @VisibleForTesting
+    static final long UNRESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_INTERNET)
+            | (1 << NET_CAPABILITY_MMS)
+            | (1 << NET_CAPABILITY_SUPL)
+            | (1 << NET_CAPABILITY_WIFI_P2P);
+
+    /**
      * Get a transport that can be used to classify a network when displaying its info to users.
      *
      * While networks can have multiple transports, users generally think of them as "wifi",
@@ -79,6 +172,41 @@
         return transports[0];
     }
 
+
+    /**
+     * Infers that all the capabilities it provides are typically provided by restricted networks
+     * or not.
+     *
+     * @param nc the {@link NetworkCapabilities} to infer the restricted capabilities.
+     *
+     * @return {@code true} if the network should be restricted.
+     */
+    // TODO: Use packBits(nc.getCapabilities()) to check more easily using bit masks.
+    public static boolean inferRestrictedCapability(NetworkCapabilities nc) {
+        // Check if we have any capability that forces the network to be restricted.
+        for (int capability : unpackBits(FORCE_RESTRICTED_CAPABILITIES)) {
+            if (nc.hasCapability(capability)) {
+                return true;
+            }
+        }
+
+        // Verify there aren't any unrestricted capabilities.  If there are we say
+        // the whole thing is unrestricted unless it is forced to be restricted.
+        for (int capability : unpackBits(UNRESTRICTED_CAPABILITIES)) {
+            if (nc.hasCapability(capability)) {
+                return false;
+            }
+        }
+
+        // Must have at least some restricted capabilities.
+        for (int capability : unpackBits(RESTRICTED_CAPABILITIES)) {
+            if (nc.hasCapability(capability)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Unpacks long value into an array of bits.
      */
diff --git a/common/framework/com/android/net/module/util/NetworkStackConstants.java b/common/framework/com/android/net/module/util/NetworkStackConstants.java
index 499297c..b7062e7 100644
--- a/common/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/common/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -186,6 +186,27 @@
      */
     public static final int VENDOR_SPECIFIC_IE_ID = 0xdd;
 
+
+    /**
+     * TrafficStats constants.
+     */
+    // These tags are used by the network stack to do traffic for its own purposes. Traffic
+    // tagged with these will be counted toward the network stack and must stay inside the
+    // range defined by
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_RANGE_START} and
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_RANGE_END}.
+    public static final int TAG_SYSTEM_DHCP = 0xFFFFFE01;
+    public static final int TAG_SYSTEM_NEIGHBOR = 0xFFFFFE02;
+    public static final int TAG_SYSTEM_DHCP_SERVER = 0xFFFFFE03;
+
+    // These tags are used by the network stack to do traffic on behalf of apps. Traffic
+    // tagged with these will be counted toward the app on behalf of which the network
+    // stack is doing this traffic. These values must stay inside the range defined by
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_IMPERSONATION_RANGE_START} and
+    // {@link android.net.TrafficStats#TAG_NETWORK_STACK_IMPERSONATION_RANGE_END}.
+    public static final int TAG_SYSTEM_PROBE = 0xFFFFFF81;
+    public static final int TAG_SYSTEM_DNS = 0xFFFFFF82;
+
     // TODO: Move to Inet4AddressUtils
     // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
     // for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/common/tests/unit/Android.bp b/common/tests/unit/Android.bp
index 4ce4b0e..45a89b0 100644
--- a/common/tests/unit/Android.bp
+++ b/common/tests/unit/Android.bp
@@ -15,7 +15,7 @@
         "androidx.test.rules",
         "mockito-target-extended-minus-junit4",
         "net-utils-device-common",
-        "net-tests-utils-host-device-common",
+        "net-tests-utils",
     ],
     libs: [
         "android.test.runner",
diff --git a/common/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java
index 5d5ed91..702bdaf 100644
--- a/common/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java
+++ b/common/tests/unit/src/com/android/net/module/util/Inet4AddressUtilsTest.java
@@ -26,9 +26,11 @@
 import static com.android.net.module.util.Inet4AddressUtils.netmaskToPrefixLength;
 import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTL;
+import static com.android.net.module.util.Inet4AddressUtils.trimAddressZeros;
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 import android.net.InetAddresses;
@@ -204,6 +206,17 @@
         getPrefixMaskAsInet4Address(-1);
     }
 
+    @Test
+    public void testTrimAddressZeros() {
+        assertNull(trimAddressZeros(null));
+        assertEquals("$invalid&", trimAddressZeros("$invalid&"));
+        assertEquals("example.com", trimAddressZeros("example.com"));
+        assertEquals("a.b.c.d", trimAddressZeros("a.b.c.d"));
+
+        assertEquals("192.0.2.2", trimAddressZeros("192.000.02.2"));
+        assertEquals("192.0.2.2", trimAddressZeros("192.0.2.2"));
+    }
+
     private Inet4Address ipv4Address(String addr) {
         return (Inet4Address) InetAddresses.parseNumericAddress(addr);
     }
diff --git a/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
new file mode 100644
index 0000000..2736c53
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 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 static junit.framework.Assert.assertEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InetAddressUtilsTest {
+
+    private InetAddress parcelUnparcelAddress(InetAddress addr) {
+        Parcel p = Parcel.obtain();
+        InetAddressUtils.parcelInetAddress(p, addr, 0 /* flags */);
+        p.setDataPosition(0);
+        byte[] marshalled = p.marshall();
+        p.recycle();
+        p = Parcel.obtain();
+        p.unmarshall(marshalled, 0, marshalled.length);
+        p.setDataPosition(0);
+        InetAddress out = InetAddressUtils.unparcelInetAddress(p);
+        p.recycle();
+        return out;
+    }
+
+    @Test
+    public void testParcelUnparcelIpv4Address() throws Exception {
+        InetAddress ipv4 = InetAddress.getByName("192.0.2.1");
+        assertEquals(ipv4, parcelUnparcelAddress(ipv4));
+    }
+
+    @Test
+    public void testParcelUnparcelIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("2001:db8::1");
+        assertEquals(ipv6, parcelUnparcelAddress(ipv6));
+    }
+
+    @Test
+    public void testParcelUnparcelScopedIpv6Address() throws Exception {
+        InetAddress ipv6 = InetAddress.getByName("fe80::1%42");
+        assertEquals(42, ((Inet6Address) ipv6).getScopeId());
+        Inet6Address out = (Inet6Address) parcelUnparcelAddress(ipv6);
+        assertEquals(ipv6, out);
+        assertEquals(42, out.getScopeId());
+    }
+}
diff --git a/common/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java b/common/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
new file mode 100644
index 0000000..03a1ec9
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/Ipv6UtilsTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2020 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 static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Ipv6UtilsTest {
+
+    private static final MacAddress MAC1 = MacAddress.fromString("11:22:33:44:55:66");
+    private static final MacAddress MAC2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+    private static final Inet6Address LINK_LOCAL = addr("fe80::1");
+    private static final Inet6Address ROUTER_LINK_LOCAL = addr("fe80::cafe:d00d");
+    private static final Inet6Address ALL_ROUTERS =
+            NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+    private static final Inet6Address ALL_NODES =
+            NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+
+    @Test
+    public void testBuildRsPacket() {
+        ByteBuffer b = Ipv6Utils.buildRsPacket(MAC1, MAC2, LINK_LOCAL, ALL_ROUTERS /* no opts */);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC1, eth.srcMac);
+        assertEquals(MAC2, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_ROUTERS, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    @Test
+    public void testBuildRaPacket() {
+        final byte pioFlags =
+                NetworkStackConstants.PIO_FLAG_AUTONOMOUS | NetworkStackConstants.PIO_FLAG_ON_LINK;
+        ByteBuffer pio1 = PrefixInformationOption.build(new IpPrefix("2001:db8:1::/64"),
+                pioFlags, 3600 /* validLifetime */, 1800 /* preferredLifetime */);
+        ByteBuffer pio2 = PrefixInformationOption.build(new IpPrefix("fdcd:a17f:6502:1::/64"),
+                pioFlags, 86400 /* validLifetime */, 86400 /* preferredLifetime */);
+
+        ByteBuffer b = Ipv6Utils.buildRaPacket(MAC2, MAC1, ROUTER_LINK_LOCAL, ALL_NODES,
+                (byte) 0 /* flags */, 7200 /* lifetime */,
+                30_000 /* reachableTime */, 750 /* retransTimer */,
+                pio1, pio2);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC2, eth.srcMac);
+        assertEquals(MAC1, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(ROUTER_LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+
+        RaHeader ra = Struct.parse(RaHeader.class, b);
+        assertEquals(0, ra.hopLimit);  // Hop limit: unspecified.
+        assertEquals(0, ra.flags);
+        assertEquals(7200, ra.lifetime);
+        assertEquals(30_000, ra.reachableTime);
+        assertEquals(750, ra.retransTimer);
+
+        PrefixInformationOption pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "2001:db8:1::/64", pioFlags, 3600, 1800);
+        pio = Struct.parse(PrefixInformationOption.class, b);
+        assertPioEquals(pio, "fdcd:a17f:6502:1::/64", pioFlags, 86400, 86400);
+    }
+
+    @Test
+    public void testBuildEchoRequestPacket() {
+        final ByteBuffer b = Ipv6Utils.buildEchoRequestPacket(MAC2, MAC1, LINK_LOCAL, ALL_NODES);
+
+        EthernetHeader eth = Struct.parse(EthernetHeader.class, b);
+        assertEquals(MAC2, eth.srcMac);
+        assertEquals(MAC1, eth.dstMac);
+
+        Ipv6Header ipv6 = Struct.parse(Ipv6Header.class, b);
+        assertEquals(255, ipv6.hopLimit);
+        assertEquals(OsConstants.IPPROTO_ICMPV6, ipv6.nextHeader);
+        assertEquals(LINK_LOCAL, ipv6.srcIp);
+        assertEquals(ALL_NODES, ipv6.dstIp);
+
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, b);
+        assertEquals(NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE, icmpv6.type);
+        assertEquals(0, icmpv6.code);
+    }
+
+    private void assertPioEquals(PrefixInformationOption pio, String prefix, byte flags,
+            long valid, long preferred) {
+        assertEquals(NetworkStackConstants.ICMPV6_ND_OPTION_PIO, pio.type);
+        assertEquals(4, pio.length);
+        assertEquals(flags, pio.flags);
+        assertEquals(valid, pio.validLifetime);
+        assertEquals(preferred, pio.preferredLifetime);
+        IpPrefix expected = new IpPrefix(prefix);
+        IpPrefix actual = new IpPrefix(pio.prefix, pio.prefixLen);
+        assertEquals(expected, actual);
+    }
+
+    private static Inet6Address addr(String addr) {
+        return (Inet6Address) InetAddresses.parseNumericAddress(addr);
+    }
+
+    private byte[] slice(byte[] array, int length) {
+        return Arrays.copyOf(array, length);
+    }
+}
diff --git a/common/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/common/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
new file mode 100644
index 0000000..3fb1e92
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2020 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 static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+/** Unit tests for {@link LocationPermissionChecker}. */
+public class LocationPermissionCheckerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+            Build.VERSION_CODES.Q /* ignoreClassUpTo */);
+
+    // Mock objects for testing
+    @Mock private Context mMockContext;
+    @Mock private PackageManager mMockPkgMgr;
+    @Mock private ApplicationInfo mMockApplInfo;
+    @Mock private AppOpsManager mMockAppOps;
+    @Mock private UserManager mMockUserManager;
+    @Mock private LocationManager mLocationManager;
+
+    private static final String TEST_PKG_NAME = "com.google.somePackage";
+    private static final String TEST_FEATURE_ID = "com.google.someFeature";
+    private static final int MANAGED_PROFILE_UID = 1100000;
+    private static final int OTHER_USER_UID = 1200000;
+
+    private final String mInteractAcrossUsersFullPermission =
+            "android.permission.INTERACT_ACROSS_USERS_FULL";
+    private final String mManifestStringCoarse =
+            Manifest.permission.ACCESS_COARSE_LOCATION;
+    private final String mManifestStringFine =
+            Manifest.permission.ACCESS_FINE_LOCATION;
+
+    // Test variables
+    private int mWifiScanAllowApps;
+    private int mUid;
+    private int mCoarseLocationPermission;
+    private int mAllowCoarseLocationApps;
+    private int mFineLocationPermission;
+    private int mAllowFineLocationApps;
+    private int mNetworkSettingsPermission;
+    private int mCurrentUid;
+    private boolean mIsLocationEnabled;
+    private boolean mThrowSecurityException;
+    private Answer<Integer> mReturnPermission;
+    private HashMap<String, Integer> mPermissionsList = new HashMap<String, Integer>();
+    private LocationPermissionChecker mChecker;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        initTestVars();
+    }
+
+    private void setupMocks() throws Exception {
+        when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
+                .thenReturn(mMockApplInfo);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
+        when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
+                TEST_FEATURE_ID, null)).thenReturn(mWifiScanAllowApps);
+        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
+                .thenReturn(mAllowCoarseLocationApps);
+        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
+                .thenReturn(mAllowFineLocationApps);
+        if (mThrowSecurityException) {
+            doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
+                    + " to application bound to user " + mUid))
+                    .when(mMockAppOps).checkPackage(mUid, TEST_PKG_NAME);
+        }
+        mockSystemService(Context.APP_OPS_SERVICE, AppOpsManager.class, mMockAppOps);
+        mockSystemService(Context.USER_SERVICE, UserManager.class, mMockUserManager);
+        mockSystemService(Context.LOCATION_SERVICE, LocationManager.class, mLocationManager);
+    }
+
+    private <T> void mockSystemService(String name, Class<T> clazz, T service) {
+        when(mMockContext.getSystemService(name)).thenReturn(service);
+        when(mMockContext.getSystemServiceName(clazz)).thenReturn(name);
+        // Do not use mockito extended final method mocking
+        when(mMockContext.getSystemService(clazz)).thenCallRealMethod();
+    }
+
+    private void setupTestCase() throws Exception {
+        setupMocks();
+        setupMockInterface();
+        mChecker = new LocationPermissionChecker(mMockContext) {
+            @Override
+            protected int getCurrentUser() {
+                // Get the user ID of the process running the test rather than the foreground user
+                // id: ActivityManager.getCurrentUser() requires privileged permissions.
+                return UserHandle.getUserHandleForUid(Process.myUid()).getIdentifier();
+            }
+        };
+    }
+
+    private void initTestVars() {
+        mPermissionsList.clear();
+        mReturnPermission = createPermissionAnswer();
+        mWifiScanAllowApps = AppOpsManager.MODE_ERRORED;
+        mUid = OTHER_USER_UID;
+        mThrowSecurityException = true;
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.M;
+        mIsLocationEnabled = false;
+        mCurrentUid = Process.myUid();
+        mCoarseLocationPermission = PackageManager.PERMISSION_DENIED;
+        mFineLocationPermission = PackageManager.PERMISSION_DENIED;
+        mAllowCoarseLocationApps = AppOpsManager.MODE_ERRORED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
+        mNetworkSettingsPermission = PackageManager.PERMISSION_DENIED;
+    }
+
+    private void setupMockInterface() {
+        Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
+        doAnswer(mReturnPermission).when(mMockContext).checkPermission(
+                anyString(), anyInt(), anyInt());
+        when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM,
+                UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID)))
+                .thenReturn(true);
+        when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
+                .thenReturn(mCoarseLocationPermission);
+        when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
+                .thenReturn(mFineLocationPermission);
+        when(mMockContext.checkPermission(NETWORK_SETTINGS, -1, mUid))
+                .thenReturn(mNetworkSettingsPermission);
+        when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
+    }
+
+    private Answer<Integer> createPermissionAnswer() {
+        return new Answer<Integer>() {
+            @Override
+            public Integer answer(InvocationOnMock invocation) {
+                int myUid = (int) invocation.getArguments()[1];
+                String myPermission = (String) invocation.getArguments()[0];
+                mPermissionsList.get(myPermission);
+                if (mPermissionsList.containsKey(myPermission)) {
+                    int uid = mPermissionsList.get(myPermission);
+                    if (myUid == uid) {
+                        return PackageManager.PERMISSION_GRANTED;
+                    }
+                }
+                return PackageManager.PERMISSION_DENIED;
+            }
+        };
+    }
+
+    @Test
+    public void testEnforceLocationPermission_HasAllPermissions_BeforeQ() throws Exception {
+        mIsLocationEnabled = true;
+        mThrowSecurityException = false;
+        mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowCoarseLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        mUid = mCurrentUid;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+    @Test
+    public void testEnforceLocationPermission_HasAllPermissions_AfterQ() throws Exception {
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
+        mIsLocationEnabled = true;
+        mThrowSecurityException = false;
+        mUid = mCurrentUid;
+        mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+    @Test
+    public void testEnforceLocationPermission_PkgNameAndUidMismatch() throws Exception {
+        mThrowSecurityException = true;
+        mIsLocationEnabled = true;
+        mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        setupTestCase();
+
+        assertThrows(SecurityException.class,
+                () -> mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_NoCoarseLocationPermission() throws Exception {
+        mThrowSecurityException = false;
+        mIsLocationEnabled = true;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_NoFineLocationPermission() throws Exception {
+        mThrowSecurityException = false;
+        mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
+        mIsLocationEnabled = true;
+        mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
+        mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
+        mUid = MANAGED_PROFILE_UID;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
+        verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_LocationModeDisabled() throws Exception {
+        mThrowSecurityException = false;
+        mUid = MANAGED_PROFILE_UID;
+        mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
+        mPermissionsList.put(mInteractAcrossUsersFullPermission, mUid);
+        mIsLocationEnabled = false;
+
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_MODE_OFF, result);
+    }
+
+    @Test
+    public void testenforceCanAccessScanResults_LocationModeDisabledHasNetworkSettings()
+            throws Exception {
+        mThrowSecurityException = false;
+        mIsLocationEnabled = false;
+        mNetworkSettingsPermission = PackageManager.PERMISSION_GRANTED;
+        setupTestCase();
+
+        final int result =
+                mChecker.checkLocationPermissionWithDetailInfo(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.SUCCEEDED, result);
+    }
+
+
+    private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
+        try {
+            r.run();
+            Assert.fail("Expected " + exceptionClass + " to be thrown.");
+        } catch (Exception exception) {
+            assertTrue(exceptionClass.isInstance(exception));
+        }
+    }
+}
diff --git a/common/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt b/common/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
index df2f459..5f15c6a 100644
--- a/common/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
+++ b/common/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
@@ -16,6 +16,12 @@
 
 package com.android.net.module.util
 
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CBS
+import android.net.NetworkCapabilities.NET_CAPABILITY_EIMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID
 import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
@@ -25,6 +31,8 @@
 import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.NetworkCapabilitiesUtils.RESTRICTED_CAPABILITIES
+import com.android.net.module.util.NetworkCapabilitiesUtils.UNRESTRICTED_CAPABILITIES
 import com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport
 import com.android.net.module.util.NetworkCapabilitiesUtils.packBits
 import com.android.net.module.util.NetworkCapabilitiesUtils.unpackBits
@@ -33,6 +41,7 @@
 import java.lang.IllegalArgumentException
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
 @RunWith(AndroidJUnit4::class)
@@ -78,4 +87,41 @@
         assertEquals(packedBits, packBits(bits))
         assertTrue(bits contentEquals unpackBits(packedBits))
     }
+
+    @Test
+    fun testInferRestrictedCapability() {
+        val nc = NetworkCapabilities()
+        // Default capabilities don't have restricted capability.
+        assertFalse(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // If there is a force restricted capability, then the network capabilities is restricted.
+        nc.addCapability(NET_CAPABILITY_OEM_PAID)
+        nc.addCapability(NET_CAPABILITY_INTERNET)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // Except for the force restricted capability, if there is any unrestricted capability in
+        // capabilities, then the network capabilities is not restricted.
+        nc.removeCapability(NET_CAPABILITY_OEM_PAID)
+        nc.addCapability(NET_CAPABILITY_CBS)
+        assertFalse(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        // Except for the force restricted capability, the network capabilities will only be treated
+        // as restricted when there is no any unrestricted capability.
+        nc.removeCapability(NET_CAPABILITY_INTERNET)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+    }
+
+    @Test
+    fun testRestrictedUnrestrictedCapabilities() {
+        // verify EIMS is restricted
+        assertEquals((1 shl NET_CAPABILITY_EIMS).toLong() and RESTRICTED_CAPABILITIES,
+                (1 shl NET_CAPABILITY_EIMS).toLong())
+
+        // verify CBS is also restricted
+        assertEquals((1 shl NET_CAPABILITY_CBS).toLong() and RESTRICTED_CAPABILITIES,
+                (1 shl NET_CAPABILITY_CBS).toLong())
+
+        // verify default is not restricted
+        assertEquals((1 shl NET_CAPABILITY_INTERNET).toLong() and RESTRICTED_CAPABILITIES, 0)
+
+        // just to see
+        assertEquals(RESTRICTED_CAPABILITIES and UNRESTRICTED_CAPABILITIES, 0)
+    }
 }
diff --git a/common/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt b/common/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
new file mode 100644
index 0000000..9fb4d8c
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
@@ -0,0 +1,446 @@
+/*
+ * 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 ?")
+    }
+}
diff --git a/common/testutils/Android.bp b/common/testutils/Android.bp
new file mode 100644
index 0000000..d4465dd
--- /dev/null
+++ b/common/testutils/Android.bp
@@ -0,0 +1,55 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "net-tests-utils",
+    srcs: [
+        "devicetests/**/*.java",
+        "devicetests/**/*.kt",
+    ],
+    defaults: ["lib_mockito_extended"],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "kotlin-reflect",
+        "libnanohttpd",
+        "net-tests-utils-host-device-common",
+        "net-utils-device-common",
+    ],
+}
+
+java_library {
+  // Consider using net-tests-utils instead if writing device code.
+  // That library has a lot more useful tools into it for users that
+  // work on Android and includes this lib.
+  name: "net-tests-utils-host-device-common",
+  srcs: [
+      "hostdevice/**/*.java",
+      "hostdevice/**/*.kt",
+  ],
+  host_supported: true,
+  visibility: [
+      "//frameworks/libs/net/common/tests:__subpackages__",
+      "//frameworks/libs/net/client-libs/tests:__subpackages__",
+  ],
+  static_libs: [
+      "kotlin-test"
+  ]
+}
diff --git a/common/devicetests/com/android/testutils/ArpResponder.kt b/common/testutils/devicetests/com/android/testutils/ArpResponder.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/ArpResponder.kt
rename to common/testutils/devicetests/com/android/testutils/ArpResponder.kt
diff --git a/common/devicetests/com/android/testutils/CompatUtil.kt b/common/testutils/devicetests/com/android/testutils/CompatUtil.kt
similarity index 91%
rename from common/devicetests/com/android/testutils/CompatUtil.kt
rename to common/testutils/devicetests/com/android/testutils/CompatUtil.kt
index 4bb90a8..ff8c668 100644
--- a/common/devicetests/com/android/testutils/CompatUtil.kt
+++ b/common/testutils/devicetests/com/android/testutils/CompatUtil.kt
@@ -27,7 +27,7 @@
     fun makeTestNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no TestNetworkSpecifier, StringNetworkSpecifier was used instead
         if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
-            makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // TestNetworkSpecifier is not part of the SDK in some branches using this utility
         // TODO: replace with a direct call to the constructor
@@ -38,7 +38,7 @@
     fun makeEthernetNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no EthernetNetworkSpecifier, StringNetworkSpecifier was used instead
         if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
-            makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // EthernetNetworkSpecifier is not part of the SDK in some branches using this utility
         // TODO: replace with a direct call to the constructor
diff --git a/common/devicetests/com/android/testutils/ConcurrentInterpreter.kt b/common/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/ConcurrentInterpreter.kt
rename to common/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
diff --git a/common/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
rename to common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
diff --git a/common/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
rename to common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
diff --git a/common/devicetests/com/android/testutils/FakeDns.kt b/common/testutils/devicetests/com/android/testutils/FakeDns.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/FakeDns.kt
rename to common/testutils/devicetests/com/android/testutils/FakeDns.kt
diff --git a/common/devicetests/com/android/testutils/HandlerUtils.kt b/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/HandlerUtils.kt
rename to common/testutils/devicetests/com/android/testutils/HandlerUtils.kt
diff --git a/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java b/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
new file mode 100644
index 0000000..e84a224
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.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.testutils;
+
+import android.net.NetworkStats;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.os.RemoteException;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderCbBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderCbStubCompat extends INetworkStatsProviderCallback.Stub {
+    @Override
+    public void notifyStatsUpdated(int token, NetworkStats ifaceStats, NetworkStats uidStats)
+            throws RemoteException {}
+
+    @Override
+    public void notifyAlertReached() throws RemoteException {}
+
+    // Removed in S.
+    public void notifyLimitReached() throws RemoteException {}
+
+    // Added in S.
+    public void notifyWarningOrLimitReached() throws RemoteException {}
+
+    @Override
+    public void unregister() throws RemoteException {}
+}
diff --git a/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java b/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
new file mode 100644
index 0000000..a77aa02
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
@@ -0,0 +1,37 @@
+/*
+ * 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.testutils;
+
+import android.net.netstats.provider.INetworkStatsProvider;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderStubCompat extends INetworkStatsProvider.Stub {
+    @Override
+    public void onRequestStatsUpdate(int token) {}
+
+    // Removed and won't be called in S+.
+    public void onSetLimit(String iface, long quotaBytes) {}
+
+    @Override
+    public void onSetAlert(long bytes) {}
+
+    // Added in S.
+    public void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes) {}
+}
diff --git a/common/devicetests/com/android/testutils/NetworkStatsUtils.kt b/common/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/NetworkStatsUtils.kt
rename to common/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
diff --git a/common/devicetests/com/android/testutils/PacketResponder.kt b/common/testutils/devicetests/com/android/testutils/PacketResponder.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/PacketResponder.kt
rename to common/testutils/devicetests/com/android/testutils/PacketResponder.kt
diff --git a/common/devicetests/com/android/testutils/ParcelUtils.kt b/common/testutils/devicetests/com/android/testutils/ParcelUtils.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/ParcelUtils.kt
rename to common/testutils/devicetests/com/android/testutils/ParcelUtils.kt
diff --git a/common/devicetests/com/android/testutils/TapPacketReader.java b/common/testutils/devicetests/com/android/testutils/TapPacketReader.java
similarity index 100%
rename from common/devicetests/com/android/testutils/TapPacketReader.java
rename to common/testutils/devicetests/com/android/testutils/TapPacketReader.java
diff --git a/common/devicetests/com/android/testutils/TapPacketReaderRule.kt b/common/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/TapPacketReaderRule.kt
rename to common/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
diff --git a/common/devicetests/com/android/testutils/TestHttpServer.kt b/common/testutils/devicetests/com/android/testutils/TestHttpServer.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/TestHttpServer.kt
rename to common/testutils/devicetests/com/android/testutils/TestHttpServer.kt
diff --git a/common/devicetests/com/android/testutils/TestNetworkTracker.kt b/common/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/TestNetworkTracker.kt
rename to common/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
diff --git a/common/devicetests/com/android/testutils/TestPermissionUtil.kt b/common/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/TestPermissionUtil.kt
rename to common/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
diff --git a/common/devicetests/com/android/testutils/TestableNetworkCallback.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
similarity index 90%
rename from common/devicetests/com/android/testutils/TestableNetworkCallback.kt
rename to common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 043654f..1b3d0f6 100644
--- a/common/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -25,6 +25,7 @@
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Losing
@@ -82,7 +83,10 @@
             override val network: Network,
             val blocked: Boolean
         ) : CallbackEntry()
-
+        data class BlockedStatusInt(
+            override val network: Network,
+            val blocked: Int
+        ) : CallbackEntry()
         // Convenience constants for expecting a type
         companion object {
             @JvmField
@@ -103,6 +107,8 @@
             val UNAVAILABLE = Unavailable::class
             @JvmField
             val BLOCKED_STATUS = BlockedStatus::class
+            @JvmField
+            val BLOCKED_STATUS_INT = BlockedStatusInt::class
         }
     }
 
@@ -131,6 +137,9 @@
         history.add(BlockedStatus(network, blocked))
     }
 
+    // Cannot do:
+    // fun onBlockedStatusChanged(network: Network, blocked: Int) {
+    // because on S, that needs to be "override fun", and on R, that cannot be "override fun".
     override fun onNetworkSuspended(network: Network) {
         Log.d(TAG, "onNetworkSuspended $network $network")
         history.add(Suspended(network))
@@ -272,13 +281,33 @@
         blocked: Boolean = false,
         tmt: Long = defaultTimeoutMs
     ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectBlockedStatusCallback(blocked, net, tmt)
+    }
+
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean,
+        blockedStatus: Int,
+        tmt: Long
+    ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectBlockedStatusCallback(blockedStatus, net)
+    }
+
+    private fun expectAvailableCallbacksCommon(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean,
+        tmt: Long
+    ) {
         expectCallback<Available>(net, tmt)
         if (suspended) {
             expectCallback<Suspended>(net, tmt)
         }
         expectCapabilitiesThat(net, tmt) { validated == it.hasCapability(NET_CAPABILITY_VALIDATED) }
         expectCallback<LinkPropertiesChanged>(net, tmt)
-        expectBlockedStatusCallback(blocked, net)
     }
 
     // Backward compatibility for existing Java code. Use named arguments instead and remove all
@@ -295,6 +324,12 @@
         }
     }
 
+    fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) {
+        expectCallback<BlockedStatusInt>(net, tmt).also {
+            assertEquals(it.blocked, blocked, "Unexpected blocked status ${it.blocked}")
+        }
+    }
+
     // Expects the available callbacks (where the onCapabilitiesChanged must contain the
     // VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
     // one we just sent.
@@ -314,6 +349,16 @@
         expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
     }
 
+    fun expectAvailableThenValidatedCallbacks(
+        net: Network,
+        blockedStatus: Int,
+        tmt: Long = defaultTimeoutMs
+    ) {
+        expectAvailableCallbacks(net, validated = false, suspended = false,
+                blockedStatus = blockedStatus, tmt = tmt)
+        expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+    }
+
     // Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
     // calls with networkAgent can be routed through here without moving MockNetworkAgent.
     // TODO: clean this up, remove this method.
diff --git a/common/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
similarity index 100%
rename from common/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
rename to common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
diff --git a/common/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
similarity index 73%
rename from common/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
rename to common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
index 02922d8..643346b 100644
--- a/common/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
@@ -16,18 +16,21 @@
 
 package com.android.testutils
 
-import android.net.netstats.provider.INetworkStatsProvider
 import com.android.net.module.util.ArrayTrackRecord
 import kotlin.test.assertEquals
 import kotlin.test.fail
 
 private const val DEFAULT_TIMEOUT_MS = 200L
 
-open class TestableNetworkStatsProviderBinder : INetworkStatsProvider.Stub() {
+open class TestableNetworkStatsProviderBinder : NetworkStatsProviderStubCompat() {
     sealed class CallbackType {
         data class OnRequestStatsUpdate(val token: Int) : CallbackType()
-        data class OnSetLimit(val iface: String?, val quotaBytes: Long) : CallbackType()
         data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+        data class OnSetWarningAndLimit(
+            val iface: String,
+            val warningBytes: Long,
+            val limitBytes: Long
+        ) : CallbackType()
     }
 
     private val history = ArrayTrackRecord<CallbackType>().ReadHead()
@@ -36,20 +39,21 @@
         history.add(CallbackType.OnRequestStatsUpdate(token))
     }
 
-    override fun onSetLimit(iface: String?, quotaBytes: Long) {
-        history.add(CallbackType.OnSetLimit(iface, quotaBytes))
-    }
-
     override fun onSetAlert(quotaBytes: Long) {
         history.add(CallbackType.OnSetAlert(quotaBytes))
     }
 
+    override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
+    }
+
     fun expectOnRequestStatsUpdate(token: Int) {
         assertEquals(CallbackType.OnRequestStatsUpdate(token), history.poll(DEFAULT_TIMEOUT_MS))
     }
 
-    fun expectOnSetLimit(iface: String?, quotaBytes: Long) {
-        assertEquals(CallbackType.OnSetLimit(iface, quotaBytes), history.poll(DEFAULT_TIMEOUT_MS))
+    fun expectOnSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        assertEquals(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes),
+                history.poll(DEFAULT_TIMEOUT_MS))
     }
 
     fun expectOnSetAlert(quotaBytes: Long) {
diff --git a/common/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
similarity index 85%
rename from common/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
rename to common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
index 163473a..f15f610 100644
--- a/common/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
@@ -17,7 +17,6 @@
 package com.android.testutils
 
 import android.net.NetworkStats
-import android.net.netstats.provider.INetworkStatsProviderCallback
 import com.android.net.module.util.ArrayTrackRecord
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
@@ -25,14 +24,14 @@
 
 private const val DEFAULT_TIMEOUT_MS = 3000L
 
-open class TestableNetworkStatsProviderCbBinder : INetworkStatsProviderCallback.Stub() {
+open class TestableNetworkStatsProviderCbBinder : NetworkStatsProviderCbStubCompat() {
     sealed class CallbackType {
         data class NotifyStatsUpdated(
             val token: Int,
             val ifaceStats: NetworkStats,
             val uidStats: NetworkStats
         ) : CallbackType()
-        object NotifyLimitReached : CallbackType()
+        object NotifyWarningOrLimitReached : CallbackType()
         object NotifyAlertReached : CallbackType()
         object Unregister : CallbackType()
     }
@@ -43,8 +42,8 @@
         history.add(CallbackType.NotifyStatsUpdated(token, ifaceStats, uidStats))
     }
 
-    override fun notifyLimitReached() {
-        history.add(CallbackType.NotifyLimitReached)
+    override fun notifyWarningOrLimitReached() {
+        history.add(CallbackType.NotifyWarningOrLimitReached)
     }
 
     override fun notifyAlertReached() {
@@ -70,8 +69,8 @@
         assertNetworkStatsEquals(uidStats, event.uidStats)
     }
 
-    fun expectNotifyLimitReached() =
-            assertEquals(CallbackType.NotifyLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
+    fun expectNotifyWarningOrLimitReached() =
+            assertEquals(CallbackType.NotifyWarningOrLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
 
     fun expectNotifyAlertReached() =
             assertEquals(CallbackType.NotifyAlertReached, history.poll(DEFAULT_TIMEOUT_MS))
diff --git a/common/hostdevice/com/android/net/module/util/TrackRecord.kt b/common/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
similarity index 100%
rename from common/hostdevice/com/android/net/module/util/TrackRecord.kt
rename to common/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
diff --git a/common/hostdevice/com/android/testutils/ConcurrentUtils.kt b/common/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
similarity index 100%
rename from common/hostdevice/com/android/testutils/ConcurrentUtils.kt
rename to common/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
diff --git a/common/hostdevice/com/android/testutils/ExceptionUtils.java b/common/testutils/hostdevice/com/android/testutils/ExceptionUtils.java
similarity index 100%
rename from common/hostdevice/com/android/testutils/ExceptionUtils.java
rename to common/testutils/hostdevice/com/android/testutils/ExceptionUtils.java
diff --git a/common/hostdevice/com/android/testutils/FileUtils.kt b/common/testutils/hostdevice/com/android/testutils/FileUtils.kt
similarity index 100%
rename from common/hostdevice/com/android/testutils/FileUtils.kt
rename to common/testutils/hostdevice/com/android/testutils/FileUtils.kt
diff --git a/common/hostdevice/com/android/testutils/MiscAsserts.kt b/common/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
similarity index 96%
rename from common/hostdevice/com/android/testutils/MiscAsserts.kt
rename to common/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
index 91fb1cd..a8c6b3c 100644
--- a/common/hostdevice/com/android/testutils/MiscAsserts.kt
+++ b/common/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -32,8 +32,8 @@
     assertEquals(0, len, "Expected empty array, but length was $len")
 }
 
-fun <T> assertEmpty(ts: List<T>) = ts.size.let { len ->
-    assertEquals(0, len, "Expected empty list, but length was $len")
+fun <T> assertEmpty(ts: Collection<T>) = ts.size.let { len ->
+    assertEquals(0, len, "Expected empty collection, but length was $len")
 }
 
 fun <T> assertLength(expected: Int, got: Array<T>) = got.size.let { len ->
diff --git a/common/hostdevice/com/android/testutils/PacketFilter.kt b/common/testutils/hostdevice/com/android/testutils/PacketFilter.kt
similarity index 100%
rename from common/hostdevice/com/android/testutils/PacketFilter.kt
rename to common/testutils/hostdevice/com/android/testutils/PacketFilter.kt
diff --git a/common/hostdevice/com/android/testutils/SkipPresubmit.kt b/common/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt
similarity index 100%
rename from common/hostdevice/com/android/testutils/SkipPresubmit.kt
rename to common/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt