Merge RQ3A.210705.001 to aosp-master - DO NOT MERGE

Merged-In: I096ef2def0a5c627ca9f1c8528d67dd7591628fe
Merged-In: I126fc07cdfa2ae8628906bb6ea95c88cb66263e2
Merged-In: I126fc07cdfa2ae8628906bb6ea95c88cb66263e2
Change-Id: Ifafb33977ae84ceb43fd0b66e6ba52c36bcac056
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2732435
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# Generated build files
+gen/com/android/networkstack/**
+
+# IntelliJ project files
+**/.idea
+**/*.iml
+**/*.ipr
diff --git a/Android.bp b/Android.bp
index 37e001c..dd2d391 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22,12 +22,12 @@
 //                                            /    \
 //           +NetworkStackApiStableShims --> /      \ <-- +NetworkStackApiCurrentShims
 //           +NetworkStackReleaseApiLevel   /        \    +NetworkStackDevApiLevel
-//           +jarjar apistub.api[latest].* /          \   +module src/
-//            to apistub.*                /            \
+//           +jarjar apishim.api[latest].* /          \
+//            to apishim.*                /            \
 //                                       /              \
-//         NetworkStackApiStableDependencies             \
+//                                      /                \
 //                                     /                  \               android libs w/ all code
-//                   +module src/ --> /                    \              (also used in unit tests)
+//                                    / <- +module src/ -> \              (also used in unit tests)
 //                                   /                      \                        |
 //               NetworkStackApiStableLib               NetworkStackApiCurrentLib <--*
 //                          |                                     |
@@ -41,39 +41,171 @@
 //                                                         TestNetworkStack
 
 // Common defaults to define SDK level
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Whether to enable the targets in this file that target current SDKs.
+// Set to false in branches like mainline-prod where API classes are too old to build current code.
+enable_current_sdk_targets = true
+
 java_defaults {
     name: "NetworkStackDevApiLevel",
+    min_sdk_version: "29",
     sdk_version: "system_current",
 }
 
 java_defaults {
     name: "NetworkStackReleaseApiLevel",
-    sdk_version: "system_30",
+    sdk_version: "module_current",
     min_sdk_version: "29",
-    target_sdk_version: "30",
+    target_sdk_version: "31",
+    libs: [
+        "framework-connectivity",
+        "framework-statsd",
+        "framework-wifi",
+    ]
 }
 
-// Filegroups for the API shims
-filegroup {
-    name: "NetworkStackApiCurrentShims",
+// Libraries for the API shims
+java_defaults {
+    name: "NetworkStackShimsDefaults",
+    libs: [
+        "androidx.annotation_annotation",
+        "networkstack-client",
+    ],
+    static_libs : [
+        "modules-utils-build_system"
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",  // For InProcessNetworkStack and InProcessTethering
+    ],
+    min_sdk_version: "29",
+}
+
+// Common shim code. This includes the shim interface definitions themselves, and things like
+// ShimUtils and UnsupportedApiLevelException. Compiles against system_current because ShimUtils
+// needs access to all Build.VERSION_CODES.*, which by definition are only in the newest SDK.
+// TODO: consider moving ShimUtils into a library (or removing it in favour of SdkLevel) and compile
+// this target against the lowest-supported SDK (currently 29).
+java_library {
+    name: "NetworkStackShimsCommon",
+    defaults: ["NetworkStackShimsDefaults"],
+    srcs: ["apishim/common/**/*.java"],
+    sdk_version: "system_current",
+    visibility: ["//visibility:private"],
+}
+
+// Each level of the shims (29, 30, ...) is its own java_library compiled against the corresponding
+// system_X SDK. this ensures that each shim can only use SDK classes that exist in its SDK level.
+java_library {
+    name: "NetworkStackApi29Shims",
+    defaults: ["NetworkStackShimsDefaults"],
+    srcs: ["apishim/29/**/*.java"],
+    libs: [
+        "NetworkStackShimsCommon",
+    ],
+    sdk_version: "system_29",
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "NetworkStackApi30Shims",
+    defaults: ["NetworkStackShimsDefaults"],
     srcs: [
-        "apishim/common/**/*.java",
-        "apishim/29/**/*.java",
         "apishim/30/**/*.java",
+    ],
+    libs: [
+        "NetworkStackShimsCommon",
+        "NetworkStackApi29Shims",
+    ],
+    sdk_version: "system_30",
+    visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline-api-30-shims.xml",
+    },
+}
+
+// Shims for APIs being added to the current development version of Android. These APIs are not
+// stable and have no defined version number. These could be called 10000, but they use the next
+// integer so if the next SDK release happens to use that integer, we don't need to rename them.
+java_library {
+    name: "NetworkStackApi31Shims",
+    enabled: enable_current_sdk_targets,
+    defaults: ["NetworkStackShimsDefaults"],
+    srcs: [
         "apishim/31/**/*.java",
-        ":networkstack-module-utils-srcs",
+    ],
+    libs: [
+        "NetworkStackShimsCommon",
+        "NetworkStackApi29Shims",
+        "NetworkStackApi30Shims",
+        "framework-connectivity",
+    ],
+    sdk_version: "module_current",
+    visibility: ["//visibility:private"],
+}
+
+
+// Shims for APIs being added to the current development version of Android. These APIs are not
+// stable and have no defined version number. These could be called 10000, but they use the next
+// integer so if the next SDK release happens to use that integer, we don't need to rename them.
+java_library {
+    name: "NetworkStackApi32Shims",
+    defaults: ["NetworkStackShimsDefaults"],
+    srcs: [
+        "apishim/32/**/*.java",
+    ],
+    libs: [
+        "NetworkStackShimsCommon",
+        "NetworkStackApi29Shims",
+        "NetworkStackApi30Shims",
+        "NetworkStackApi31Shims",
+        "framework-connectivity",
+    ],
+    sdk_version: "module_current",
+    visibility: ["//visibility:private"],
+}
+
+// API current uses the API current shims directly.
+// The current (in-progress) shims are in the com.android.networkstack.apishim package and are
+// called directly by the networkstack code.
+java_library {
+    name: "NetworkStackApiCurrentShims",
+    enabled: enable_current_sdk_targets,
+    defaults: ["NetworkStackShimsDefaults"],
+    static_libs: [
+        "NetworkStackShimsCommon",
+        "NetworkStackApi29Shims",
+        "NetworkStackApi30Shims",
+        "NetworkStackApi31Shims",
+        "NetworkStackApi32Shims",
+    ],
+    sdk_version: "module_current",
+    visibility: [
+        "//packages/modules/Connectivity/Tethering",
+        "//packages/modules/Connectivity/tests/cts/net",
     ],
 }
 
-// API stable shims only include the compat package, but it is jarjared to replace the non-compat
-// package
-filegroup {
+// API stable uses jarjar to rename the latest stable apishim package from
+// com.android.networkstack.apishim.apiXX to com.android.networkstack.apishim, which is called by
+// the networkstack code.
+java_library {
     name: "NetworkStackApiStableShims",
-    srcs: [
-        "apishim/common/**/*.java",
-        "apishim/29/**/*.java",
-        "apishim/30/**/*.java",
-        ":networkstack-module-utils-srcs",
+    defaults: ["NetworkStackShimsDefaults"],
+    static_libs: [
+        "NetworkStackShimsCommon",
+        "NetworkStackApi29Shims",
+        "NetworkStackApi30Shims",
+        "NetworkStackApi31Shims",
+    ],
+    jarjar_rules: "apishim/jarjar-rules-compat.txt",
+    sdk_version: "module_current",
+    visibility: [
+        "//packages/modules/Connectivity/Tethering",
+        "//packages/modules/Connectivity/tests/cts/net",
     ],
 }
 
@@ -83,11 +215,13 @@
     name: "NetworkStackAndroidLibraryDefaults",
     srcs: [
         ":framework-networkstack-shared-srcs",
+        ":networkstack-module-utils-srcs",
     ],
     libs: ["unsupportedappusage"],
     static_libs: [
         "androidx.annotation_annotation",
-        "netd_aidl_interface-java",
+        "modules-utils-build_system",
+        "netd_aidl_interface-lateststable-java",
         "netlink-client",
         "networkstack-client",
         "net-utils-framework-common",
@@ -95,46 +229,55 @@
         "datastallprotosnano",
         "statsprotos",
         "captiveportal-lib",
+        "net-utils-device-common",
     ],
     plugins: ["java_api_finder"],
 }
 
-// The versions of the android library containing network stack code compiled for each SDK variant
-// API current uses the sources of the API current shims directly.
-// This allows API current code to be treated identically to code in src/ (it will be moved
-// there eventually), and to use the compat shim as fallback on older devices.
+// The versions of the android library containing network stack code compiled for each SDK variant.
 android_library {
     name: "NetworkStackApiCurrentLib",
     defaults: ["NetworkStackDevApiLevel", "NetworkStackAndroidLibraryDefaults"],
     srcs: [
-        ":NetworkStackApiCurrentShims",
         "src/**/*.java",
-        ":statslog-networkstack-java-gen"
+        ":statslog-networkstack-java-gen-current"
     ],
+    static_libs: ["NetworkStackApiCurrentShims"],
     manifest: "AndroidManifestBase.xml",
-}
-
-// For API stable, first build the dependencies using jarjar compat rules, then build the sources
-// linking with the dependencies.
-java_library {
-    name: "NetworkStackApiStableDependencies",
-    defaults: ["NetworkStackReleaseApiLevel", "NetworkStackAndroidLibraryDefaults"],
-    srcs: [":NetworkStackApiStableShims"],
-    jarjar_rules: "apishim/jarjar-rules-compat.txt",
+    enabled: enable_current_sdk_targets,
+    visibility: [
+        "//frameworks/base/tests/net/integration",
+        "//packages/modules/Connectivity/Tethering/tests/integration",
+        "//packages/modules/Connectivity/tests/cts/net",
+        "//packages/modules/NetworkStack/tests/unit",
+        "//packages/modules/NetworkStack/tests/integration",
+    ],
+    lint: {
+        baseline_filename: "lint-baseline-current-lib.xml",
+    },
 }
 
 android_library {
     name: "NetworkStackApiStableLib",
-    defaults: ["NetworkStackReleaseApiLevel"],
+    defaults: ["NetworkStackReleaseApiLevel", "NetworkStackAndroidLibraryDefaults"],
     srcs: [
         "src/**/*.java",
-        ":statslog-networkstack-java-gen",
+        ":statslog-networkstack-java-gen-stable",
     ],
-    // API stable uses a jarjared version of the shims
-    static_libs: [
-        "NetworkStackApiStableDependencies",
-    ],
+    static_libs: ["NetworkStackApiStableShims"],
     manifest: "AndroidManifestBase.xml",
+    visibility: [
+        "//frameworks/base/packages/Connectivity/tests/integration",
+        "//frameworks/base/tests/net/integration",
+        "//packages/modules/Connectivity/Tethering/tests/integration",
+        "//packages/modules/Connectivity/tests/cts/net",
+        "//packages/modules/Connectivity/tests/integration",
+        "//packages/modules/NetworkStack/tests/unit",
+        "//packages/modules/NetworkStack/tests/integration",
+    ],
+    lint: {
+        baseline_filename: "lint-baseline-stable-lib.xml",
+    },
 }
 
 filegroup {
@@ -143,7 +286,7 @@
     visibility: [
         "//packages/modules/NetworkStack/tests/unit",
         "//packages/modules/NetworkStack/tests/integration",
-        "//frameworks/base/packages/Tethering/tests/integration",
+        "//packages/modules/Connectivity/Tethering/tests/integration",
     ]
 }
 
@@ -176,7 +319,11 @@
     // The permission configuration *must* be included to ensure security of the device
     // The InProcessNetworkStack goes together with the PlatformCaptivePortalLogin, which replaces
     // the default CaptivePortalLogin.
-    required: ["PlatformNetworkPermissionConfig", "PlatformCaptivePortalLogin"],
+    required: [
+        "PlatformNetworkPermissionConfig",
+        "PlatformCaptivePortalLogin",
+    ],
+    enabled: enable_current_sdk_targets,
 }
 
 // Pre-merge the AndroidManifest for NetworkStackNext, so that its manifest can be merged on top
@@ -184,7 +331,8 @@
     name: "NetworkStackNextManifestBase",
     defaults: ["NetworkStackAppDefaults", "NetworkStackDevApiLevel"],
     static_libs: ["NetworkStackApiCurrentLib"],
-    manifest: "AndroidManifest.xml"
+    manifest: "AndroidManifest.xml",
+    enabled: enable_current_sdk_targets,
 }
 
 // NetworkStack build targeting the current API release, for testing on in-development SDK
@@ -195,7 +343,11 @@
     certificate: "networkstack",
     manifest: "AndroidManifest_Next.xml",
     // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
+    required: [
+        "NetworkPermissionConfig",
+        "privapp_whitelist_com.android.networkstack",
+    ],
+    enabled: enable_current_sdk_targets,
 }
 
 // Updatable network stack for finalized API
@@ -206,20 +358,11 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
-    updatable: true,
-}
-
-// Android library to derive test APKs for integration tests
-android_library {
-    name: "TestNetworkStackLib",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
-    static_libs: ["NetworkStackApiStableLib"],
-    manifest: "AndroidManifestBase.xml",
-    visibility: [
-        "//frameworks/base/tests/net/integration",
-        "//cts/tests/tests/net",
+    required: [
+        "NetworkPermissionConfig",
+        "privapp_whitelist_com.android.networkstack",
     ],
+    updatable: true,
 }
 
 cc_library_shared {
@@ -233,6 +376,9 @@
         "liblog",
         "libnativehelper_compat_libc++",
     ],
+    static_libs: [
+        "libnetjniutils",
+    ],
 
     // We cannot use plain "libc++" here to link libc++ dynamically because it results in:
     //   java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found
@@ -254,14 +400,24 @@
 }
 
 genrule {
-    name: "statslog-networkstack-java-gen",
+    name: "statslog-networkstack-java-gen-current",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" +
          " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
-         " --supportQ",
+         " --minApiLevel 29",
     out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
 }
 
+genrule {
+    name: "statslog-networkstack-java-gen-stable",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" +
+         " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
+         " --minApiLevel 29 --compileApiLevel 30",
+    out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
+}
+
+
 version_code_networkstack_next = "300000000"
 version_code_networkstack_test = "999999999"
 
@@ -277,12 +433,15 @@
 
 android_app {
     name: "TestNetworkStack",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackDevApiLevel"],
-    static_libs: ["NetworkStackApiCurrentLib"],
+    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
+    static_libs: ["NetworkStackApiStableLib"],
     certificate: "networkstack",
     manifest: ":NetworkStackTestAndroidManifest",
     // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
+    required: [
+        "NetworkPermissionConfig",
+        "privapp_whitelist_com.android.networkstack",
+    ],
 }
 
 // When adding or modifying protos, the jarjar rules and possibly proguard rules need
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6524efe..6a11b2c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -19,13 +19,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.android.networkstack"
   android:sharedUserId="android.uid.networkstack"
-  android:versionCode="300000000"
-  android:versionName="2019-09"
+  android:versionCode="319999900"
+  android:versionName="s_aml_319999900"
 >
     <!-- Permissions must be defined here, and not in the base manifest, as the network stack
          running in the system server process does not need any permission, and having privileged
          permissions added would cause crashes on startup unless they are also added to the
-         privileged permissions whitelist for that package. -->
+         privileged permissions allowlist for that package. -->
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
@@ -44,11 +44,23 @@
         android:persistent="true"
         android:process="com.android.networkstack.process">
         <service android:name="com.android.server.NetworkStackService"
+                 android:exported="true"
                  android:permission="android.permission.MAINLINE_NETWORK_STACK">
             <intent-filter>
                 <action android:name="android.net.INetworkStackConnector"/>
             </intent-filter>
         </service>
+        <!-- Test instrumentation service, only usable on debuggable builds.
+             The service is protected by NETWORK_SETTINGS permissions as there is no better
+             networking-related permission that exists on Q, is sufficiently protected (signature),
+             and can be obtained via shell permissions. -->
+        <service android:name="com.android.server.TestNetworkStackService"
+                 android:permission="android.permission.NETWORK_SETTINGS"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.net.INetworkStackConnector.Test"/>
+            </intent-filter>
+        </service>
         <service android:name="com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
diff --git a/AndroidManifest_InProcess.xml b/AndroidManifest_InProcess.xml
index 41b2e86..2cb146a 100644
--- a/AndroidManifest_InProcess.xml
+++ b/AndroidManifest_InProcess.xml
@@ -23,6 +23,7 @@
     <application>
         <service android:name="com.android.server.NetworkStackService"
                  android:process="system"
+                 android:exported="true"
                  android:permission="android.permission.MAINLINE_NETWORK_STACK">
             <intent-filter>
                 <action android:name="android.net.INetworkStackConnector.InProcess"/>
diff --git a/AndroidManifest_Next.xml b/AndroidManifest_Next.xml
index 02fcb64..9ad69ae 100644
--- a/AndroidManifest_Next.xml
+++ b/AndroidManifest_Next.xml
@@ -17,6 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.networkstack"
           android:sharedUserId="android.uid.networkstack"
-          android:versionCode="300000000"
-          android:versionName="R-next">
+          android:versionCode="320000000"
+          android:versionName="T-next">
 </manifest>
diff --git a/OWNERS b/OWNERS
index 0e1e65d..8cb7492 100644
--- a/OWNERS
+++ b/OWNERS
@@ -2,5 +2,7 @@
 jchalard@google.com
 junyulai@google.com
 lorenzo@google.com
+maze@google.com
 reminv@google.com
 satk@google.com
+xiaom@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 48370c0..ed6a1a8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -5,14 +5,52 @@
     },
     {
       "name": "NetworkStackNextTests"
+    },
+    {
+      "name": "NetworkStackIntegrationTests"
     }
   ],
   "postsubmit": [
     {
       "name": "NetworkStackHostTests"
+    }
+  ],
+  "auto-postsubmit": [
+    // Test tag for automotive targets. These are only running in postsubmit so as to harden the
+    // automotive targets to avoid introducing additional test flake and build time. The plan for
+    // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
+    // Additionally, this tag is used in targeted test suites to limit resource usage on the test
+    // infra during the hardening phase.
+    // TODO: this tag to be removed once the above is no longer an issue.
+    {
+      "name": "NetworkStackTests"
+    },
+    {
+      "name": "NetworkStackNextTests"
+    },
+    {
+      "name": "NetworkStackHostTests"
     },
     {
       "name": "NetworkStackIntegrationTests"
     }
+  ],
+  "mainline-presubmit": [
+    // These are unit tests only, so they don't actually require any modules to be installed.
+    // We must specify at least one module here or the tests won't run. Use the same set as CTS
+    // so in theory the infra would not need to reinstall/reboot devices to run both.
+    {
+      "name": "NetworkStackTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    }
+  ],
+  "mainline-postsubmit": [
+    {
+      "name": "NetworkStackIntegrationTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    }
+  ],
+  "imports": [
+    {
+      "path": "packages/modules/Connectivity"
+    }
   ]
 }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
index 681ba1a..8719e83 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.apishim.api29;
 
+import android.net.Uri;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
@@ -26,7 +28,7 @@
 import org.json.JSONObject;
 
 /**
- * Compatibility implementation of {@link CaptivePortalDataShim}.
+ * Compatibility implementation of {@link CaptivePortalData}.
  *
  * <p>Use {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl} instead of this
  * fallback implementation.
@@ -35,7 +37,7 @@
     protected CaptivePortalDataShimImpl() {}
 
     /**
-     * Parse a {@link android.net.CaptivePortalData} from JSON.
+     * Parse a {@link android.net.CaptivePortalDataShim} from JSON.
      *
      * <p>Use
      * {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl#fromJson(JSONObject)}
@@ -48,8 +50,52 @@
         throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
     }
 
+    @Override
+    public CharSequence getVenueFriendlyName() {
+        // Not supported in API level 29
+        return null;
+    }
+
+    @Override
+    public int getUserPortalUrlSource() {
+        // Not supported in API level 29
+        return ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_OTHER;
+    }
+
     @VisibleForTesting
     public static boolean isSupported() {
         return false;
     }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalData} object with a friendly name set
+     */
+    @Override
+    public CaptivePortalDataShim withVenueFriendlyName(String friendlyName)
+            throws UnsupportedApiLevelException {
+        // Not supported in API level 29
+        throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
+    }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name and Passpoint external
+     * URLs set
+     *
+     * @param friendlyName The friendly name to set
+     * @param venueInfoUrl Venue information URL
+     * @param termsAndConditionsUrl Terms and conditions URL
+     *
+     * @return a {@link CaptivePortalDataShim} object with friendly name, venue info URL and terms
+     * and conditions URL set
+     */
+    @Override
+    public CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName,
+            @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl)
+            throws UnsupportedApiLevelException {
+        // Not supported in API level 29
+        throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
+    }
 }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java
new file mode 100644
index 0000000..07327be
--- /dev/null
+++ b/apishim/29/com/android/networkstack/apishim/api29/ConnectivityManagerShimImpl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api29;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+
+/**
+ * Implementation of {@link ConnectivityManagerShim} for API 29.
+ */
+public class ConnectivityManagerShimImpl implements ConnectivityManagerShim {
+    protected final ConnectivityManager mCm;
+    protected ConnectivityManagerShimImpl(Context context) {
+        mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    /**
+     * Get a new instance of {@link ConnectivityManagerShim}.
+     */
+    public static ConnectivityManagerShim newInstance(Context context) {
+        return new ConnectivityManagerShimImpl(context);
+    }
+    /**
+     * See android.net.ConnectivityManager#requestBackgroundNetwork
+     * @throws UnsupportedApiLevelException if API is not available in this API level.
+     */
+    @Override
+    public void requestBackgroundNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler)
+            throws UnsupportedApiLevelException {
+        // Not supported for API 29.
+        throw new UnsupportedApiLevelException("Not supported in API 29.");
+    }
+
+    /**
+     * See android.net.ConnectivityManager#registerSystemDefaultNetworkCallback
+     */
+    @Override
+    public void registerSystemDefaultNetworkCallback(@NonNull NetworkCallback networkCallback,
+            @NonNull Handler handler) {
+        // defaultNetworkRequest is not really a "request", just a way of tracking the system
+        // default network. It's guaranteed not to actually bring up any networks because it
+        // should be the same request as the ConnectivityService default request, and thus
+        // shares fate with it.  In API <= R, registerSystemDefaultNetworkCallback is not
+        // available, and registerDefaultNetworkCallback will not track the system default when
+        // a VPN applies to the UID of this process.
+        final NetworkRequest defaultNetworkRequest = makeEmptyCapabilitiesRequest()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .build();
+        mCm.requestNetwork(defaultNetworkRequest, networkCallback, handler);
+    }
+
+    @NonNull
+    protected NetworkRequest.Builder makeEmptyCapabilitiesRequest() {
+        // Q does not have clearCapabilities(), so assume the default capabilities are as below
+        return new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_VPN);
+    }
+}
diff --git a/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java b/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
index b655858..0b000a9 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
@@ -34,4 +34,12 @@
     // Constants defined in android.net.ConnectivityDiagnosticsManager.
     public static final int DETECTION_METHOD_DNS_EVENTS = 1;
     public static final int DETECTION_METHOD_TCP_METRICS = 2;
+
+    // Constants defined in android.net.CaptivePortalData.
+    public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0;
+    public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1;
+
+    // Constants defined in android.net.NetworkCapabilities.
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+
 }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
index 1fa4907..e68020b 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
@@ -57,6 +57,14 @@
         return false;
     }
 
+    /**
+     * Indicates whether the shim can use APIs above the R SDK.
+     */
+    @VisibleForTesting
+    public static boolean useApiAboveR() {
+        return false;
+    }
+
     @Nullable
     @Override
     public Uri getCaptivePortalApiUrl(@Nullable LinkProperties lp) {
@@ -105,4 +113,14 @@
             @NonNull Inet4Address serverAddress) {
         // Not supported on this API level: no-op
     }
+
+    /**
+     * Set captive portal data in {@link LinkProperties}
+     * @param lp Link properties object to be updated
+     * @param captivePortalData Captive portal data to be used
+     */
+    public void setCaptivePortalData(@NonNull LinkProperties lp,
+            @Nullable CaptivePortalDataShim captivePortalData) {
+        // Not supported on this API level: no-op
+    }
 }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..0c1d837
--- /dev/null
+++ b/apishim/29/com/android/networkstack/apishim/api29/NetworkRequestShimImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api29;
+
+import android.net.NetworkRequest;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+
+import java.util.Set;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 29.
+ */
+public class NetworkRequestShimImpl implements NetworkRequestShim {
+    protected NetworkRequestShimImpl() {}
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    public static NetworkRequestShim newInstance() {
+        return new NetworkRequestShimImpl();
+    }
+
+    @Override
+    public void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) throws UnsupportedApiLevelException {
+        // Not supported before API 31.
+        throw new UnsupportedApiLevelException("Not supported before API 31.");
+    }
+}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
index 5639386..5825021 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
@@ -37,12 +37,16 @@
 public class CaptivePortalDataShimImpl
         extends com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl {
     @NonNull
-    private final CaptivePortalData mData;
+    protected final CaptivePortalData mData;
 
-    protected CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
+    public CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
         mData = data;
     }
 
+    public CaptivePortalData getData() {
+        return mData;
+    }
+
     /**
      * Parse a {@link CaptivePortalDataShim} from a JSON object.
      * @throws JSONException The JSON is not a representation of correct captive portal data.
@@ -116,4 +120,36 @@
     public void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException {
         cb.notifyCaptivePortalDataChanged(mData);
     }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalDataShim} object with a friendly name set
+     */
+    @Override
+    public CaptivePortalDataShim withVenueFriendlyName(String friendlyName)
+            throws UnsupportedApiLevelException {
+        // Not supported in API level 29
+        throw new UnsupportedApiLevelException("FriendlyName not supported on API 30");
+    }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name and Passpoint external
+     * URLs set
+     *
+     * @param friendlyName The friendly name to set
+     * @param venueInfoUrl Venue information URL
+     * @param termsAndConditionsUrl Terms and conditions URL
+     *
+     * @return a {@link CaptivePortalDataShim} object with friendly name, venue info URL and terms
+     * and conditions URL set
+     */
+    @Override
+    public CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName,
+            @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl)
+            throws UnsupportedApiLevelException {
+        // Not supported in API level 29
+        throw new UnsupportedApiLevelException("PasspointInfo not supported on API 30");
+    }
 }
diff --git a/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java
new file mode 100644
index 0000000..7c1d786
--- /dev/null
+++ b/apishim/30/com/android/networkstack/apishim/api30/ConnectivityManagerShimImpl.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api30;
+
+import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
+
+import android.content.Context;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+
+/**
+ * Implementation of {@link ConnectivityManagerShim} for API 30.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public class ConnectivityManagerShimImpl
+        extends com.android.networkstack.apishim.api29.ConnectivityManagerShimImpl {
+    protected ConnectivityManagerShimImpl(Context context) {
+        super(context);
+    }
+
+    /**
+     * Get a new instance of {@link ConnectivityManagerShim}.
+     */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    public static ConnectivityManagerShim newInstance(Context context) {
+        if (!isAtLeastR()) {
+            return com.android.networkstack.apishim.api29.ConnectivityManagerShimImpl
+                    .newInstance(context);
+        }
+        return new ConnectivityManagerShimImpl(context);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#requestBackgroundNetwork
+     * @throws UnsupportedApiLevelException if API is not available in this API level.
+     */
+    @Override
+    public void requestBackgroundNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler)
+            throws UnsupportedApiLevelException {
+        // Not supported for API 30.
+        throw new UnsupportedApiLevelException("Not supported in API 30.");
+    }
+
+    @NonNull
+    @Override
+    protected NetworkRequest.Builder makeEmptyCapabilitiesRequest() {
+        return new NetworkRequest.Builder().clearCapabilities();
+    }
+}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/ConstantsShim.java b/apishim/30/com/android/networkstack/apishim/api30/ConstantsShim.java
index 27fd745..19ff9d3 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/ConstantsShim.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/ConstantsShim.java
@@ -38,8 +38,12 @@
     public static final int DETECTION_METHOD_TCP_METRICS =
             DataStallReport.DETECTION_METHOD_TCP_METRICS;
 
-    /**
-     * @see android.net.NetworkCapabilities
-     */
+    // Constants defined in android.net.ConnectivityManager.
+    public static final int BLOCKED_REASON_NONE = 0;
+    public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16;
+
+    // Constants defined in android.net.NetworkCapabilities.
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+    public static final int NET_CAPABILITY_ENTERPRISE = 29;
     public static final int TRANSPORT_TEST = 7;
 }
diff --git a/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java
index 5d9b013..477dd42 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java
@@ -21,6 +21,7 @@
 import android.net.NetworkCapabilities;
 import android.net.Uri;
 import android.os.Build;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -37,6 +38,8 @@
  */
 public class NetworkInformationShimImpl extends
         com.android.networkstack.apishim.api29.NetworkInformationShimImpl {
+    private static final String TAG = "api30.NetworkInformationShimImpl";
+
     protected NetworkInformationShimImpl() {}
 
     /**
@@ -105,4 +108,20 @@
             @NonNull Inet4Address serverAddress) {
         lp.setDhcpServerAddress(serverAddress);
     }
+
+    @Override
+    public void setCaptivePortalData(@NonNull LinkProperties lp,
+            @Nullable CaptivePortalDataShim captivePortalData) {
+        if (lp == null) {
+            return;
+        }
+        if (!(captivePortalData instanceof CaptivePortalDataShimImpl)) {
+            // The caller passed in a subclass that is not a CaptivePortalDataShimImpl.
+            // This is a programming error, but don't crash with ClassCastException.
+            Log.wtf(TAG, "Expected CaptivePortalDataShimImpl, but got "
+                    + captivePortalData.getClass().getName());
+            return;
+        }
+        lp.setCaptivePortalData(((CaptivePortalDataShimImpl) captivePortalData).getData());
+    }
 }
diff --git a/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..b65a556
--- /dev/null
+++ b/apishim/30/com/android/networkstack/apishim/api30/NetworkRequestShimImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api30;
+
+import android.os.Build;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 30.
+ */
+public class NetworkRequestShimImpl
+        extends com.android.networkstack.apishim.api29.NetworkRequestShimImpl {
+    protected NetworkRequestShimImpl() {
+        super();
+    }
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    public static NetworkRequestShim newInstance() {
+        if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) {
+            return com.android.networkstack.apishim.api29.NetworkRequestShimImpl
+                    .newInstance();
+        }
+        return new NetworkRequestShimImpl();
+    }
+}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/SettingsShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/SettingsShimImpl.java
new file mode 100644
index 0000000..b8188c6
--- /dev/null
+++ b/apishim/30/com/android/networkstack/apishim/api30/SettingsShimImpl.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api30;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.apishim.common.SettingsShim;
+
+/**
+ * Implementation of {@link SettingsShim} for API 30.
+ */
+public class SettingsShimImpl implements SettingsShim {
+    protected SettingsShimImpl() { }
+
+    /**
+     * Get a new instance of {@link SettingsShim}.
+     *
+     * Use com.android.networkstack.apishim.SeetingsShim#newInstance()
+     * (non-API30 version) instead, to use the correct shims depending on build SDK.
+     */
+    public static SettingsShim newInstance() {
+        return new SettingsShimImpl();
+    }
+
+    @Override
+    public boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+            @NonNull String callingPackage, @Nullable String callingAttributionTag,
+            boolean throwException) {
+        return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
+                throwException);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/api31/CaptivePortalDataShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/CaptivePortalDataShimImpl.java
new file mode 100644
index 0000000..5ae006b
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/api31/CaptivePortalDataShimImpl.java
@@ -0,0 +1,83 @@
+/*
+ * 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.networkstack.apishim.api31;
+
+import android.net.CaptivePortalData;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+
+/**
+ * Compatibility implementation of {@link CaptivePortalDataShim}.
+ */
+public class CaptivePortalDataShimImpl
+        extends com.android.networkstack.apishim.api30.CaptivePortalDataShimImpl {
+    public CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
+        super(data);
+    }
+
+    @Override
+    public CharSequence getVenueFriendlyName() {
+        return mData.getVenueFriendlyName();
+    }
+
+    /**
+     * Get the information source of the User portal
+     * @return The source that the User portal was obtained from
+     */
+    @Override
+    public int getUserPortalUrlSource() {
+        return mData.getUserPortalUrlSource();
+    }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalDataShim} object with a friendly name set
+     */
+    @Override
+    public CaptivePortalDataShim withVenueFriendlyName(String friendlyName) {
+        return new CaptivePortalDataShimImpl(new CaptivePortalData.Builder(mData)
+                .setVenueFriendlyName(friendlyName)
+                .build());
+    }
+
+    /**
+     * Generate a {@link CaptivePortalDataShim} object with a friendly name and Passpoint external
+     * URLs set
+     *
+     * @param friendlyName The friendly name to set
+     * @param venueInfoUrl Venue information URL
+     * @param termsAndConditionsUrl Terms and conditions URL
+     *
+     * @return a {@link CaptivePortalDataShim} object with friendly name, venue info URL and terms
+     * and conditions URL set
+     */
+    @Override
+    public CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName,
+            @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl) {
+        return new CaptivePortalDataShimImpl(new CaptivePortalData.Builder(mData)
+                .setVenueFriendlyName(friendlyName)
+                .setVenueInfoUrl(venueInfoUrl, ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .setUserPortalUrl(termsAndConditionsUrl,
+                        ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .build());
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/api31/ConnectivityManagerShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/ConnectivityManagerShimImpl.java
new file mode 100644
index 0000000..201b5d8
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/api31/ConnectivityManagerShimImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api31;
+
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.content.Context;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+
+import java.util.Collection;
+
+/**
+ * Implementation of {@link ConnectivityManagerShim} for API 31.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class ConnectivityManagerShimImpl
+        extends com.android.networkstack.apishim.api30.ConnectivityManagerShimImpl  {
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    protected ConnectivityManagerShimImpl(Context context) {
+        super(context);
+    }
+
+    /**
+     * Get a new instance of {@link ConnectivityManagerShim}.
+     */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    public static ConnectivityManagerShim newInstance(Context context) {
+        if (!isAtLeastS()) {
+            return com.android.networkstack.apishim.api30.ConnectivityManagerShimImpl
+                    .newInstance(context);
+        }
+        return new ConnectivityManagerShimImpl(context);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#requestBackgroundNetwork
+     */
+    @Override
+    public void requestBackgroundNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        mCm.requestBackgroundNetwork(request, networkCallback, handler);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#registerSystemDefaultNetworkCallback
+     */
+    @Override
+    public void registerSystemDefaultNetworkCallback(
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        mCm.registerSystemDefaultNetworkCallback(networkCallback, handler);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#registerDefaultNetworkCallbackAsUid
+     */
+    @Override
+    public void registerDefaultNetworkCallbackForUid(
+            int uid, @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        mCm.registerDefaultNetworkCallbackForUid(uid, networkCallback, handler);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#setLegacyLockdownVpnEnabled
+     */
+    @Override
+    public void setLegacyLockdownVpnEnabled(boolean enabled) {
+        mCm.setLegacyLockdownVpnEnabled(enabled);
+    }
+
+    /**
+     * See android.net.ConnectivityManager#setRequireVpnForUids
+     */
+    @Override
+    public void setRequireVpnForUids(boolean requireVpn, Collection<Range<Integer>> ranges) {
+        mCm.setRequireVpnForUids(requireVpn, ranges);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/ConstantsShim.java b/apishim/31/com/android/networkstack/apishim/api31/ConstantsShim.java
similarity index 96%
rename from apishim/31/com/android/networkstack/apishim/ConstantsShim.java
rename to apishim/31/com/android/networkstack/apishim/api31/ConstantsShim.java
index 0184845..95ff072 100644
--- a/apishim/31/com/android/networkstack/apishim/ConstantsShim.java
+++ b/apishim/31/com/android/networkstack/apishim/api31/ConstantsShim.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.apishim;
+package com.android.networkstack.apishim.api31;
 
 import androidx.annotation.VisibleForTesting;
 
diff --git a/apishim/31/com/android/networkstack/apishim/api31/NetworkInformationShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/NetworkInformationShimImpl.java
new file mode 100644
index 0000000..a5c9a71
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/api31/NetworkInformationShimImpl.java
@@ -0,0 +1,69 @@
+/*
+ * 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.networkstack.apishim.api31;
+
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+import com.android.networkstack.apishim.common.NetworkInformationShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
+/**
+ * Compatibility implementation of {@link NetworkInformationShim}.
+ */
+public class NetworkInformationShimImpl
+        extends com.android.networkstack.apishim.api30.NetworkInformationShimImpl {
+    protected NetworkInformationShimImpl() {}
+
+    /**
+     * Indicates whether the shim can use APIs above the R SDK.
+     */
+    @VisibleForTesting
+    public static boolean useApiAboveR() {
+        return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R);
+    }
+
+    /**
+     * Get a new instance of {@link NetworkInformationShim}.
+     */
+    public static NetworkInformationShim newInstance() {
+        if (!useApiAboveR()) {
+            return com.android.networkstack.apishim.api30.NetworkInformationShimImpl.newInstance();
+        }
+        return new NetworkInformationShimImpl();
+    }
+
+    @Nullable
+    @Override
+    public CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp) {
+        if (lp == null || lp.getCaptivePortalData() == null) return null;
+        return new CaptivePortalDataShimImpl(lp.getCaptivePortalData());
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    @Nullable
+    @Override
+    public String getCapabilityCarrierName(int capability) {
+        return NetworkCapabilities.getCapabilityCarrierName(capability);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/api31/NetworkRequestShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..29b2e0f
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/api31/NetworkRequestShimImpl.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api31;
+
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+
+import java.util.Set;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 31.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class NetworkRequestShimImpl
+        extends com.android.networkstack.apishim.api30.NetworkRequestShimImpl {
+    @RequiresApi(Build.VERSION_CODES.S)
+    protected NetworkRequestShimImpl() {
+        super();
+    }
+
+    /**
+     * Get a new instance of {@link NetworkRequestShim}.
+     */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    public static NetworkRequestShim newInstance() {
+        if (!isAtLeastS()) {
+            return com.android.networkstack.apishim.api30.NetworkRequestShimImpl.newInstance();
+        }
+        return new NetworkRequestShimImpl();
+    }
+
+    @Override
+    public void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) {
+        builder.setUids(uids);
+    }
+
+    @Override
+    public NetworkRequest.Builder newBuilder(@NonNull NetworkRequest request) {
+        return new NetworkRequest.Builder(request);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/NetworkShimImpl.java
similarity index 95%
rename from apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java
rename to apishim/31/com/android/networkstack/apishim/api31/NetworkShimImpl.java
index 0c92391..eda8e27 100644
--- a/apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/api31/NetworkShimImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.apishim;
+package com.android.networkstack.apishim.api31;
 
 import android.net.Network;
 
diff --git a/apishim/31/com/android/networkstack/apishim/api31/SettingsShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/SettingsShimImpl.java
new file mode 100644
index 0000000..a7c9fb0
--- /dev/null
+++ b/apishim/31/com/android/networkstack/apishim/api31/SettingsShimImpl.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.api31;
+
+import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.apishim.common.SettingsShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
+/**
+ * Implementation of {@link SettingsShim} for API 31.
+ */
+public class SettingsShimImpl
+        extends com.android.networkstack.apishim.api30.SettingsShimImpl {
+    protected SettingsShimImpl() { }
+
+    /**
+     * Get a new instance of {@link SettingsShim}.
+     */
+    public static SettingsShim newInstance() {
+        if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R)) {
+            return com.android.networkstack.apishim.api30.SettingsShimImpl
+                    .newInstance();
+        }
+        return new SettingsShimImpl();
+    }
+
+    @Override
+    public boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+            @NonNull String callingPackage, @Nullable String callingAttributionTag,
+            boolean throwException) {
+        // Since checkAndNoteWriteSettingsOperation with callingAttributionTag (S method) is not
+        // available in AOSP, calling R method (same as API 30 shim) temporary.
+        return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
+                throwException);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/SocketUtilsShimImpl.java
similarity index 94%
rename from apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java
rename to apishim/31/com/android/networkstack/apishim/api31/SocketUtilsShimImpl.java
index 483bde0..f5aa80b 100644
--- a/apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/api31/SocketUtilsShimImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.apishim;
+package com.android.networkstack.apishim.api31;
 
 /**
  * Implementation of {@link NetworkShim} for API 30.
diff --git a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java b/apishim/32/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
similarity index 63%
rename from apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
rename to apishim/32/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
index 6ff6adb..2056b1b 100644
--- a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
+++ b/apishim/32/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -17,16 +17,21 @@
 package com.android.networkstack.apishim;
 
 import android.net.CaptivePortalData;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
 
 /**
  * Compatibility implementation of {@link CaptivePortalDataShim}.
  */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
 public class CaptivePortalDataShimImpl
-        extends com.android.networkstack.apishim.api30.CaptivePortalDataShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
-    protected CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
+        extends com.android.networkstack.apishim.api31.CaptivePortalDataShimImpl {
+    // Currently identical to the API 31 shim, so inherit everything
+    public CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
         super(data);
     }
 }
diff --git a/apishim/32/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java b/apishim/32/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java
new file mode 100644
index 0000000..a7aa0c8
--- /dev/null
+++ b/apishim/32/com/android/networkstack/apishim/ConnectivityManagerShimImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+
+/**
+ * Compatibility implementation of {@link ConnectivityManagerShim}.
+ */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
+public class ConnectivityManagerShimImpl
+        extends com.android.networkstack.apishim.api31.ConnectivityManagerShimImpl  {
+    // Currently identical to the API 31 shim, so inherit everything
+    protected ConnectivityManagerShimImpl(Context context) {
+        super(context);
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/ConstantsShim.java b/apishim/32/com/android/networkstack/apishim/ConstantsShim.java
similarity index 76%
copy from apishim/31/com/android/networkstack/apishim/ConstantsShim.java
copy to apishim/32/com/android/networkstack/apishim/ConstantsShim.java
index 0184845..0a5b555 100644
--- a/apishim/31/com/android/networkstack/apishim/ConstantsShim.java
+++ b/apishim/32/com/android/networkstack/apishim/ConstantsShim.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -21,7 +21,7 @@
 /**
  * Utility class for defining and importing constants from the Android platform.
  */
-public class ConstantsShim extends com.android.networkstack.apishim.api30.ConstantsShim {
+public class ConstantsShim extends com.android.networkstack.apishim.api31.ConstantsShim {
     /**
      * Constant that callers can use to determine what version of the shim they are using.
      * Must be the same as the version of the shims.
@@ -29,9 +29,5 @@
      * the shimmed objects and methods themselves.
      */
     @VisibleForTesting
-    public static final int VERSION = 31;
-
-    // When removing this shim, the version in NetworkMonitorUtils should be removed too.
-    // TODO: add TRANSPORT_TEST to system API in API 31 (it is only a test API as of R)
-    public static final int TRANSPORT_TEST = 7;
+    public static final int VERSION = 32;
 }
diff --git a/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java b/apishim/32/com/android/networkstack/apishim/NetworkInformationShimImpl.java
similarity index 65%
rename from apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java
rename to apishim/32/com/android/networkstack/apishim/NetworkInformationShimImpl.java
index 828063c..28aa75c 100644
--- a/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java
+++ b/apishim/32/com/android/networkstack/apishim/NetworkInformationShimImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -16,11 +16,18 @@
 
 package com.android.networkstack.apishim;
 
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.NetworkInformationShim;
+
 /**
  * Compatibility implementation of {@link NetworkInformationShim}.
  */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
 public class NetworkInformationShimImpl
-        extends com.android.networkstack.apishim.api30.NetworkInformationShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
+        extends com.android.networkstack.apishim.api31.NetworkInformationShimImpl {
+    // Currently identical to the API 31 shim, so inherit everything
     protected NetworkInformationShimImpl() {}
 }
diff --git a/apishim/32/com/android/networkstack/apishim/NetworkRequestShimImpl.java b/apishim/32/com/android/networkstack/apishim/NetworkRequestShimImpl.java
new file mode 100644
index 0000000..95ae5ba
--- /dev/null
+++ b/apishim/32/com/android/networkstack/apishim/NetworkRequestShimImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+
+/**
+ * Implementation of {@link NetworkRequestShim} for API 31.
+ */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
+public class NetworkRequestShimImpl
+        extends com.android.networkstack.apishim.api31.NetworkRequestShimImpl {
+    // Currently identical to the API 31 shim, so inherit everything
+    protected NetworkRequestShimImpl() {
+        super();
+    }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java b/apishim/32/com/android/networkstack/apishim/NetworkShimImpl.java
similarity index 70%
copy from apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java
copy to apishim/32/com/android/networkstack/apishim/NetworkShimImpl.java
index 0c92391..2e31a78 100644
--- a/apishim/31/com/android/networkstack/apishim/NetworkShimImpl.java
+++ b/apishim/32/com/android/networkstack/apishim/NetworkShimImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -17,14 +17,17 @@
 package com.android.networkstack.apishim;
 
 import android.net.Network;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
 /**
- * Implementation of {@link NetworkShim} for API 30.
+ * Compatibility implementation of {@link com.android.networkstack.apishim.common.NetworkShim}.
  */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
 public class NetworkShimImpl extends com.android.networkstack.apishim.api30.NetworkShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
+    // Currently, this is the same as the API 31 shim, so inherit everything from that.
     protected NetworkShimImpl(@NonNull Network network) {
         super(network);
     }
diff --git a/apishim/32/com/android/networkstack/apishim/SettingsShimImpl.java b/apishim/32/com/android/networkstack/apishim/SettingsShimImpl.java
new file mode 100644
index 0000000..46d2102
--- /dev/null
+++ b/apishim/32/com/android/networkstack/apishim/SettingsShimImpl.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.networkstack.apishim.common.SettingsShim;
+
+/**
+ * Compatibility implementation of {@link SettingsShim} for API 31.
+ */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
+public class SettingsShimImpl
+        extends com.android.networkstack.apishim.api30.SettingsShimImpl {
+    // Currently identical to the API 31 shim, so inherit everything
+    protected SettingsShimImpl() { }
+}
diff --git a/apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java b/apishim/32/com/android/networkstack/apishim/SocketUtilsShimImpl.java
similarity index 68%
copy from apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java
copy to apishim/32/com/android/networkstack/apishim/SocketUtilsShimImpl.java
index 483bde0..2f4e500 100644
--- a/apishim/31/com/android/networkstack/apishim/SocketUtilsShimImpl.java
+++ b/apishim/32/com/android/networkstack/apishim/SocketUtilsShimImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -16,11 +16,16 @@
 
 package com.android.networkstack.apishim;
 
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
 /**
- * Implementation of {@link NetworkShim} for API 30.
+ * Implementation of {@link com.android.networkstack.apishim.common.SocketUtilsShim}.
  */
+@RequiresApi(Build.VERSION_CODES.S) // Change to T when version code available, and adding T methods
 public class SocketUtilsShimImpl
         extends com.android.networkstack.apishim.api30.SocketUtilsShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
+    // Currently, this is the same as the API 31 shim, so inherit everything from that.
     protected SocketUtilsShimImpl() {}
 }
diff --git a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
index a18ba49..13bf257 100644
--- a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
@@ -20,6 +20,8 @@
 import android.net.Uri;
 import android.os.RemoteException;
 
+import androidx.annotation.NonNull;
+
 /**
  * Compatibility interface for {@link android.net.CaptivePortalData}.
  */
@@ -50,7 +52,43 @@
     Uri getVenueInfoUrl();
 
     /**
+     * @see CaptivePortalData#getVenueFriendlyName()
+     */
+    CharSequence getVenueFriendlyName();
+
+    /**
+     * @see CaptivePortalData#getUserPortalUrlSource()
+     */
+    int getUserPortalUrlSource();
+
+    /**
      * @see INetworkMonitorCallbacks#notifyCaptivePortalDataChanged(android.net.CaptivePortalData)
      */
     void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException;
+
+    /**
+     * Generate a {@link CaptivePortalData} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @throws UnsupportedApiLevelException when used with API level lower than 31
+     * @return a {@link CaptivePortalData} object with a friendly name set
+     */
+    CaptivePortalDataShim withVenueFriendlyName(@NonNull String friendlyName)
+            throws UnsupportedApiLevelException;
+
+    /**
+     * Generate a {@link CaptivePortalData} object with a friendly name and Passpoint external URLs
+     * set
+     *
+     * @param friendlyName The friendly name to set
+     * @param venueInfoUrl Venue information URL
+     * @param termsAndConditionsUrl Terms and conditions URL
+     *
+     * @throws UnsupportedApiLevelException when used with API level lower than 31
+     * @return a {@link CaptivePortalData} object with friendly name, venue info URL and terms
+     * and conditions URL set
+     */
+    CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName,
+            @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl)
+            throws UnsupportedApiLevelException;
 }
diff --git a/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java b/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java
new file mode 100644
index 0000000..86d785e
--- /dev/null
+++ b/apishim/common/com/android/networkstack/apishim/common/ConnectivityManagerShim.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.common;
+
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collection;
+
+/**
+ * Interface used to access API methods in {@link android.net.ConnectivityManager}, with
+ * appropriate fallbacks if the methods are not yet part of the released API.
+ *
+ * <p>This interface makes it easier for callers to use ConnectivityManagerShimImpl, as it's more
+ * obvious what methods must be implemented on each API level, and it abstracts from callers the
+ * need to reference classes that have different implementations (which also does not work well
+ * with IDEs).
+ */
+public interface ConnectivityManagerShim {
+    /** See android.net.ConnectivityManager#requestBackgroundNetwork */
+    void requestBackgroundNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler)
+            throws UnsupportedApiLevelException;
+
+    /** See android.net.ConnectivityManager#registerSystemDefaultNetworkCallback */
+    void registerSystemDefaultNetworkCallback(
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler);
+
+    /** See android.net.ConnectivityManager#registerDefaultNetworkCallbackForUid */
+    default void registerDefaultNetworkCallbackForUid(
+            int uid, @NonNull NetworkCallback networkCallback, @NonNull Handler handler)
+            throws UnsupportedApiLevelException {
+        throw new UnsupportedApiLevelException("Only supported starting from API 31");
+    }
+
+    /** See android.net.ConnectivityManager#setLegacyLockdownVpnEnabled */
+    default void setLegacyLockdownVpnEnabled(boolean enabled) throws UnsupportedApiLevelException {
+        throw new UnsupportedApiLevelException("Only supported starting from API 31");
+    }
+
+    /** See android.net.ConnectivityManager#setRequireVpnForUids */
+    default void setRequireVpnForUids(boolean requireVpn, Collection<Range<Integer>> ranges)
+            throws UnsupportedApiLevelException {
+        throw new UnsupportedApiLevelException("Only supported starting from API 31");
+    }
+}
diff --git a/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java b/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
index ee19759..7fa1777 100644
--- a/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
@@ -76,4 +76,24 @@
      */
     void setDhcpServerAddress(@NonNull LinkProperties lp, @NonNull Inet4Address serverAddress);
 
+    /**
+     * Set captive portal data in {@link LinkProperties}
+     * @param lp Link properties object to be updated
+     * @param captivePortalData Captive portal data to be used
+     */
+    void setCaptivePortalData(@NonNull LinkProperties lp,
+            @Nullable CaptivePortalDataShim captivePortalData);
+
+    /**
+     * Get the name of the given capability that carriers use.
+     * If the capability does not have a carrier-name, returns null.
+     *
+     * @param capability The capability to get the carrier-name of.
+     * @return The carrier-name of the capability, or null if it doesn't exist.
+     * @hide
+     */
+    @Nullable
+    default String getCapabilityCarrierName(int capability) {
+        return null;
+    }
 }
diff --git a/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java b/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java
new file mode 100644
index 0000000..7d6d7bf
--- /dev/null
+++ b/apishim/common/com/android/networkstack/apishim/common/NetworkRequestShim.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.common;
+
+import android.net.NetworkRequest;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+
+/**
+ * Interface used to access API methods in {@link android.net.NetworkRequest}, with
+ * appropriate fallbacks if the methods are not yet part of the released API.
+ */
+public interface NetworkRequestShim {
+    /**
+     * See android.net.NetworkRequest.Builder#setUids.
+     * Set the {@code uids} into {@code builder}.
+     */
+    void setUids(@NonNull NetworkRequest.Builder builder,
+            @Nullable Set<Range<Integer>> uids) throws UnsupportedApiLevelException;
+
+    /**
+     * See android.net.NetworkRequest.Builder(NetworkRequest).
+     * @throws UnsupportedApiLevelException if API is not available in the API level.
+     */
+    default NetworkRequest.Builder newBuilder(@NonNull NetworkRequest request)
+            throws UnsupportedApiLevelException {
+        // Not supported before API 31.
+        throw new UnsupportedApiLevelException("Not supported before API 31.");
+    }
+}
diff --git a/apishim/common/com/android/networkstack/apishim/common/SettingsShim.java b/apishim/common/com/android/networkstack/apishim/common/SettingsShim.java
new file mode 100644
index 0000000..2453084
--- /dev/null
+++ b/apishim/common/com/android/networkstack/apishim/common/SettingsShim.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.apishim.common;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Interce for accessing API methods in {@link android.provider.Settings} by different API level.
+ */
+public interface SettingsShim {
+    /**
+     * @see android.provider.Settings#checkAndNoteWriteSettingsOperation(Context, int, String,
+     * String, boolean)
+     */
+    boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+            @NonNull String callingPackage, @Nullable String callingAttributionTag,
+            boolean throwException);
+}
diff --git a/apishim/common/com/android/networkstack/apishim/common/ShimUtils.java b/apishim/common/com/android/networkstack/apishim/common/ShimUtils.java
index d78cd0c..648751b 100644
--- a/apishim/common/com/android/networkstack/apishim/common/ShimUtils.java
+++ b/apishim/common/com/android/networkstack/apishim/common/ShimUtils.java
@@ -49,4 +49,11 @@
     public static boolean isAtLeastR() {
         return isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
     }
+
+    /**
+     * Check whether the device supports in-development or final S networking APIs.
+     */
+    public static boolean isAtLeastS() {
+        return isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R);
+    }
 }
diff --git a/apishim/jarjar-rules-compat.txt b/apishim/jarjar-rules-compat.txt
index dba2b49..4f34ccb 100644
--- a/apishim/jarjar-rules-compat.txt
+++ b/apishim/jarjar-rules-compat.txt
@@ -1,7 +1,7 @@
 # jarjar rules to use on API stable builds.
 # Use the latest stable apishim package as the main apishim package, to replace and avoid building
 # the unstable, non-compatibility shims.
-# Once API 31 is stable, apishim/31/com.android.networkstack.apishim should be moved to the
-# com.android.networkstack.apishim.api31 package, a new apishim/32/com.android.networkstack.apishim
-# package should be created, and this rule should reference api31.
-rule com.android.networkstack.apishim.api30.** com.android.networkstack.apishim.@1
\ No newline at end of file
+# Once API 32 is stable, apishim/32/com.android.networkstack.apishim should be moved to the
+# com.android.networkstack.apishim.api32 package, a new apishim/33/com.android.networkstack.apishim
+# package should be created, and this rule should reference api32.
+rule com.android.networkstack.apishim.api31.** com.android.networkstack.apishim.@1
\ No newline at end of file
diff --git a/common/captiveportal/Android.bp b/common/captiveportal/Android.bp
index d34ab85..876e733 100644
--- a/common/captiveportal/Android.bp
+++ b/common/captiveportal/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_library {
     name: "captiveportal-lib",
     srcs: ["src/**/*.java"],
@@ -21,4 +25,6 @@
         "androidx.annotation_annotation",
     ],
     sdk_version: "system_current",
-}
\ No newline at end of file
+    // this is part of updatable modules(NetworkStack) which targets 29(Q)
+    min_sdk_version: "29",
+}
diff --git a/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java b/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java
index 2ba1dcc..8b388ad 100755
--- a/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java
+++ b/common/captiveportal/src/android/net/captiveportal/CaptivePortalProbeResult.java
@@ -103,11 +103,22 @@
     }
 
     public boolean isSuccessful() {
-        return mHttpResponseCode == SUCCESS_CODE;
+        return isSuccessCode(mHttpResponseCode);
     }
 
     public boolean isPortal() {
-        return !isSuccessful() && (mHttpResponseCode >= 200) && (mHttpResponseCode <= 399);
+        return isPortalCode(mHttpResponseCode);
+    }
+
+    private static boolean isSuccessCode(int responseCode) {
+        return responseCode == SUCCESS_CODE;
+    }
+
+    /**
+     * @return Whether the specified HTTP return code indicates a captive portal.
+     */
+    public static boolean isPortalCode(int responseCode) {
+        return !isSuccessCode(responseCode) && (responseCode >= 200) && (responseCode <= 399);
     }
 
     public boolean isFailed() {
diff --git a/common/moduleutils/Android.bp b/common/moduleutils/Android.bp
index 9dfec42..2230549 100644
--- a/common/moduleutils/Android.bp
+++ b/common/moduleutils/Android.bp
@@ -17,21 +17,18 @@
 // Shared utility sources to be used by multiple network modules
 // TODO: remove all frameworks/base dependencies on packages/modules/NetworkStack and
 // frameworks/base/packages/Tethering by moving these files to frameworks/libs/net.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: remove this filegroup together with services.net
 filegroup {
     name: "net-module-utils-srcs",
     srcs: [
-        "src/android/net/util/SharedLog.java",
-        "src/android/net/shared/InitialConfiguration.java",
-        "src/android/net/shared/Layer2Information.java",
-        "src/android/net/shared/LinkPropertiesParcelableUtil.java",
-        "src/android/net/shared/ParcelableUtil.java",
         "src/android/net/shared/NetdUtils.java",
-        "src/android/net/shared/NetworkMonitorUtils.java",
-        "src/android/net/shared/ParcelableUtil.java",
-        "src/android/net/shared/PrivateDnsConfig.java",
-        "src/android/net/shared/ProvisioningConfiguration.java",
         "src/android/net/shared/RouteUtils.java",
         "src/android/net/util/InterfaceParams.java",
+        "src/android/net/util/SharedLog.java",
     ],
     visibility: [
         "//frameworks/base/services/net",
@@ -39,6 +36,19 @@
 }
 
 filegroup {
+    name: "connectivity-module-utils-srcs",
+    srcs: [
+        "src/android/net/util/SharedLog.java",
+        "src/android/net/shared/NetdUtils.java",
+        "src/android/net/shared/NetworkMonitorUtils.java",
+        "src/android/net/shared/RouteUtils.java",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity/service",
+    ]
+}
+
+filegroup {
     name: "networkstack-module-utils-srcs",
     srcs: ["src/**/*.java"],
     visibility: [
@@ -50,28 +60,18 @@
 filegroup {
     name: "tethering-module-utils-srcs",
     srcs: [
+        "src/android/net/ip/ConntrackMonitor.java",
         "src/android/net/ip/InterfaceController.java",
         "src/android/net/ip/IpNeighborMonitor.java",
         "src/android/net/ip/NetlinkMonitor.java",
         "src/android/net/netlink/*.java",
         "src/android/net/shared/NetdUtils.java",
         "src/android/net/shared/RouteUtils.java",
-        "src/android/net/util/FdEventsReader.java",
         "src/android/net/util/InterfaceParams.java",
-        "src/android/net/util/PacketReader.java",
-        "src/android/net/util/SharedLog.java"
+        "src/android/net/util/SharedLog.java",
     ],
-    visibility: ["//frameworks/base/packages/Tethering"],
-}
-
-// Utility sources used by test libraries.
-// This is its own group to limit indiscriminate dependency of test code on production code.
-// TODO: move these classes and NetworkStack/tests/lib to frameworks/libs/net, and remove this.
-filegroup {
-    name: "net-module-utils-srcs-for-tests",
-    visibility: ["//packages/modules/NetworkStack/tests/lib"],
-    srcs: [
-        "src/android/net/util/FdEventsReader.java",
-        "src/android/net/util/PacketReader.java",
+    visibility: [
+        "//frameworks/base/packages/Tethering",
+        "//packages/modules/Connectivity/Tethering"
     ],
 }
diff --git a/common/moduleutils/src/android/net/ip/ConntrackMonitor.java b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
new file mode 100644
index 0000000..6c72984
--- /dev/null
+++ b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
@@ -0,0 +1,204 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.netlink.ConntrackMessage.DYING_MASK;
+import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
+import android.net.netlink.ConntrackMessage;
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkMessage;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+
+/**
+ * ConntrackMonitor.
+ *
+ * Monitors the netfilter conntrack notifications and presents to callers
+ * ConntrackEvents describing each event.
+ *
+ * @hide
+ */
+public class ConntrackMonitor extends NetlinkMonitor {
+    private static final String TAG = ConntrackMonitor.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+    public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+    public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+    public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+
+    // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+    // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+    // if too many connections are disconnected by losing network and so on. Use a large-enough
+    // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+    private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
+    /**
+     * A class for describing parsed netfilter conntrack events.
+     */
+    public static class ConntrackEvent {
+        /**
+         * Conntrack event type.
+         */
+        public final short msgType;
+        /**
+         * Original direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleOrig;
+        /**
+         * Reply direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleReply;
+        /**
+         * Connection status. A bitmask of ip_conntrack_status enum flags.
+         */
+        public final int status;
+        /**
+         * Conntrack timeout.
+         */
+        public final int timeoutSec;
+
+        public ConntrackEvent(ConntrackMessage msg) {
+            this.msgType = msg.getHeader().nlmsg_type;
+            this.tupleOrig = msg.tupleOrig;
+            this.tupleReply = msg.tupleReply;
+            this.status = msg.status;
+            this.timeoutSec = msg.timeoutSec;
+        }
+
+        @VisibleForTesting
+        public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+                ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+            this.msgType = msgType;
+            this.tupleOrig = tupleOrig;
+            this.tupleReply = tupleReply;
+            this.status = status;
+            this.timeoutSec = timeoutSec;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof ConntrackEvent)) return false;
+            ConntrackEvent that = (ConntrackEvent) o;
+            return this.msgType == that.msgType
+                    && Objects.equals(this.tupleOrig, that.tupleOrig)
+                    && Objects.equals(this.tupleReply, that.tupleReply)
+                    && this.status == that.status
+                    && this.timeoutSec == that.timeoutSec;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+        }
+
+        @Override
+        public String toString() {
+            return "ConntrackEvent{"
+                    + "msg_type{"
+                    + NetlinkConstants.stringForNlMsgType(msgType, OsConstants.NETLINK_NETFILTER)
+                    + "}, "
+                    + "tuple_orig{" + tupleOrig + "}, "
+                    + "tuple_reply{" + tupleReply + "}, "
+                    + "status{"
+                    + status + "(" + ConntrackMessage.stringForIpConntrackStatus(status) + ")"
+                    + "}, "
+                    + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+                    + "}";
+        }
+
+        /**
+         * Check the established NAT session conntrack message.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if an established NAT message, false if not.
+         */
+        public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec == 0) return false;
+            if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+            return true;
+        }
+
+        /**
+         * Check the dying NAT session conntrack message.
+         * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if a dying NAT message, false if not.
+         */
+        public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec != 0) return false;
+            if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+            return true;
+        }
+    }
+
+    /**
+     * A callback to caller for conntrack event.
+     */
+    public interface ConntrackEventConsumer {
+        /**
+         * Every conntrack event received on the netlink socket is passed in
+         * here.
+         */
+        void accept(@NonNull ConntrackEvent event);
+    }
+
+    private final ConntrackEventConsumer mConsumer;
+
+    public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+            @NonNull ConntrackEventConsumer cb) {
+        super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
+                | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
+        mConsumer = cb;
+    }
+
+    @Override
+    public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof ConntrackMessage)) {
+            mLog.e("non-conntrack msg: " + nlMsg);
+            return;
+        }
+
+        final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+        if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+                || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+            return;
+        }
+
+        mConsumer.accept(new ConntrackEvent(conntrackMsg));
+    }
+}
diff --git a/common/moduleutils/src/android/net/ip/IpNeighborMonitor.java b/common/moduleutils/src/android/net/ip/IpNeighborMonitor.java
index 18a9f90..b45c061 100644
--- a/common/moduleutils/src/android/net/ip/IpNeighborMonitor.java
+++ b/common/moduleutils/src/android/net/ip/IpNeighborMonitor.java
@@ -105,7 +105,7 @@
         public String toString() {
             final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
             return j.add("@" + elapsedMs)
-                    .add(stringForNlMsgType(msgType))
+                    .add(stringForNlMsgType(msgType, NETLINK_ROUTE))
                     .add("if=" + ifindex)
                     .add(ip.getHostAddress())
                     .add(StructNdMsg.stringForNudState(nudState))
diff --git a/common/moduleutils/src/android/net/ip/NetlinkMonitor.java b/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
index 7c86beb..2025967 100644
--- a/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
+++ b/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
@@ -21,12 +21,13 @@
 import static android.system.OsConstants.AF_NETLINK;
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
 
 import android.annotation.NonNull;
 import android.net.netlink.NetlinkErrorMessage;
 import android.net.netlink.NetlinkMessage;
 import android.net.netlink.NetlinkSocket;
-import android.net.util.PacketReader;
 import android.net.util.SharedLog;
 import android.net.util.SocketUtils;
 import android.os.Handler;
@@ -35,6 +36,8 @@
 import android.system.Os;
 import android.util.Log;
 
+import com.android.net.module.util.PacketReader;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.SocketAddress;
@@ -55,9 +58,13 @@
     protected final String mTag;
     private final int mFamily;
     private final int mBindGroups;
+    private final int mSockRcvbufSize;
 
     private static final boolean DBG = false;
 
+    // Default socket receive buffer size. This means the specific buffer size is not set.
+    private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
     /**
      * Constructs a new {@code NetlinkMonitor} instance.
      *
@@ -67,14 +74,23 @@
      * @param tag The log tag to use for log messages.
      * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
      * @param bindGroups the netlink groups to bind to.
+     * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+     *        set the specific socket receive buffer size in #createFd and use the default value in
+     *        /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
      */
     public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
-            int family, int bindGroups) {
+            int family, int bindGroups, int sockRcvbufSize) {
         super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
         mLog = log.forSubComponent(tag);
         mTag = tag;
         mFamily = family;
         mBindGroups = bindGroups;
+        mSockRcvbufSize = sockRcvbufSize;
+    }
+
+    public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+            int family, int bindGroups) {
+        this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
     }
 
     @Override
@@ -83,6 +99,9 @@
 
         try {
             fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+            if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+                Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+            }
             Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
             NetlinkSocket.connectToKernel(fd);
 
@@ -108,7 +127,7 @@
         while (byteBuffer.remaining() > 0) {
             try {
                 final int position = byteBuffer.position();
-                final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
+                final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer, mFamily);
                 if (nlMsg == null || nlMsg.getHeader() == null) {
                     byteBuffer.position(position);
                     mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
diff --git a/common/moduleutils/src/android/net/shared/NetworkMonitorUtils.java b/common/moduleutils/src/android/net/shared/NetworkMonitorUtils.java
index 981a576..0cd9f65 100644
--- a/common/moduleutils/src/android/net/shared/NetworkMonitorUtils.java
+++ b/common/moduleutils/src/android/net/shared/NetworkMonitorUtils.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -27,6 +28,8 @@
 
 import android.net.NetworkCapabilities;
 
+import com.android.modules.utils.build.SdkLevel;
+
 /** @hide */
 public class NetworkMonitorUtils {
     // This class is used by both NetworkMonitor and ConnectivityService, so it cannot use
@@ -36,6 +39,14 @@
     // TODO: use NetworkCapabilities.TRANSPORT_TEST once NetworkStack builds against API 31.
     private static final int TRANSPORT_TEST = 7;
 
+    // This class is used by both NetworkMonitor and ConnectivityService, so it cannot use
+    // NetworkStack shims, but at the same time cannot use non-system APIs.
+    // NET_CAPABILITY_NOT_VCN_MANAGED is system API as of S (so it is enforced to always be 28 and
+    // can't be changed).
+    // TODO: use NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED once NetworkStack builds against
+    //       API 31.
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+
     // Network conditions broadcast constants
     public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
             "android.net.conn.NETWORK_CONDITIONS_MEASURED";
@@ -59,11 +70,16 @@
     public static boolean isPrivateDnsValidationRequired(NetworkCapabilities nc) {
         if (nc == null) return false;
 
+        final boolean isVcnManaged = SdkLevel.isAtLeastS()
+                && !nc.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final boolean isOemPaid = nc.hasCapability(NET_CAPABILITY_OEM_PAID)
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+        final boolean isDefaultCapable = nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+
         // TODO: Consider requiring validation for DUN networks.
         if (nc.hasCapability(NET_CAPABILITY_INTERNET)
-                && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                && nc.hasCapability(NET_CAPABILITY_TRUSTED)) {
-            // Real networks
+                && (isVcnManaged || isOemPaid || isDefaultCapable)) {
             return true;
         }
 
diff --git a/common/moduleutils/src/android/net/util/FdEventsReader.java b/common/moduleutils/src/android/net/util/FdEventsReader.java
deleted file mode 100644
index 9045c01..0000000
--- a/common/moduleutils/src/android/net/util/FdEventsReader.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.util;
-
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.MessageQueue;
-import android.system.ErrnoException;
-import android.system.OsConstants;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-
-
-/**
- * This class encapsulates the mechanics of registering a file descriptor
- * with a thread's Looper and handling read events (and errors).
- *
- * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override
- * onStop() and onStart().
- *
- * Subclasses can expect a call life-cycle like the following:
- *
- *     [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all
- *         goes well. Implementations may override onStart() for additional initialization.
- *
- *     [2] yield, waiting for read event or error notification:
- *
- *             [a] readPacket() && handlePacket()
- *
- *             [b] if (no error):
- *                     goto 2
- *                 else:
- *                     goto 3
- *
- *     [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never
- *         started). Implementations may override onStop() for additional cleanup.
- *
- * The packet receive buffer is recycled on every read call, so subclasses
- * should make any copies they would like inside their handlePacket()
- * implementation.
- *
- * All public methods MUST only be called from the same thread with which
- * the Handler constructor argument is associated.
- *
- * @param <BufferType> the type of the buffer used to read data.
- */
-public abstract class FdEventsReader<BufferType> {
-    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
-    private static final int UNREGISTER_THIS_FD = 0;
-
-    @NonNull
-    private final Handler mHandler;
-    @NonNull
-    private final MessageQueue mQueue;
-    @NonNull
-    private final BufferType mBuffer;
-    @Nullable
-    private FileDescriptor mFd;
-    private long mPacketsReceived;
-
-    protected static void closeFd(FileDescriptor fd) {
-        try {
-            SocketUtils.closeSocket(fd);
-        } catch (IOException ignored) {
-        }
-    }
-
-    protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) {
-        mHandler = h;
-        mQueue = mHandler.getLooper().getQueue();
-        mBuffer = buffer;
-    }
-
-    @VisibleForTesting
-    @NonNull
-    protected MessageQueue getMessageQueue() {
-        return mQueue;
-    }
-
-    /** Start this FdEventsReader. */
-    public boolean start() {
-        if (!onCorrectThread()) {
-            throw new IllegalStateException("start() called from off-thread");
-        }
-
-        return createAndRegisterFd();
-    }
-
-    /** Stop this FdEventsReader and destroy the file descriptor. */
-    public void stop() {
-        if (!onCorrectThread()) {
-            throw new IllegalStateException("stop() called from off-thread");
-        }
-
-        unregisterAndDestroyFd();
-    }
-
-    @NonNull
-    public Handler getHandler() {
-        return mHandler;
-    }
-
-    protected abstract int recvBufSize(@NonNull BufferType buffer);
-
-    /** Returns the size of the receive buffer. */
-    public int recvBufSize() {
-        return recvBufSize(mBuffer);
-    }
-
-    /**
-     * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
-     *
-     * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
-     */
-    public final long numPacketsReceived() {
-        return mPacketsReceived;
-    }
-
-    /**
-     * Subclasses MUST create the listening socket here, including setting all desired socket
-     * options, interface or address/port binding, etc. The socket MUST be created nonblocking.
-     */
-    @Nullable
-    protected abstract FileDescriptor createFd();
-
-    /**
-     * Implementations MUST return the bytes read or throw an Exception.
-     *
-     * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or
-     * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer
-     * contents and respectively wait for further input or retry the read immediately. For all other
-     * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this
-     * method.
-     */
-    protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer)
-            throws Exception;
-
-    /**
-     * Called by the main loop for every packet.  Any desired copies of
-     * |recvbuf| should be made in here, as the underlying byte array is
-     * reused across all reads.
-     */
-    protected void handlePacket(@NonNull BufferType recvbuf, int length) {}
-
-    /**
-     * Called by the main loop to log errors.  In some cases |e| may be null.
-     */
-    protected void logError(@NonNull String msg, @Nullable Exception e) {}
-
-    /**
-     * Called by start(), if successful, just prior to returning.
-     */
-    protected void onStart() {}
-
-    /**
-     * Called by stop() just prior to returning.
-     */
-    protected void onStop() {}
-
-    private boolean createAndRegisterFd() {
-        if (mFd != null) return true;
-
-        try {
-            mFd = createFd();
-        } catch (Exception e) {
-            logError("Failed to create socket: ", e);
-            closeFd(mFd);
-            mFd = null;
-        }
-
-        if (mFd == null) return false;
-
-        getMessageQueue().addOnFileDescriptorEventListener(
-                mFd,
-                FD_EVENTS,
-                (fd, events) -> {
-                    // Always call handleInput() so read/recvfrom are given
-                    // a proper chance to encounter a meaningful errno and
-                    // perhaps log a useful error message.
-                    if (!isRunning() || !handleInput()) {
-                        unregisterAndDestroyFd();
-                        return UNREGISTER_THIS_FD;
-                    }
-                    return FD_EVENTS;
-                });
-        onStart();
-        return true;
-    }
-
-    private boolean isRunning() {
-        return (mFd != null) && mFd.valid();
-    }
-
-    // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
-    private boolean handleInput() {
-        while (isRunning()) {
-            final int bytesRead;
-
-            try {
-                bytesRead = readPacket(mFd, mBuffer);
-                if (bytesRead < 1) {
-                    if (isRunning()) logError("Socket closed, exiting", null);
-                    break;
-                }
-                mPacketsReceived++;
-            } catch (ErrnoException e) {
-                if (e.errno == OsConstants.EAGAIN) {
-                    // We've read everything there is to read this time around.
-                    return true;
-                } else if (e.errno == OsConstants.EINTR) {
-                    continue;
-                } else {
-                    if (isRunning()) logError("readPacket error: ", e);
-                    break;
-                }
-            } catch (Exception e) {
-                if (isRunning()) logError("readPacket error: ", e);
-                break;
-            }
-
-            try {
-                handlePacket(mBuffer, bytesRead);
-            } catch (Exception e) {
-                logError("handlePacket error: ", e);
-                Log.wtf(FdEventsReader.class.getSimpleName(), "Error handling packet", e);
-            }
-        }
-
-        return false;
-    }
-
-    private void unregisterAndDestroyFd() {
-        if (mFd == null) return;
-
-        getMessageQueue().removeOnFileDescriptorEventListener(mFd);
-        closeFd(mFd);
-        mFd = null;
-        onStop();
-    }
-
-    private boolean onCorrectThread() {
-        return (mHandler.getLooper() == Looper.myLooper());
-    }
-}
diff --git a/common/moduleutils/src/android/net/util/PacketReader.java b/common/moduleutils/src/android/net/util/PacketReader.java
deleted file mode 100644
index 0be7187..0000000
--- a/common/moduleutils/src/android/net/util/PacketReader.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.util;
-
-import static java.lang.Math.max;
-
-import android.os.Handler;
-import android.system.Os;
-
-import java.io.FileDescriptor;
-
-/**
- * Specialization of {@link FdEventsReader} that reads packets into a byte array.
- *
- * TODO: rename this class to something more correctly descriptive (something
- * like [or less horrible than] FdReadEventsHandler?).
- */
-public abstract class PacketReader extends FdEventsReader<byte[]> {
-
-    public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
-
-    protected PacketReader(Handler h) {
-        this(h, DEFAULT_RECV_BUF_SIZE);
-    }
-
-    protected PacketReader(Handler h, int recvBufSize) {
-        super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]);
-    }
-
-    @Override
-    protected final int recvBufSize(byte[] buffer) {
-        return buffer.length;
-    }
-
-    /**
-     * Subclasses MAY override this to change the default read() implementation
-     * in favour of, say, recvfrom().
-     *
-     * Implementations MUST return the bytes read or throw an Exception.
-     */
-    @Override
-    protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
-        return Os.read(fd, packetBuffer, 0, packetBuffer.length);
-    }
-}
diff --git a/common/netlinkclient/Android.bp b/common/netlinkclient/Android.bp
index a61cdbd..9a60e57 100644
--- a/common/netlinkclient/Android.bp
+++ b/common/netlinkclient/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_library {
     name: "netlink-client",
     srcs: [
@@ -24,4 +28,6 @@
         "androidx.annotation_annotation",
     ],
     sdk_version: "system_current",
+    // this is part of updatable modules(NetworkStack) which targets 29(Q)
+    min_sdk_version: "29",
 }
diff --git a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
index 6978739..cc8bb7e 100644
--- a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
@@ -16,17 +16,27 @@
 
 package android.net.netlink;
 
+import static android.net.netlink.StructNlAttr.findNextAttrOfType;
+import static android.net.netlink.StructNlAttr.makeNestedType;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_ACK;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REPLACE;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static java.nio.ByteOrder.BIG_ENDIAN;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.system.OsConstants;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Objects;
 
 
 /**
@@ -39,12 +49,10 @@
 public class ConntrackMessage extends NetlinkMessage {
     public static final int STRUCT_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
 
-    public static final short NFNL_SUBSYS_CTNETLINK = 1;
-    public static final short IPCTNL_MSG_CT_NEW = 0;
-
     // enum ctattr_type
     public static final short CTA_TUPLE_ORIG  = 1;
     public static final short CTA_TUPLE_REPLY = 2;
+    public static final short CTA_STATUS      = 3;
     public static final short CTA_TIMEOUT     = 7;
 
     // enum ctattr_tuple
@@ -60,6 +68,119 @@
     public static final short CTA_PROTO_SRC_PORT = 2;
     public static final short CTA_PROTO_DST_PORT = 3;
 
+    // enum ip_conntrack_status
+    public static final int IPS_EXPECTED      = 0x00000001;
+    public static final int IPS_SEEN_REPLY    = 0x00000002;
+    public static final int IPS_ASSURED       = 0x00000004;
+    public static final int IPS_CONFIRMED     = 0x00000008;
+    public static final int IPS_SRC_NAT       = 0x00000010;
+    public static final int IPS_DST_NAT       = 0x00000020;
+    public static final int IPS_SEQ_ADJUST    = 0x00000040;
+    public static final int IPS_SRC_NAT_DONE  = 0x00000080;
+    public static final int IPS_DST_NAT_DONE  = 0x00000100;
+    public static final int IPS_DYING         = 0x00000200;
+    public static final int IPS_FIXED_TIMEOUT = 0x00000400;
+    public static final int IPS_TEMPLATE      = 0x00000800;
+    public static final int IPS_UNTRACKED     = 0x00001000;
+    public static final int IPS_HELPER        = 0x00002000;
+    public static final int IPS_OFFLOAD       = 0x00004000;
+    public static final int IPS_HW_OFFLOAD    = 0x00008000;
+
+    // ip_conntrack_status mask
+    // Interesting on the NAT conntrack session which has already seen two direction traffic.
+    // TODO: Probably IPS_{SRC, DST}_NAT_DONE are also interesting.
+    public static final int ESTABLISHED_MASK = IPS_CONFIRMED | IPS_ASSURED | IPS_SEEN_REPLY
+            | IPS_SRC_NAT;
+    // Interesting on the established NAT conntrack session which is dying.
+    public static final int DYING_MASK = ESTABLISHED_MASK | IPS_DYING;
+
+    /**
+     * A tuple for the conntrack connection information.
+     *
+     * see also CTA_TUPLE_ORIG and CTA_TUPLE_REPLY.
+     */
+    public static class Tuple {
+        public final Inet4Address srcIp;
+        public final Inet4Address dstIp;
+
+        // Both port and protocol number are unsigned numbers stored in signed integers, and that
+        // callers that want to compare them to integers should either cast those integers, or
+        // convert them to unsigned using Byte.toUnsignedInt() and Short.toUnsignedInt().
+        public final short srcPort;
+        public final short dstPort;
+        public final byte protoNum;
+
+        public Tuple(TupleIpv4 ip, TupleProto proto) {
+            this.srcIp = ip.src;
+            this.dstIp = ip.dst;
+            this.srcPort = proto.srcPort;
+            this.dstPort = proto.dstPort;
+            this.protoNum = proto.protoNum;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof Tuple)) return false;
+            Tuple that = (Tuple) o;
+            return Objects.equals(this.srcIp, that.srcIp)
+                    && Objects.equals(this.dstIp, that.dstIp)
+                    && this.srcPort == that.srcPort
+                    && this.dstPort == that.dstPort
+                    && this.protoNum == that.protoNum;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(srcIp, dstIp, srcPort, dstPort, protoNum);
+        }
+
+        @Override
+        public String toString() {
+            final String srcIpStr = (srcIp == null) ? "null" : srcIp.getHostAddress();
+            final String dstIpStr = (dstIp == null) ? "null" : dstIp.getHostAddress();
+            final String protoStr = NetlinkConstants.stringForProtocol(protoNum);
+
+            return "Tuple{"
+                    + protoStr + ": "
+                    + srcIpStr + ":" + Short.toUnsignedInt(srcPort) + " -> "
+                    + dstIpStr + ":" + Short.toUnsignedInt(dstPort)
+                    + "}";
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection address.
+     *
+     * see also CTA_TUPLE_IP.
+     */
+    public static class TupleIpv4 {
+        public final Inet4Address src;
+        public final Inet4Address dst;
+
+        public TupleIpv4(Inet4Address src, Inet4Address dst) {
+            this.src = src;
+            this.dst = dst;
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection protocol.
+     *
+     * see also CTA_TUPLE_PROTO.
+     */
+    public static class TupleProto {
+        public final byte protoNum;
+        public final short srcPort;
+        public final short dstPort;
+
+        public TupleProto(byte protoNum, short srcPort, short dstPort) {
+            this.protoNum = protoNum;
+            this.srcPort = srcPort;
+            this.dstPort = dstPort;
+        }
+    }
+
     public static byte[] newIPv4TimeoutUpdateRequest(
             int proto, Inet4Address src, int sport, Inet4Address dst, int dport, int timeoutSec) {
         // *** STYLE WARNING ***
@@ -84,7 +205,8 @@
 
         final ConntrackMessage ctmsg = new ConntrackMessage();
         ctmsg.mHeader.nlmsg_len = bytes.length;
-        ctmsg.mHeader.nlmsg_type = (NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW;
+        ctmsg.mHeader.nlmsg_type = (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8)
+                | NetlinkConstants.IPCTNL_MSG_CT_NEW;
         ctmsg.mHeader.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE;
         ctmsg.mHeader.nlmsg_seq = 1;
         ctmsg.pack(byteBuffer);
@@ -95,15 +217,336 @@
         return bytes;
     }
 
-    protected StructNfGenMsg mNfGenMsg;
+    /**
+     * Parses a netfilter conntrack message from a {@link ByteBuffer}.
+     *
+     * @param header the netlink message header.
+     * @param byteBuffer The buffer from which to parse the netfilter conntrack message.
+     * @return the parsed netfilter conntrack message, or {@code null} if the netfilter conntrack
+     *         message could not be parsed successfully (for example, if it was truncated).
+     */
+    public static ConntrackMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
+        // Just build the netlink header and netfilter header for now and pretend the whole message
+        // was consumed.
+        // TODO: Parse the conntrack attributes.
+        final StructNfGenMsg nfGenMsg = StructNfGenMsg.parse(byteBuffer);
+        if (nfGenMsg == null) {
+            return null;
+        }
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_STATUS, byteBuffer);
+        int status = 0;
+        if (nlAttr != null) {
+            status = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(CTA_TIMEOUT, byteBuffer);
+        int timeoutSec = 0;
+        if (nlAttr != null) {
+            timeoutSec = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_ORIG), byteBuffer);
+        Tuple tupleOrig = null;
+        if (nlAttr != null) {
+            tupleOrig = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_REPLY), byteBuffer);
+        Tuple tupleReply = null;
+        if (nlAttr != null) {
+            tupleReply = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
+        // Advance to the end of the message.
+        byteBuffer.position(baseOffset);
+        final int kMinConsumed = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+        final int kAdditionalSpace = NetlinkConstants.alignedLengthOf(
+                header.nlmsg_len - kMinConsumed);
+        if (byteBuffer.remaining() < kAdditionalSpace) {
+            return null;
+        }
+        byteBuffer.position(baseOffset + kAdditionalSpace);
+
+        return new ConntrackMessage(header, nfGenMsg, tupleOrig, tupleReply, status, timeoutSec);
+    }
+
+    /**
+     * Parses a conntrack tuple from a {@link ByteBuffer}.
+     *
+     * The attribute parsing is interesting on:
+     * - CTA_TUPLE_IP
+     *     CTA_IP_V4_SRC
+     *     CTA_IP_V4_DST
+     * - CTA_TUPLE_PROTO
+     *     CTA_PROTO_NUM
+     *     CTA_PROTO_SRC_PORT
+     *     CTA_PROTO_DST_PORT
+     *
+     * Assume that the minimum size is the sum of CTA_TUPLE_IP (size: 20) and CTA_TUPLE_PROTO
+     * (size: 28). Here is an example for an expected CTA_TUPLE_ORIG message in raw data:
+     * +--------------------------------------------------------------------------------------+
+     * | CTA_TUPLE_ORIG                                                                       |
+     * +--------------------------+-----------------------------------------------------------+
+     * | 1400                     | nla_len = 20                                              |
+     * | 0180                     | nla_type = nested CTA_TUPLE_IP                            |
+     * |     0800 0100 C0A8500C   |     nla_type=CTA_IP_V4_SRC, ip=192.168.80.12              |
+     * |     0800 0200 8C700874   |     nla_type=CTA_IP_V4_DST, ip=140.112.8.116              |
+     * | 1C00                     | nla_len = 28                                              |
+     * | 0280                     | nla_type = nested CTA_TUPLE_PROTO                         |
+     * |     0500 0100 06 000000  |     nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)         |
+     * |     0600 0200 F3F1 0000  |     nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)  |
+     * |     0600 0300 01BB 0000  |     nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)    |
+     * +--------------------------+-----------------------------------------------------------+
+     *
+     * The position of the byte buffer doesn't set to the end when the function returns. It is okay
+     * because the caller ConntrackMessage#parse has passed a copy which is used for this parser
+     * only. Moreover, the parser behavior is the same as other existing netlink struct class
+     * parser. Ex: StructInetDiagMsg#parse.
+     */
+    @Nullable
+    private static Tuple parseTuple(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        TupleIpv4 tupleIpv4 = null;
+        TupleProto tupleProto = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_IP), byteBuffer);
+        if (nlAttr != null) {
+            tupleIpv4 = parseTupleIpv4(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleIpv4 == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_PROTO), byteBuffer);
+        if (nlAttr != null) {
+            tupleProto = parseTupleProto(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleProto == null) return null;
+
+        return new Tuple(tupleIpv4, tupleProto);
+    }
+
+    @Nullable
+    private static Inet4Address castToInet4Address(@Nullable InetAddress address) {
+        if (address == null || !(address instanceof Inet4Address)) return null;
+        return (Inet4Address) address;
+    }
+
+    @Nullable
+    private static TupleIpv4 parseTupleIpv4(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        Inet4Address src = null;
+        Inet4Address dst = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_IP_V4_SRC, byteBuffer);
+        if (nlAttr != null) {
+            src = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (src == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(CTA_IP_V4_DST, byteBuffer);
+        if (nlAttr != null) {
+            dst = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (dst == null) return null;
+
+        return new TupleIpv4(src, dst);
+    }
+
+    @Nullable
+    private static TupleProto parseTupleProto(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        byte protoNum = 0;
+        short srcPort = 0;
+        short dstPort = 0;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_PROTO_NUM, byteBuffer);
+        if (nlAttr != null) {
+            protoNum = nlAttr.getValueAsByte((byte) 0);
+        }
+        if (!(protoNum == IPPROTO_TCP || protoNum == IPPROTO_UDP)) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_SRC_PORT, byteBuffer);
+        if (nlAttr != null) {
+            srcPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (srcPort == 0) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_DST_PORT, byteBuffer);
+        if (nlAttr != null) {
+            dstPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (dstPort == 0) return null;
+
+        return new TupleProto(protoNum, srcPort, dstPort);
+    }
+
+    /**
+     * Netfilter header.
+     */
+    public final StructNfGenMsg nfGenMsg;
+    /**
+     * Original direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_ORIG, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleOrig;
+    /**
+     * Reply direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_REPLY, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleReply;
+    /**
+     * Connection status. A bitmask of ip_conntrack_status enum flags.
+     *
+     * The status is determined by the parsed attribute value CTA_STATUS, or 0 if the status could
+     * not be parsed successfully (for example, if it was truncated or absent). For the message
+     * from kernel, the valid status is non-zero. For the message from user space, the status may
+     * be 0 (absent).
+     */
+    public final int status;
+    /**
+     * Conntrack timeout.
+     *
+     * The timeout is determined by the parsed attribute value CTA_TIMEOUT, or 0 if the timeout
+     * could not be parsed successfully (for example, if it was truncated or absent). For
+     * IPCTNL_MSG_CT_NEW event, the valid timeout is non-zero. For IPCTNL_MSG_CT_DELETE event, the
+     * timeout is 0 (absent).
+     */
+    public final int timeoutSec;
 
     private ConntrackMessage() {
         super(new StructNlMsgHdr());
-        mNfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+        nfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+
+        // This constructor is only used by #newIPv4TimeoutUpdateRequest which doesn't use these
+        // data member for packing message. Simply fill them to null or 0.
+        tupleOrig = null;
+        tupleReply = null;
+        status = 0;
+        timeoutSec = 0;
+    }
+
+    private ConntrackMessage(@NonNull StructNlMsgHdr header, @NonNull StructNfGenMsg nfGenMsg,
+            @Nullable Tuple tupleOrig, @Nullable Tuple tupleReply, int status, int timeoutSec) {
+        super(header);
+        this.nfGenMsg = nfGenMsg;
+        this.tupleOrig = tupleOrig;
+        this.tupleReply = tupleReply;
+        this.status = status;
+        this.timeoutSec = timeoutSec;
     }
 
     public void pack(ByteBuffer byteBuffer) {
         mHeader.pack(byteBuffer);
-        mNfGenMsg.pack(byteBuffer);
+        nfGenMsg.pack(byteBuffer);
+    }
+
+    public short getMessageType() {
+        return (short) (getHeader().nlmsg_type & ~(NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8));
+    }
+
+    /**
+     * Convert an ip conntrack status to a string.
+     */
+    public static String stringForIpConntrackStatus(int flags) {
+        final StringBuilder sb = new StringBuilder();
+
+        if ((flags & IPS_EXPECTED) != 0) {
+            sb.append("IPS_EXPECTED");
+        }
+        if ((flags & IPS_SEEN_REPLY) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SEEN_REPLY");
+        }
+        if ((flags & IPS_ASSURED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_ASSURED");
+        }
+        if ((flags & IPS_CONFIRMED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_CONFIRMED");
+        }
+        if ((flags & IPS_SRC_NAT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SRC_NAT");
+        }
+        if ((flags & IPS_DST_NAT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DST_NAT");
+        }
+        if ((flags & IPS_SEQ_ADJUST) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SEQ_ADJUST");
+        }
+        if ((flags & IPS_SRC_NAT_DONE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_SRC_NAT_DONE");
+        }
+        if ((flags & IPS_DST_NAT_DONE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DST_NAT_DONE");
+        }
+        if ((flags & IPS_DYING) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_DYING");
+        }
+        if ((flags & IPS_FIXED_TIMEOUT) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_FIXED_TIMEOUT");
+        }
+        if ((flags & IPS_TEMPLATE) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_TEMPLATE");
+        }
+        if ((flags & IPS_UNTRACKED) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_UNTRACKED");
+        }
+        if ((flags & IPS_HELPER) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_HELPER");
+        }
+        if ((flags & IPS_OFFLOAD) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_OFFLOAD");
+        }
+        if ((flags & IPS_HW_OFFLOAD) != 0) {
+            if (sb.length() > 0) sb.append("|");
+            sb.append("IPS_HW_OFFLOAD");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        return "ConntrackMessage{"
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(OsConstants.NETLINK_NETFILTER))
+                + "}, "
+                + "nfgenmsg{" + nfGenMsg + "}, "
+                + "tuple_orig{" + tupleOrig + "}, "
+                + "tuple_reply{" + tupleReply + "}, "
+                + "status{" + status + "(" + stringForIpConntrackStatus(status) + ")" + "}, "
+                + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+                + "}";
     }
 }
diff --git a/common/netlinkclient/src/android/net/netlink/InetDiagMessage.java b/common/netlinkclient/src/android/net/netlink/InetDiagMessage.java
index ca07630..c085123 100644
--- a/common/netlinkclient/src/android/net/netlink/InetDiagMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/InetDiagMessage.java
@@ -120,7 +120,7 @@
         NetlinkSocket.sendMessage(fd, msg, 0, msg.length, TIMEOUT_MS);
         ByteBuffer response = NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
 
-        final NetlinkMessage nlMsg = NetlinkMessage.parse(response);
+        final NetlinkMessage nlMsg = NetlinkMessage.parse(response, NETLINK_INET_DIAG);
         final StructNlMsgHdr hdr = nlMsg.getHeader();
         if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
             return INVALID_UID;
@@ -213,7 +213,8 @@
     @Override
     public String toString() {
         return "InetDiagMessage{ "
-                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(NETLINK_INET_DIAG)) + "}, "
                 + "inet_diag_msg{"
                 + (mStructInetDiagMsg == null ? "" : mStructInetDiagMsg.toString()) + "} "
                 + "}";
diff --git a/common/netlinkclient/src/android/net/netlink/NetlinkConstants.java b/common/netlinkclient/src/android/net/netlink/NetlinkConstants.java
index 9db09c4..b4d9cc0 100644
--- a/common/netlinkclient/src/android/net/netlink/NetlinkConstants.java
+++ b/common/netlinkclient/src/android/net/netlink/NetlinkConstants.java
@@ -16,6 +16,7 @@
 
 package android.net.netlink;
 
+import android.annotation.NonNull;
 import android.system.OsConstants;
 
 import java.nio.ByteBuffer;
@@ -26,8 +27,10 @@
  *
  * Values taken from:
  *
- *     &lt;linux_src&gt;/include/uapi/linux/netlink.h
- *     &lt;linux_src&gt;/include/uapi/linux/rtnetlink.h
+ *     include/uapi/linux/netfilter/nfnetlink.h
+ *     include/uapi/linux/netfilter/nfnetlink_conntrack.h
+ *     include/uapi/linux/netlink.h
+ *     include/uapi/linux/rtnetlink.h
  *
  * @hide
  */
@@ -79,43 +82,71 @@
     }
 
     // Known values for struct nlmsghdr nlm_type.
-    public static final short NLMSG_NOOP         = 1;   // Nothing
-    public static final short NLMSG_ERROR        = 2;   // Error
-    public static final short NLMSG_DONE         = 3;   // End of a dump
-    public static final short NLMSG_OVERRUN      = 4;   // Data lost
-    public static final short NLMSG_MAX_RESERVED = 15;  // Max reserved value
+    public static final short NLMSG_NOOP                    = 1;   // Nothing
+    public static final short NLMSG_ERROR                   = 2;   // Error
+    public static final short NLMSG_DONE                    = 3;   // End of a dump
+    public static final short NLMSG_OVERRUN                 = 4;   // Data lost
+    public static final short NLMSG_MAX_RESERVED            = 15;  // Max reserved value
 
-    public static final short RTM_NEWLINK        = 16;
-    public static final short RTM_DELLINK        = 17;
-    public static final short RTM_GETLINK        = 18;
-    public static final short RTM_SETLINK        = 19;
-    public static final short RTM_NEWADDR        = 20;
-    public static final short RTM_DELADDR        = 21;
-    public static final short RTM_GETADDR        = 22;
-    public static final short RTM_NEWROUTE       = 24;
-    public static final short RTM_DELROUTE       = 25;
-    public static final short RTM_GETROUTE       = 26;
-    public static final short RTM_NEWNEIGH       = 28;
-    public static final short RTM_DELNEIGH       = 29;
-    public static final short RTM_GETNEIGH       = 30;
-    public static final short RTM_NEWRULE        = 32;
-    public static final short RTM_DELRULE        = 33;
-    public static final short RTM_GETRULE        = 34;
-    public static final short RTM_NEWNDUSEROPT   = 68;
+    public static final short RTM_NEWLINK                   = 16;
+    public static final short RTM_DELLINK                   = 17;
+    public static final short RTM_GETLINK                   = 18;
+    public static final short RTM_SETLINK                   = 19;
+    public static final short RTM_NEWADDR                   = 20;
+    public static final short RTM_DELADDR                   = 21;
+    public static final short RTM_GETADDR                   = 22;
+    public static final short RTM_NEWROUTE                  = 24;
+    public static final short RTM_DELROUTE                  = 25;
+    public static final short RTM_GETROUTE                  = 26;
+    public static final short RTM_NEWNEIGH                  = 28;
+    public static final short RTM_DELNEIGH                  = 29;
+    public static final short RTM_GETNEIGH                  = 30;
+    public static final short RTM_NEWRULE                   = 32;
+    public static final short RTM_DELRULE                   = 33;
+    public static final short RTM_GETRULE                   = 34;
+    public static final short RTM_NEWNDUSEROPT              = 68;
 
-    /* see &lt;linux_src&gt;/include/uapi/linux/sock_diag.h */
+    // Netfilter netlink message types are presented by two bytes: high byte subsystem and
+    // low byte operation. See the macro NFNL_SUBSYS_ID and NFNL_MSG_TYPE in
+    // include/uapi/linux/netfilter/nfnetlink.h
+    public static final short NFNL_SUBSYS_CTNETLINK         = 1;
+
+    public static final short IPCTNL_MSG_CT_NEW             = 0;
+    public static final short IPCTNL_MSG_CT_GET             = 1;
+    public static final short IPCTNL_MSG_CT_DELETE          = 2;
+    public static final short IPCTNL_MSG_CT_GET_CTRZERO     = 3;
+    public static final short IPCTNL_MSG_CT_GET_STATS_CPU   = 4;
+    public static final short IPCTNL_MSG_CT_GET_STATS       = 5;
+    public static final short IPCTNL_MSG_CT_GET_DYING       = 6;
+    public static final short IPCTNL_MSG_CT_GET_UNCONFIRMED = 7;
+
+    /* see include/uapi/linux/sock_diag.h */
     public static final short SOCK_DIAG_BY_FAMILY = 20;
 
     // Netlink groups.
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
-    public static String stringForNlMsgType(short nlm_type) {
-        switch (nlm_type) {
+    /**
+     * Convert a netlink message type to a string for control message.
+     */
+    @NonNull
+    private static String stringForCtlMsgType(short nlmType) {
+        switch (nlmType) {
             case NLMSG_NOOP: return "NLMSG_NOOP";
             case NLMSG_ERROR: return "NLMSG_ERROR";
             case NLMSG_DONE: return "NLMSG_DONE";
             case NLMSG_OVERRUN: return "NLMSG_OVERRUN";
+            default: return "unknown control message type: " + String.valueOf(nlmType);
+        }
+    }
+
+    /**
+     * Convert a netlink message type to a string for NETLINK_ROUTE.
+     */
+    @NonNull
+    private static String stringForRtMsgType(short nlmType) {
+        switch (nlmType) {
             case RTM_NEWLINK: return "RTM_NEWLINK";
             case RTM_DELLINK: return "RTM_DELLINK";
             case RTM_GETLINK: return "RTM_GETLINK";
@@ -133,11 +164,64 @@
             case RTM_DELRULE: return "RTM_DELRULE";
             case RTM_GETRULE: return "RTM_GETRULE";
             case RTM_NEWNDUSEROPT: return "RTM_NEWNDUSEROPT";
-            default:
-                return "unknown RTM type: " + String.valueOf(nlm_type);
+            default: return "unknown RTM type: " + String.valueOf(nlmType);
         }
     }
 
+    /**
+     * Convert a netlink message type to a string for NETLINK_INET_DIAG.
+     */
+    @NonNull
+    private static String stringForInetDiagMsgType(short nlmType) {
+        switch (nlmType) {
+            case SOCK_DIAG_BY_FAMILY: return "SOCK_DIAG_BY_FAMILY";
+            default: return "unknown SOCK_DIAG type: " + String.valueOf(nlmType);
+        }
+    }
+
+    /**
+     * Convert a netlink message type to a string for NETLINK_NETFILTER.
+     */
+    @NonNull
+    private static String stringForNfMsgType(short nlmType) {
+        final byte subsysId = (byte) (nlmType >> 8);
+        final byte msgType = (byte) nlmType;
+        switch (subsysId) {
+            case NFNL_SUBSYS_CTNETLINK:
+                switch (msgType) {
+                    case IPCTNL_MSG_CT_NEW: return "IPCTNL_MSG_CT_NEW";
+                    case IPCTNL_MSG_CT_GET: return "IPCTNL_MSG_CT_GET";
+                    case IPCTNL_MSG_CT_DELETE: return "IPCTNL_MSG_CT_DELETE";
+                    case IPCTNL_MSG_CT_GET_CTRZERO: return "IPCTNL_MSG_CT_GET_CTRZERO";
+                    case IPCTNL_MSG_CT_GET_STATS_CPU: return "IPCTNL_MSG_CT_GET_STATS_CPU";
+                    case IPCTNL_MSG_CT_GET_STATS: return "IPCTNL_MSG_CT_GET_STATS";
+                    case IPCTNL_MSG_CT_GET_DYING: return "IPCTNL_MSG_CT_GET_DYING";
+                    case IPCTNL_MSG_CT_GET_UNCONFIRMED: return "IPCTNL_MSG_CT_GET_UNCONFIRMED";
+                }
+                break;
+        }
+        return "unknown NETFILTER type: " + String.valueOf(nlmType);
+    }
+
+    /**
+     * Convert a netlink message type to a string by netlink family.
+     */
+    @NonNull
+    public static String stringForNlMsgType(short nlmType, int nlFamily) {
+        // Reserved control messages. The netlink family is ignored.
+        // See NLMSG_MIN_TYPE in include/uapi/linux/netlink.h.
+        if (nlmType <= NLMSG_MAX_RESERVED) return stringForCtlMsgType(nlmType);
+
+        // Netlink family messages. The netlink family is required. Note that the reason for using
+        // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
+        // not constant.
+        if (nlFamily == OsConstants.NETLINK_ROUTE) return stringForRtMsgType(nlmType);
+        if (nlFamily == OsConstants.NETLINK_INET_DIAG) return stringForInetDiagMsgType(nlmType);
+        if (nlFamily == OsConstants.NETLINK_NETFILTER) return stringForNfMsgType(nlmType);
+
+        return "unknown type: " + String.valueOf(nlmType);
+    }
+
     private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
             'A', 'B', 'C', 'D', 'E', 'F' };
     /**
diff --git a/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java b/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java
index dafa66b..ab2c223 100644
--- a/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java
@@ -16,6 +16,10 @@
 
 package android.net.netlink;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.system.OsConstants;
+
 import java.nio.ByteBuffer;
 
 
@@ -35,7 +39,12 @@
 public class NetlinkMessage {
     private final static String TAG = "NetlinkMessage";
 
-    public static NetlinkMessage parse(ByteBuffer byteBuffer) {
+    /**
+     * Parsing netlink messages for reserved control message or specific netlink message. The
+     * netlink family is required for parsing specific netlink message. See man-pages/netlink.
+     */
+    @Nullable
+    public static NetlinkMessage parse(@NonNull ByteBuffer byteBuffer, int nlFamily) {
         final int startPosition = (byteBuffer != null) ? byteBuffer.position() : -1;
         final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(byteBuffer);
         if (nlmsghdr == null) {
@@ -50,31 +59,24 @@
             return null;
         }
 
-        switch (nlmsghdr.nlmsg_type) {
-            //case NetlinkConstants.NLMSG_NOOP:
-            case NetlinkConstants.NLMSG_ERROR:
-                return (NetlinkMessage) NetlinkErrorMessage.parse(nlmsghdr, byteBuffer);
-            case NetlinkConstants.NLMSG_DONE:
-                byteBuffer.position(byteBuffer.position() + payloadLength);
-                return new NetlinkMessage(nlmsghdr);
-            //case NetlinkConstants.NLMSG_OVERRUN:
-            case NetlinkConstants.RTM_NEWNEIGH:
-            case NetlinkConstants.RTM_DELNEIGH:
-            case NetlinkConstants.RTM_GETNEIGH:
-                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
-            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
-                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
-            case NetlinkConstants.RTM_NEWNDUSEROPT:
-                return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
-            default:
-                if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
-                    // Netlink control message.  Just parse the header for now,
-                    // pretending the whole message was consumed.
-                    byteBuffer.position(byteBuffer.position() + payloadLength);
-                    return new NetlinkMessage(nlmsghdr);
-                }
-                return null;
+        // Reserved control messages. The netlink family is ignored.
+        // See NLMSG_MIN_TYPE in include/uapi/linux/netlink.h.
+        if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
+            return parseCtlMessage(nlmsghdr, byteBuffer, payloadLength);
         }
+
+        // Netlink family messages. The netlink family is required. Note that the reason for using
+        // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
+        // not constant.
+        if (nlFamily == OsConstants.NETLINK_ROUTE) {
+            return parseRtMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == OsConstants.NETLINK_INET_DIAG) {
+            return parseInetDiagMessage(nlmsghdr, byteBuffer);
+        } else if (nlFamily == OsConstants.NETLINK_NETFILTER) {
+            return parseNfMessage(nlmsghdr, byteBuffer);
+        }
+
+        return null;
     }
 
     protected StructNlMsgHdr mHeader;
@@ -89,6 +91,63 @@
 
     @Override
     public String toString() {
+        // The netlink family is not provided to StructNlMsgHdr#toString because NetlinkMessage
+        // doesn't store the information. So the netlink message type can't be transformed into
+        // a string by StructNlMsgHdr#toString and just keep as an integer. The specific message
+        // which inherits NetlinkMessage could override NetlinkMessage#toString and provide the
+        // specific netlink family to StructNlMsgHdr#toString.
         return "NetlinkMessage{" + (mHeader == null ? "" : mHeader.toString()) + "}";
     }
+
+    @NonNull
+    private static NetlinkMessage parseCtlMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer, int payloadLength) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.NLMSG_ERROR:
+                return (NetlinkMessage) NetlinkErrorMessage.parse(nlmsghdr, byteBuffer);
+            default: {
+                // Other netlink control messages. Just parse the header for now,
+                // pretending the whole message was consumed.
+                byteBuffer.position(byteBuffer.position() + payloadLength);
+                return new NetlinkMessage(nlmsghdr);
+            }
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseRtMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.RTM_NEWNEIGH:
+            case NetlinkConstants.RTM_DELNEIGH:
+            case NetlinkConstants.RTM_GETNEIGH:
+                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWNDUSEROPT:
+                return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseInetDiagMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
+                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
+
+    @Nullable
+    private static NetlinkMessage parseNfMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            @NonNull ByteBuffer byteBuffer) {
+        switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+                    | NetlinkConstants.IPCTNL_MSG_CT_NEW:
+            case NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+                    | NetlinkConstants.IPCTNL_MSG_CT_DELETE:
+                return (NetlinkMessage) ConntrackMessage.parse(nlmsghdr, byteBuffer);
+            default: return null;
+        }
+    }
 }
diff --git a/common/netlinkclient/src/android/net/netlink/NetlinkSocket.java b/common/netlinkclient/src/android/net/netlink/NetlinkSocket.java
index 7311fc5..ab4c052 100644
--- a/common/netlinkclient/src/android/net/netlink/NetlinkSocket.java
+++ b/common/netlinkclient/src/android/net/netlink/NetlinkSocket.java
@@ -65,7 +65,7 @@
             sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT);
             final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT);
             // recvMessage() guaranteed to not return null if it did not throw.
-            final NetlinkMessage response = NetlinkMessage.parse(bytes);
+            final NetlinkMessage response = NetlinkMessage.parse(bytes, nlProto);
             if (response != null && response instanceof NetlinkErrorMessage &&
                     (((NetlinkErrorMessage) response).getNlMsgError() != null)) {
                 final int errno = ((NetlinkErrorMessage) response).getNlMsgError().error;
diff --git a/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java b/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
index 8b9e7e0..099ff07 100644
--- a/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
@@ -23,8 +23,8 @@
 
 import android.system.OsConstants;
 
-import java.net.InetAddress;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 
@@ -48,23 +48,6 @@
     public static final short NDA_IFINDEX   = 8;
     public static final short NDA_MASTER    = 9;
 
-    private static StructNlAttr findNextAttrOfType(short attrType, ByteBuffer byteBuffer) {
-        while (byteBuffer != null && byteBuffer.remaining() > 0) {
-            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
-            if (nlAttr == null) {
-                break;
-            }
-            if (nlAttr.nla_type == attrType) {
-                return StructNlAttr.parse(byteBuffer);
-            }
-            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
-                break;
-            }
-            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
-        }
-        return null;
-    }
-
     public static RtNetlinkNeighborMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
         final RtNetlinkNeighborMessage neighMsg = new RtNetlinkNeighborMessage(header);
 
@@ -75,25 +58,25 @@
 
         // Some of these are message-type dependent, and not always present.
         final int baseOffset = byteBuffer.position();
-        StructNlAttr nlAttr = findNextAttrOfType(NDA_DST, byteBuffer);
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(NDA_DST, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mDestination = nlAttr.getValueAsInetAddress();
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_LLADDR, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_LLADDR, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mLinkLayerAddr = nlAttr.nla_value;
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_PROBES, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_PROBES, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mNumProbes = nlAttr.getValueAsInt(0);
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mCacheInfo = StructNdaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
         }
@@ -234,7 +217,8 @@
     public String toString() {
         final String ipLiteral = (mDestination == null) ? "" : mDestination.getHostAddress();
         return "RtNetlinkNeighborMessage{ "
-                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "nlmsghdr{"
+                + (mHeader == null ? "" : mHeader.toString(OsConstants.NETLINK_ROUTE)) + "}, "
                 + "ndmsg{" + (mNdmsg == null ? "" : mNdmsg.toString()) + "}, "
                 + "destination{" + ipLiteral + "} "
                 + "linklayeraddr{" + NetlinkConstants.hexify(mLinkLayerAddr) + "} "
diff --git a/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java b/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
index 8155977..7f247f5 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
@@ -16,7 +16,12 @@
 
 package android.net.netlink;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
 
 
 /**
@@ -35,6 +40,36 @@
     final public byte version;
     final public short res_id;  // N.B.: this is big endian in the kernel
 
+    /**
+     * Parses a netfilter netlink header from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the netfilter netlink header.
+     * @return the parsed netfilter netlink header, or {@code null} if the netfilter netlink header
+     *         could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructNfGenMsg parse(@NonNull ByteBuffer byteBuffer) {
+        Objects.requireNonNull(byteBuffer);
+
+        if (!hasAvailableSpace(byteBuffer)) return null;
+
+        final byte nfgen_family = byteBuffer.get();
+        final byte version = byteBuffer.get();
+
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.BIG_ENDIAN);
+        final short res_id = byteBuffer.getShort();
+        byteBuffer.order(originalOrder);
+
+        return new StructNfGenMsg(nfgen_family, version, res_id);
+    }
+
+    public StructNfGenMsg(byte family, byte ver, short id) {
+        nfgen_family = family;
+        version = ver;
+        res_id = id;
+    }
+
     public StructNfGenMsg(byte family) {
         nfgen_family = family;
         version = (byte) NFNETLINK_V0;
@@ -44,6 +79,25 @@
     public void pack(ByteBuffer byteBuffer) {
         byteBuffer.put(nfgen_family);
         byteBuffer.put(version);
+
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.BIG_ENDIAN);
         byteBuffer.putShort(res_id);
+        byteBuffer.order(originalOrder);
+    }
+
+    private static boolean hasAvailableSpace(@NonNull ByteBuffer byteBuffer) {
+        return byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    @Override
+    public String toString() {
+        final String familyStr = NetlinkConstants.stringForAddressFamily(nfgen_family);
+
+        return "NfGenMsg{ "
+                + "nfgen_family{" + familyStr + "}, "
+                + "version{" + Byte.toUnsignedInt(version) + "}, "
+                + "res_id{" + Short.toUnsignedInt(res_id) + "} "
+                + "}";
     }
 }
diff --git a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
index 747998d..b6e1d3f 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
@@ -16,10 +16,12 @@
 
 package android.net.netlink;
 
+import androidx.annotation.Nullable;
+
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 
 /**
@@ -48,9 +50,7 @@
         }
         final int baseOffset = byteBuffer.position();
 
-        // Assume the byte order of the buffer is the expected byte order of the value.
-        final StructNlAttr struct = new StructNlAttr(byteBuffer.order());
-        // The byte order of nla_len and nla_type is always native.
+        final StructNlAttr struct = new StructNlAttr();
         final ByteOrder originalOrder = byteBuffer.order();
         byteBuffer.order(ByteOrder.nativeOrder());
         try {
@@ -87,20 +87,39 @@
         return struct;
     }
 
+    /**
+     * Find next netlink attribute with a given type from {@link ByteBuffer}.
+     *
+     * @param attrType The given netlink attribute type is requested for.
+     * @param byteBuffer The buffer from which to find the netlink attribute.
+     * @return the found netlink attribute, or {@code null} if the netlink attribute could not be
+     *         found or parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructNlAttr findNextAttrOfType(short attrType,
+            @Nullable ByteBuffer byteBuffer) {
+        while (byteBuffer != null && byteBuffer.remaining() > 0) {
+            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
+            if (nlAttr == null) {
+                break;
+            }
+            if (nlAttr.nla_type == attrType) {
+                return StructNlAttr.parse(byteBuffer);
+            }
+            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
+                break;
+            }
+            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
+        }
+        return null;
+    }
+
     public short nla_len = (short) NLA_HEADERLEN;
     public short nla_type;
     public byte[] nla_value;
 
-    // The byte order used to read/write the value member. Netlink length and
-    // type members are always read/written in native order.
-    private ByteOrder mByteOrder = ByteOrder.nativeOrder();
-
     public StructNlAttr() {}
 
-    public StructNlAttr(ByteOrder byteOrder) {
-        mByteOrder = byteOrder;
-    }
-
     public StructNlAttr(short type, byte value) {
         nla_type = type;
         setValue(new byte[1]);
@@ -112,10 +131,16 @@
     }
 
     public StructNlAttr(short type, short value, ByteOrder order) {
-        this(order);
         nla_type = type;
         setValue(new byte[Short.BYTES]);
-        getValueAsByteBuffer().putShort(value);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        final ByteOrder originalOrder = buf.order();
+        try {
+            buf.order(order);
+            buf.putShort(value);
+        } finally {
+            buf.order(originalOrder);
+        }
     }
 
     public StructNlAttr(short type, int value) {
@@ -123,10 +148,16 @@
     }
 
     public StructNlAttr(short type, int value, ByteOrder order) {
-        this(order);
         nla_type = type;
         setValue(new byte[Integer.BYTES]);
-        getValueAsByteBuffer().putInt(value);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        final ByteOrder originalOrder = buf.order();
+        try {
+            buf.order(order);
+            buf.putInt(value);
+        } finally {
+            buf.order(originalOrder);
+        }
     }
 
     public StructNlAttr(short type, InetAddress ip) {
@@ -152,13 +183,58 @@
         return NetlinkConstants.alignedLengthOf(nla_len);
     }
 
+    /**
+     * Get attribute value as BE16.
+     */
+    public short getValueAsBe16(short defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Short.BYTES) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getShort();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
+    public int getValueAsBe32(int defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getInt();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
     public ByteBuffer getValueAsByteBuffer() {
         if (nla_value == null) { return null; }
         final ByteBuffer byteBuffer = ByteBuffer.wrap(nla_value);
-        byteBuffer.order(mByteOrder);
+        // By convention, all buffers in this library are in native byte order because netlink is in
+        // native byte order. It's the order that is used by NetlinkSocket.recvMessage and the only
+        // order accepted by NetlinkMessage.parse.
+        byteBuffer.order(ByteOrder.nativeOrder());
         return byteBuffer;
     }
 
+    /**
+     * Get attribute value as byte.
+     */
+    public byte getValueAsByte(byte defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Byte.BYTES) {
+            return defaultValue;
+        }
+        return getValueAsByteBuffer().get();
+    }
+
     public int getValueAsInt(int defaultValue) {
         final ByteBuffer byteBuffer = getValueAsByteBuffer();
         if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
diff --git a/common/netlinkclient/src/android/net/netlink/StructNlMsgHdr.java b/common/netlinkclient/src/android/net/netlink/StructNlMsgHdr.java
index 1d03a49..55a649d 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNlMsgHdr.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNlMsgHdr.java
@@ -16,6 +16,9 @@
 
 package android.net.netlink;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import java.nio.ByteBuffer;
 
 
@@ -44,7 +47,8 @@
     public static final short NLM_F_CREATE    = 0x400;
     public static final short NLM_F_APPEND    = 0x800;
 
-
+    // TODO: Probably need to distinguish the flags which have the same value. For example,
+    // NLM_F_MATCH (0x200) and NLM_F_EXCL (0x200).
     public static String stringForNlMsgFlags(short flags) {
         final StringBuilder sb = new StringBuilder();
         if ((flags & NLM_F_REQUEST) != 0) {
@@ -124,8 +128,23 @@
 
     @Override
     public String toString() {
+        return toString(null /* unknown netlink family */);
+    }
+
+    /**
+     * Transform a netlink header into a string. The netlink family is required for transforming
+     * a netlink type integer into a string.
+     * @param nlFamily netlink family. Using Integer will not incur autoboxing penalties because
+     *                 family values are small, and all Integer objects between -128 and 127 are
+     *                 statically cached. See Integer.IntegerCache.
+     * @return A list of header elements.
+     */
+    @NonNull
+    public String toString(@Nullable Integer nlFamily) {
         final String typeStr = "" + nlmsg_type
-                + "(" + NetlinkConstants.stringForNlMsgType(nlmsg_type) + ")";
+                + "(" + (nlFamily == null
+                ? "" : NetlinkConstants.stringForNlMsgType(nlmsg_type, nlFamily))
+                + ")";
         final String flagsStr = "" + nlmsg_flags
                 + "(" + stringForNlMsgFlags(nlmsg_flags) + ")";
         return "StructNlMsgHdr{ "
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index b5a3e95..31e920d 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -15,6 +15,10 @@
 //
 
 // AIDL interfaces between the core system and the networking mainline module.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 aidl_interface {
     name: "ipmemorystore-aidl-interfaces",
     local_include_dir: "src",
@@ -22,13 +26,20 @@
         "src/android/net/IIpMemoryStore.aidl",
         "src/android/net/IIpMemoryStoreCallbacks.aidl",
         "src/android/net/ipmemorystore/**/*.aidl",
+        // New AIDL classes should go into android.net.networkstack.aidl so they can be clearly
+        // identified
+        "src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl",
     ],
     backend: {
         java: {
             apex_available: [
                 "//apex_available:platform",
                 "com.android.wifi",
+                "com.android.bluetooth.updatable",
+                "com.android.tethering",
             ],
+            // this is part of updatable modules(NetworkStack) which targets 29(Q)
+            min_sdk_version: "29",
         },
         ndk: {
             enabled: false,
@@ -45,6 +56,9 @@
         "5",
         "6",
         "7",
+        "8",
+        "9",
+        "10",
     ],
     visibility: [
         "//system/tools/aidl/build",
@@ -55,9 +69,11 @@
     name: "networkstack-aidl-interfaces",
     local_include_dir: "src",
     include_dirs: [
-        "frameworks/base/core/java", // For framework parcelables.
-        "frameworks/native/aidl/binder/android/os", // For PersistableBundle.aidl
-        "frameworks/base/wifi/aidl-export", // For wifi parcelables.
+        // For framework parcelables.
+        "frameworks/base/core/java",
+        // For API parcelables in connectivity
+        "packages/modules/Connectivity/framework/aidl-export",
+        "frameworks/native/aidl/binder", // For PersistableBundle.aidl
     ],
     srcs: [
         "src/android/net/DataStallReportParcelable.aidl",
@@ -83,6 +99,9 @@
         "src/android/net/dhcp/IDhcpServerCallbacks.aidl",
         "src/android/net/ip/IIpClient.aidl",
         "src/android/net/ip/IIpClientCallbacks.aidl",
+        // New AIDL classes should go into android.net.networkstack.aidl so they can be clearly
+        // identified
+        "src/android/net/networkstack/aidl/dhcp/DhcpOption.aidl",
     ],
     backend: {
         java: {
@@ -90,7 +109,10 @@
                 "//apex_available:platform",
                 "com.android.bluetooth.updatable",
                 "com.android.wifi",
+                "com.android.tethering",
             ],
+            // this is part of updatable modules(NetworkStack) which targets 29(Q)
+            min_sdk_version: "29",
         },
         ndk: {
             enabled: false,
@@ -108,32 +130,54 @@
         "5",
         "6",
         "7",
+        "8",
+        "9",
+        "10",
+        "11",
     ],
     // TODO: have tethering depend on networkstack-client and set visibility to private
     visibility: [
         "//system/tools/aidl/build",
         "//frameworks/base/packages/Tethering",
+        "//packages/modules/Connectivity/Tethering",
     ],
 }
 
 java_library {
     name: "networkstack-client",
     sdk_version: "system_current",
+    // this is part of updatable modules(NetworkStack) which runs on Q and above
+    min_sdk_version: "29",
     srcs: [
         ":framework-annotations",
+        "src/android/net/ip/**/*.java",
+        "src/android/net/IpMemoryStore.java",
         "src/android/net/IpMemoryStoreClient.java",
         "src/android/net/ipmemorystore/**/*.java",
+        "src/android/net/NetworkMonitorManager.java",
         "src/android/net/networkstack/**/*.java",
+        "src/android/net/networkstack/aidl/quirks/**/*.java",
         "src/android/net/shared/**/*.java",
+        "src/android/net/util/**/*.java",
+    ],
+    libs: [
+        "net-utils-framework-common",  // XXX for IpUtils.java only
     ],
     static_libs: [
-        "ipmemorystore-aidl-interfaces-java",
-        "networkstack-aidl-interfaces-java",
+        "ipmemorystore-aidl-interfaces-V10-java",
+        "networkstack-aidl-interfaces-V11-java",
     ],
     visibility: [
-        "//frameworks/base/packages/Tethering",
+        "//frameworks/base/packages/Connectivity/service",
+        "//packages/modules/Connectivity/Tethering",
+        "//packages/modules/Connectivity/service",
         "//frameworks/base/services/net",
         "//frameworks/opt/net/wifi/service",
+        "//packages/apps/Bluetooth",
         "//packages/modules/NetworkStack",
     ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
 }
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/.hash b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/.hash
new file mode 100644
index 0000000..0e5269e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/.hash
@@ -0,0 +1 @@
+d5ea5eb3ddbdaa9a986ce6ba70b0804ca3e39b0c
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStore.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStore.aidl
new file mode 100644
index 0000000..bf7a26d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStore.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStore {
+  oneway void storeNetworkAttributes(String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void storeBlob(String l2Key, String clientId, String name, in android.net.ipmemorystore.Blob data, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void findL2Key(in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnL2KeyResponseListener listener);
+  oneway void isSameNetwork(String l2Key1, String l2Key2, android.net.ipmemorystore.IOnSameL3NetworkResponseListener listener);
+  oneway void retrieveNetworkAttributes(String l2Key, android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener listener);
+  oneway void retrieveBlob(String l2Key, String clientId, String name, android.net.ipmemorystore.IOnBlobRetrievedListener listener);
+  oneway void factoryReset();
+  oneway void delete(String l2Key, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void deleteCluster(String cluster, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStoreCallbacks.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStoreCallbacks.aidl
new file mode 100644
index 0000000..2024391
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/IIpMemoryStoreCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStoreCallbacks {
+  oneway void onIpMemoryStoreFetched(in android.net.IIpMemoryStore ipMemoryStore);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/Blob.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/Blob.aidl
new file mode 100644
index 0000000..8a1b57e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/Blob.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable Blob {
+  byte[] data;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
new file mode 100644
index 0000000..e711272
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnBlobRetrievedListener {
+  oneway void onBlobRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in String name, in android.net.ipmemorystore.Blob data);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
new file mode 100644
index 0000000..4abecb9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnL2KeyResponseListener {
+  oneway void onL2KeyResponse(in android.net.ipmemorystore.StatusParcelable status, in String l2Key);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
new file mode 100644
index 0000000..05c48b3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnNetworkAttributesRetrievedListener {
+  oneway void onNetworkAttributesRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
new file mode 100644
index 0000000..0bc8c5e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnSameL3NetworkResponseListener {
+  oneway void onSameL3NetworkResponse(in android.net.ipmemorystore.StatusParcelable status, in android.net.ipmemorystore.SameL3NetworkResponseParcelable response);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusAndCountListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
new file mode 100644
index 0000000..cf30fa1
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusAndCountListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status, int count);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusListener.aidl
new file mode 100644
index 0000000..e71de47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/IOnStatusListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
new file mode 100644
index 0000000..2ac7644
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -0,0 +1,28 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable NetworkAttributesParcelable {
+  byte[] assignedV4Address;
+  long assignedV4AddressExpiry;
+  String cluster;
+  android.net.ipmemorystore.Blob[] dnsAddresses;
+  int mtu;
+  @nullable android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable ipv6ProvisioningLossQuirk;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
new file mode 100644
index 0000000..42a1feb
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable SameL3NetworkResponseParcelable {
+  String l2Key1;
+  String l2Key2;
+  float confidence;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/StatusParcelable.aidl
new file mode 100644
index 0000000..1bea082
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/ipmemorystore/StatusParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable StatusParcelable {
+  int resultCode;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
new file mode 100644
index 0000000..e2ecbb4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/10/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.quirks;
+@JavaDerive(toString=true)
+parcelable IPv6ProvisioningLossQuirkParcelable {
+  int detectionCount;
+  long quirkExpiry;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/.hash b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/.hash
new file mode 100644
index 0000000..346969a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/.hash
@@ -0,0 +1 @@
+70cbb9e5b4009a86a81aa2a9bdf25e21442224be
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStore.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStore.aidl
new file mode 100644
index 0000000..bf7a26d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStore.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStore {
+  oneway void storeNetworkAttributes(String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void storeBlob(String l2Key, String clientId, String name, in android.net.ipmemorystore.Blob data, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void findL2Key(in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnL2KeyResponseListener listener);
+  oneway void isSameNetwork(String l2Key1, String l2Key2, android.net.ipmemorystore.IOnSameL3NetworkResponseListener listener);
+  oneway void retrieveNetworkAttributes(String l2Key, android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener listener);
+  oneway void retrieveBlob(String l2Key, String clientId, String name, android.net.ipmemorystore.IOnBlobRetrievedListener listener);
+  oneway void factoryReset();
+  oneway void delete(String l2Key, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void deleteCluster(String cluster, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStoreCallbacks.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStoreCallbacks.aidl
new file mode 100644
index 0000000..2024391
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/IIpMemoryStoreCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStoreCallbacks {
+  oneway void onIpMemoryStoreFetched(in android.net.IIpMemoryStore ipMemoryStore);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/Blob.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/Blob.aidl
new file mode 100644
index 0000000..8a1b57e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/Blob.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable Blob {
+  byte[] data;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
new file mode 100644
index 0000000..e711272
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnBlobRetrievedListener {
+  oneway void onBlobRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in String name, in android.net.ipmemorystore.Blob data);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
new file mode 100644
index 0000000..4abecb9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnL2KeyResponseListener {
+  oneway void onL2KeyResponse(in android.net.ipmemorystore.StatusParcelable status, in String l2Key);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
new file mode 100644
index 0000000..05c48b3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnNetworkAttributesRetrievedListener {
+  oneway void onNetworkAttributesRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
new file mode 100644
index 0000000..0bc8c5e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnSameL3NetworkResponseListener {
+  oneway void onSameL3NetworkResponse(in android.net.ipmemorystore.StatusParcelable status, in android.net.ipmemorystore.SameL3NetworkResponseParcelable response);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusAndCountListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
new file mode 100644
index 0000000..cf30fa1
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusAndCountListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status, int count);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusListener.aidl
new file mode 100644
index 0000000..e71de47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/IOnStatusListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
new file mode 100644
index 0000000..92a570d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable NetworkAttributesParcelable {
+  byte[] assignedV4Address;
+  long assignedV4AddressExpiry;
+  String cluster;
+  android.net.ipmemorystore.Blob[] dnsAddresses;
+  int mtu;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
new file mode 100644
index 0000000..eca0987
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable SameL3NetworkResponseParcelable {
+  String l2Key1;
+  String l2Key2;
+  float confidence;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/StatusParcelable.aidl
new file mode 100644
index 0000000..7554608
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/8/android/net/ipmemorystore/StatusParcelable.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable StatusParcelable {
+  int resultCode;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/.hash b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/.hash
new file mode 100644
index 0000000..12aa24a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/.hash
@@ -0,0 +1 @@
+c34b6b37db3616fac1c4e705a04c6899772cc428
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStore.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStore.aidl
new file mode 100644
index 0000000..bf7a26d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStore.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStore {
+  oneway void storeNetworkAttributes(String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void storeBlob(String l2Key, String clientId, String name, in android.net.ipmemorystore.Blob data, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void findL2Key(in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnL2KeyResponseListener listener);
+  oneway void isSameNetwork(String l2Key1, String l2Key2, android.net.ipmemorystore.IOnSameL3NetworkResponseListener listener);
+  oneway void retrieveNetworkAttributes(String l2Key, android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener listener);
+  oneway void retrieveBlob(String l2Key, String clientId, String name, android.net.ipmemorystore.IOnBlobRetrievedListener listener);
+  oneway void factoryReset();
+  oneway void delete(String l2Key, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void deleteCluster(String cluster, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStoreCallbacks.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStoreCallbacks.aidl
new file mode 100644
index 0000000..2024391
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/IIpMemoryStoreCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface IIpMemoryStoreCallbacks {
+  oneway void onIpMemoryStoreFetched(in android.net.IIpMemoryStore ipMemoryStore);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/Blob.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/Blob.aidl
new file mode 100644
index 0000000..8a1b57e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/Blob.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable Blob {
+  byte[] data;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
new file mode 100644
index 0000000..e711272
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnBlobRetrievedListener {
+  oneway void onBlobRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in String name, in android.net.ipmemorystore.Blob data);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
new file mode 100644
index 0000000..4abecb9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnL2KeyResponseListener {
+  oneway void onL2KeyResponse(in android.net.ipmemorystore.StatusParcelable status, in String l2Key);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
new file mode 100644
index 0000000..05c48b3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnNetworkAttributesRetrievedListener {
+  oneway void onNetworkAttributesRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
new file mode 100644
index 0000000..0bc8c5e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnSameL3NetworkResponseListener {
+  oneway void onSameL3NetworkResponse(in android.net.ipmemorystore.StatusParcelable status, in android.net.ipmemorystore.SameL3NetworkResponseParcelable response);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusAndCountListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
new file mode 100644
index 0000000..cf30fa1
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusAndCountListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status, int count);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusListener.aidl
new file mode 100644
index 0000000..e71de47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/IOnStatusListener.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+interface IOnStatusListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
new file mode 100644
index 0000000..c52a5c4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable NetworkAttributesParcelable {
+  byte[] assignedV4Address;
+  long assignedV4AddressExpiry;
+  String cluster;
+  android.net.ipmemorystore.Blob[] dnsAddresses;
+  int mtu;
+  @nullable android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable ipv6ProvisioningLossQuirk;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
new file mode 100644
index 0000000..eca0987
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable SameL3NetworkResponseParcelable {
+  String l2Key1;
+  String l2Key2;
+  float confidence;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/StatusParcelable.aidl
new file mode 100644
index 0000000..7554608
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/ipmemorystore/StatusParcelable.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+parcelable StatusParcelable {
+  int resultCode;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
new file mode 100644
index 0000000..3769230
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/9/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.quirks;
+parcelable IPv6ProvisioningLossQuirkParcelable {
+  int detectionCount;
+  long quirkExpiry;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
index 92a570d..2ac7644 100644
--- a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -17,10 +17,12 @@
 
 package android.net.ipmemorystore;
 /* @hide */
+@JavaDerive(toString=true)
 parcelable NetworkAttributesParcelable {
   byte[] assignedV4Address;
   long assignedV4AddressExpiry;
   String cluster;
   android.net.ipmemorystore.Blob[] dnsAddresses;
   int mtu;
+  @nullable android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable ipv6ProvisioningLossQuirk;
 }
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
index eca0987..42a1feb 100644
--- a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -17,6 +17,7 @@
 
 package android.net.ipmemorystore;
 /* @hide */
+@JavaDerive(toString=true)
 parcelable SameL3NetworkResponseParcelable {
   String l2Key1;
   String l2Key2;
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/StatusParcelable.aidl
index 7554608..1bea082 100644
--- a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/StatusParcelable.aidl
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/StatusParcelable.aidl
@@ -17,6 +17,7 @@
 
 package android.net.ipmemorystore;
 /* @hide */
+@JavaDerive(toString=true)
 parcelable StatusParcelable {
   int resultCode;
 }
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
new file mode 100644
index 0000000..e2ecbb4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.quirks;
+@JavaDerive(toString=true)
+parcelable IPv6ProvisioningLossQuirkParcelable {
+  int detectionCount;
+  long quirkExpiry;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/.hash
new file mode 100644
index 0000000..8125724
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/.hash
@@ -0,0 +1 @@
+4925f4fdbb270e4f35cc5519a15ed8dd8c69a549
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..0f860a5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..4445be7
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,29 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..db9145f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitor.aidl
@@ -0,0 +1,43 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 1;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
+  const int NETWORK_VALIDATION_PROBE_DNS = 4;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 8;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 32;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 64;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..b2685ad
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,31 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  oneway void showProvisioningNotification(String action, String packageName) = 3;
+  oneway void hideProvisioningNotification() = 4;
+  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..396b42a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..97c9970
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..77fca83
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6137305
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..d3adbb3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..b45f6da
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..7634ac9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..1d0bbbe
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..c6d6361
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..171817c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,38 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ProvisioningConfigurationParcelable {
+  boolean enableIPv4;
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+  @nullable List<android.net.networkstack.aidl.dhcp.DhcpOption> options;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..4646ede
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..00f15da
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..b0a0f0f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,28 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..d56ef8e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..8f3288e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..83cebdf
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..35da06c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..5607b2a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClient.aidl
@@ -0,0 +1,37 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9a84784
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..c97212b
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/10/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+  byte type;
+  @nullable byte[] value;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/.hash
new file mode 100644
index 0000000..2914d2a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/.hash
@@ -0,0 +1 @@
+7fecd0a7a6d978705afad88c5e492613cc46e2cb
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..771deda
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..31f2194
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,44 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..d92196d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitor.aidl
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 1;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 4;
+  const int NETWORK_VALIDATION_PROBE_DNS = 4;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 8;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 32;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 64;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..36eda8e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  oneway void showProvisioningNotification(String action, String packageName) = 3;
+  oneway void hideProvisioningNotification() = 4;
+  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..8120ffc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..0b6b778
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..6103774
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6a597e6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..83796ee
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..4b3fff5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,39 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..18cf954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..4d6d5a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..1457caf
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..0b7a7a1
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,54 @@
+/*
+**
+** 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.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ProvisioningConfigurationParcelable {
+  boolean enableIPv4;
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+  @nullable List<android.net.networkstack.aidl.dhcp.DhcpOption> options;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..94fc27f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..0e1c21c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3cd8860
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,43 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..fa412cb
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,48 @@
+/**
+ *
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..9312f47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..1109f35
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..ab8577c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..1fe4c4c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClient.aidl
@@ -0,0 +1,52 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..488510d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,51 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..eea3e0d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/11/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,39 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+  byte type;
+  @nullable byte[] value;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/.hash
new file mode 100644
index 0000000..c7ab51a
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/.hash
@@ -0,0 +1 @@
+f6e2137b5033902774f78726d429399db3b18cab
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..69ff31f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..7bb5c41
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..5945819
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitor.aidl
@@ -0,0 +1,42 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 1;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
+  const int NETWORK_VALIDATION_PROBE_DNS = 4;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 8;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 32;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 64;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..b7ddad9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  oneway void showProvisioningNotification(String action, String packageName) = 3;
+  oneway void hideProvisioningNotification() = 4;
+  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..0864886
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..ec16def
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..c882bf4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..c91d7a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..dca5138
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..2e0955f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..aa09c3d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..f31a669
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..cada4d3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..b8dfb91
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,35 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable ProvisioningConfigurationParcelable {
+  boolean enableIPv4;
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..f7ac167
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..c50f541
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..adbd57d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..d66ca9d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..dfcaf98
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..ef936cc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,29 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..63b89ad
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..9245954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClient.aidl
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9aabb1f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/8/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,35 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/.hash
new file mode 100644
index 0000000..5f75a2c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/.hash
@@ -0,0 +1 @@
+5c69ca9edf1e0f21e3c7c54ef40f9802d5011bec
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..5fd65f3
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..a351016
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,28 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..5945819
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitor.aidl
@@ -0,0 +1,42 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 1;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
+  const int NETWORK_VALIDATION_PROBE_DNS = 4;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 8;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 32;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 64;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..b7ddad9
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,30 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  oneway void showProvisioningNotification(String action, String packageName) = 3;
+  oneway void hideProvisioningNotification() = 4;
+  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..0864886
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..ec16def
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..ea76763
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..f8d4aa4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..30cf4d5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..30fbbc7
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..4f2736e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,25 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..8dd07bc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,26 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..7f124ae
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,23 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..4747855
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ProvisioningConfigurationParcelable {
+  boolean enableIPv4;
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..5e76ab5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..4401b92
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,31 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3aef40f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..d36b11e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,31 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..dfcaf98
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..ef936cc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,29 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..63b89ad
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..9245954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClient.aidl
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9aabb1f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/9/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,35 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
+// edit this file. It looks like you are doing that because you have modified
+// an AIDL interface in a backward-incompatible way, e.g., deleting a function
+// from an interface or a field from a parcelable and it broke the build. That
+// breakage is intended.
+//
+// You must not make a backward incompatible changes to the AIDL files built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DataStallReportParcelable.aidl
index 69ff31f..0f860a5 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DataStallReportParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DataStallReportParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable DataStallReportParcelable {
   long timestampMillis = 0;
   int detectionMethod = 1;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DhcpResultsParcelable.aidl
index 7bb5c41..4445be7 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DhcpResultsParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/DhcpResultsParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable DhcpResultsParcelable {
   android.net.StaticIpConfiguration baseConfiguration;
   int leaseDuration;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
index 5945819..9e7b40d 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -34,6 +35,7 @@
   const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
   const int NETWORK_VALIDATION_RESULT_VALID = 1;
   const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 4;
   const int NETWORK_VALIDATION_PROBE_DNS = 4;
   const int NETWORK_VALIDATION_PROBE_HTTP = 8;
   const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
index b7ddad9..b2685ad 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackConnector.aidl
index 17a65cf..396b42a 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackConnector.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackConnector.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -22,4 +23,5 @@
   oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
   oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
   oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackStatusCallback.aidl
index ec16def..97c9970 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackStatusCallback.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkStackStatusCallback.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InformationElementParcelable.aidl
index c882bf4..77fca83 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InformationElementParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InformationElementParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable InformationElementParcelable {
   int id;
   byte[] payload;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InitialConfigurationParcelable.aidl
index c91d7a2..6137305 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InitialConfigurationParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/InitialConfigurationParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable InitialConfigurationParcelable {
   android.net.LinkAddress[] ipAddresses;
   android.net.IpPrefix[] directlyConnectedRoutes;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2InformationParcelable.aidl
index dca5138..d3adbb3 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2InformationParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2InformationParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable Layer2InformationParcelable {
   String l2Key;
   String cluster;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2PacketParcelable.aidl
index 2e0955f..b45f6da 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2PacketParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/Layer2PacketParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable Layer2PacketParcelable {
   android.net.MacAddress dstMacAddress;
   byte[] payload;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NattKeepalivePacketDataParcelable.aidl
index aa09c3d..7634ac9 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NattKeepalivePacketDataParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable NattKeepalivePacketDataParcelable {
   byte[] srcAddress;
   int srcPort;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NetworkTestResultParcelable.aidl
index f31a669..1d0bbbe 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NetworkTestResultParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/NetworkTestResultParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable NetworkTestResultParcelable {
   long timestampMillis;
   int result;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
index cada4d3..c6d6361 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable PrivateDnsConfigParcel {
   String hostname;
   String[] ips;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ProvisioningConfigurationParcelable.aidl
index b8dfb91..171817c 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ProvisioningConfigurationParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ProvisioningConfigurationParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable ProvisioningConfigurationParcelable {
   boolean enableIPv4;
   boolean enableIPv6;
@@ -32,4 +34,5 @@
   boolean enablePreconnection;
   @nullable android.net.ScanResultInfoParcelable scanResultInfo;
   @nullable android.net.Layer2InformationParcelable layer2Info;
+  @nullable List<android.net.networkstack.aidl.dhcp.DhcpOption> options;
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ScanResultInfoParcelable.aidl
index f7ac167..4646ede 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ScanResultInfoParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ScanResultInfoParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable ScanResultInfoParcelable {
   String ssid;
   String bssid;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/TcpKeepalivePacketDataParcelable.aidl
index c50f541..00f15da 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/TcpKeepalivePacketDataParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net;
+@JavaDerive(toString=true)
 parcelable TcpKeepalivePacketDataParcelable {
   byte[] srcAddress;
   int srcPort;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpLeaseParcelable.aidl
index adbd57d..b0a0f0f 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpLeaseParcelable.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net.dhcp;
+@JavaDerive(toString=true)
 parcelable DhcpLeaseParcelable {
   byte[] clientId;
   byte[] hwAddr;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
index d66ca9d..d56ef8e 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
@@ -16,6 +17,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package android.net.dhcp;
+@JavaDerive(toString=true)
 parcelable DhcpServingParamsParcel {
   int serverAddr;
   int serverAddrPrefixLength;
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpEventCallbacks.aidl
index dfcaf98..8f3288e 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpEventCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServer.aidl
index ef936cc..83cebdf 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServer.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServer.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServerCallbacks.aidl
index 63b89ad..35da06c 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServerCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
index 9245954..5607b2a 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
index 9aabb1f..9a84784 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
@@ -2,13 +2,14 @@
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
 
-// This file is a snapshot of an AIDL interface (or parcelable). Do not try to
-// edit this file. It looks like you are doing that because you have modified
-// an AIDL interface in a backward-incompatible way, e.g., deleting a function
-// from an interface or a field from a parcelable and it broke the build. That
-// breakage is intended.
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
 //
-// You must not make a backward incompatible changes to the AIDL files built
+// You must not make a backward incompatible change to any AIDL file built
 // with the aidl_interface module type with versions property set. The module
 // type is used to build AIDL files in a way that they can be used across
 // independently updatable components of the system. If a device is shipped
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..c97212b
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,24 @@
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+  byte type;
+  @nullable byte[] value;
+}
diff --git a/common/networkstackclient/src/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/src/android/net/DataStallReportParcelable.aidl
index c5ba19d..4cd5aff 100644
--- a/common/networkstackclient/src/android/net/DataStallReportParcelable.aidl
+++ b/common/networkstackclient/src/android/net/DataStallReportParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable DataStallReportParcelable {
     /**
      * Timestamp of the report, relative to SystemClock.elapsedRealtime().
@@ -45,4 +46,4 @@
      * Only set if the detection method is DNS, otherwise 0.
      */
     int dnsConsecutiveTimeouts = 4;
-}
\ No newline at end of file
+}
diff --git a/common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl
index 0439d7b..d53c1fb 100644
--- a/common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl
+++ b/common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl
@@ -18,6 +18,7 @@
 
 import android.net.StaticIpConfiguration;
 
+@JavaDerive(toString=true)
 parcelable DhcpResultsParcelable {
     StaticIpConfiguration baseConfiguration;
     int leaseDuration;
diff --git a/common/networkstackclient/src/android/net/INetworkMonitor.aidl b/common/networkstackclient/src/android/net/INetworkMonitor.aidl
index 3fc81a3..b124734 100644
--- a/common/networkstackclient/src/android/net/INetworkMonitor.aidl
+++ b/common/networkstackclient/src/android/net/INetworkMonitor.aidl
@@ -44,10 +44,16 @@
     // are set, then it's equal to NETWORK_TEST_RESULT_INVALID. If NETWORK_VALIDATION_RESULT_VALID
     // is set, then the network validates and equal to NETWORK_TEST_RESULT_VALID. If
     // NETWORK_VALIDATION_RESULT_PARTIAL is set, then the network has partial connectivity which
-    // is equal to NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY. NETWORK_VALIDATION_PROBE_* is set
-    // when the specific probe result of the network is resolved.
+    // is equal to NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY. Networks receiving validation that both
+    // do not require validation and are not validated will have NETWORK_VALIDATION_RESULT_SKIPPED
+    // set. NETWORK_VALIDATION_PROBE_* is set when the specific probe result of the network is
+    // resolved.
     const int NETWORK_VALIDATION_RESULT_VALID = 0x01;
     const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
+    const int NETWORK_VALIDATION_RESULT_SKIPPED = 0x04;
+
+    // NETWORK_VALIDATION_RESULT_* and NETWORK_VALIDATION_PROBE_* are independent values sent in
+    // different ints.
     const int NETWORK_VALIDATION_PROBE_DNS = 0x04;
     const int NETWORK_VALIDATION_PROBE_HTTP = 0x08;
     const int NETWORK_VALIDATION_PROBE_HTTPS = 0x10;
diff --git a/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
index 79eb95a..b5fd280 100644
--- a/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
+++ b/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
@@ -21,6 +21,7 @@
 import android.net.INetworkMonitor;
 import android.net.NetworkTestResultParcelable;
 import android.net.PrivateDnsConfigParcel;
+import android.os.PersistableBundle;
 
 /** @hide */
 oneway interface INetworkMonitorCallbacks {
diff --git a/common/networkstackclient/src/android/net/INetworkStackConnector.aidl b/common/networkstackclient/src/android/net/INetworkStackConnector.aidl
index 3751c36..fa7abf5 100644
--- a/common/networkstackclient/src/android/net/INetworkStackConnector.aidl
+++ b/common/networkstackclient/src/android/net/INetworkStackConnector.aidl
@@ -17,6 +17,7 @@
 
 import android.net.IIpMemoryStoreCallbacks;
 import android.net.INetworkMonitorCallbacks;
+import android.net.INetworkStackStatusCallback;
 import android.net.Network;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpServerCallbacks;
@@ -29,4 +30,21 @@
     void makeNetworkMonitor(in Network network, String name, in INetworkMonitorCallbacks cb);
     void makeIpClient(in String ifName, in IIpClientCallbacks callbacks);
     void fetchIpMemoryStore(in IIpMemoryStoreCallbacks cb);
+    /**
+     * Mark a UID as test UID, allowing it to use the TestNetworkStackService.
+     *
+     * TestNetworkStackService is a binder service identical to NetworkStackService, but only
+     * available on userdebug builds, and only usable by the test UID. It does not require the
+     * MAINLINE_NETWORK_STACK signature permission like NetworkStackService does, so it allows the
+     * test UID to use other methods in this interface even though it would otherwise not have
+     * permission to.
+     *
+     * This method must be called as root and can only be used on debuggable builds. It only affects
+     * the NetworkStack until it is restarted.
+     * Callers should pass in -1 to reset after use.
+     *
+     * @param cb Callback that will be called with a status of 0 if the call succeeds. Calls without
+     *           sufficient permissions may be dropped without generating a callback.
+     */
+    oneway void allowTestUid(int uid, in INetworkStackStatusCallback cb);
 }
diff --git a/common/networkstackclient/src/android/net/InformationElementParcelable.aidl b/common/networkstackclient/src/android/net/InformationElementParcelable.aidl
index c70bf6f..57e82e8 100644
--- a/common/networkstackclient/src/android/net/InformationElementParcelable.aidl
+++ b/common/networkstackclient/src/android/net/InformationElementParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable InformationElementParcelable {
     int id;
     byte[] payload;
diff --git a/common/networkstackclient/src/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/src/android/net/InitialConfigurationParcelable.aidl
index 3fa88c3..56c942b 100644
--- a/common/networkstackclient/src/android/net/InitialConfigurationParcelable.aidl
+++ b/common/networkstackclient/src/android/net/InitialConfigurationParcelable.aidl
@@ -19,9 +19,10 @@
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 
+@JavaDerive(toString=true)
 parcelable InitialConfigurationParcelable {
     LinkAddress[] ipAddresses;
     IpPrefix[] directlyConnectedRoutes;
     String[] dnsServers;
     String gateway;
-}
\ No newline at end of file
+}
diff --git a/common/networkstackclient/src/android/net/IpMemoryStore.java b/common/networkstackclient/src/android/net/IpMemoryStore.java
new file mode 100644
index 0000000..f2c1d35
--- /dev/null
+++ b/common/networkstackclient/src/android/net/IpMemoryStore.java
@@ -0,0 +1,98 @@
+/*
+ * 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 android.net;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.networkstack.ModuleNetworkStackClient;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Manager class used to communicate with the ip memory store service in the network stack,
+ * which is running in a separate module.
+ * @hide
+*/
+public class IpMemoryStore extends IpMemoryStoreClient {
+    private static final String TAG = IpMemoryStore.class.getSimpleName();
+    @NonNull private final CompletableFuture<IIpMemoryStore> mService;
+    @NonNull private final AtomicReference<CompletableFuture<IIpMemoryStore>> mTailNode;
+
+    public IpMemoryStore(@NonNull final Context context) {
+        super(context);
+        mService = new CompletableFuture<>();
+        mTailNode = new AtomicReference<CompletableFuture<IIpMemoryStore>>(mService);
+        getModuleNetworkStackClient(context).fetchIpMemoryStore(
+                new IIpMemoryStoreCallbacks.Stub() {
+                    @Override
+                    public void onIpMemoryStoreFetched(@NonNull final IIpMemoryStore memoryStore) {
+                        mService.complete(memoryStore);
+                    }
+
+                    @Override
+                    public int getInterfaceVersion() {
+                        return this.VERSION;
+                    }
+
+                    @Override
+                    public String getInterfaceHash() {
+                        return this.HASH;
+                    }
+                });
+    }
+
+    /*
+     *  If the IpMemoryStore is ready, this function will run the request synchronously.
+     *  Otherwise, it will enqueue the requests for execution immediately after the
+     *  service becomes ready. The requests are guaranteed to be executed in the order
+     *  they are sumbitted.
+     */
+    @Override
+    protected void runWhenServiceReady(Consumer<IIpMemoryStore> cb) throws ExecutionException {
+        mTailNode.getAndUpdate(future -> future.handle((store, exception) -> {
+            if (exception != null) {
+                // this should never happens since we also catch the exception below
+                Log.wtf(TAG, "Error fetching IpMemoryStore", exception);
+                return store;
+            }
+
+            try {
+                cb.accept(store);
+            } catch (Exception e) {
+                Log.wtf(TAG, "Exception occurred: " + e.getMessage());
+            }
+            return store;
+        }));
+    }
+
+    @VisibleForTesting
+    protected ModuleNetworkStackClient getModuleNetworkStackClient(Context context) {
+        return ModuleNetworkStackClient.getInstance(context);
+    }
+
+    /** Gets an instance of the memory store */
+    @NonNull
+    public static IpMemoryStore getMemoryStore(final Context context) {
+        return new IpMemoryStore(context);
+    }
+}
diff --git a/common/networkstackclient/src/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/src/android/net/Layer2InformationParcelable.aidl
index a8eda0d..380f77e 100644
--- a/common/networkstackclient/src/android/net/Layer2InformationParcelable.aidl
+++ b/common/networkstackclient/src/android/net/Layer2InformationParcelable.aidl
@@ -18,6 +18,7 @@
 
 import android.net.MacAddress;
 
+@JavaDerive(toString=true)
 parcelable Layer2InformationParcelable {
     String l2Key;
     String cluster;
diff --git a/common/networkstackclient/src/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/src/android/net/Layer2PacketParcelable.aidl
index f49ade4..9f41cd0 100644
--- a/common/networkstackclient/src/android/net/Layer2PacketParcelable.aidl
+++ b/common/networkstackclient/src/android/net/Layer2PacketParcelable.aidl
@@ -18,6 +18,7 @@
 
 import android.net.MacAddress;
 
+@JavaDerive(toString=true)
 parcelable Layer2PacketParcelable {
     MacAddress dstMacAddress;
     byte[] payload;
diff --git a/common/networkstackclient/src/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/src/android/net/NattKeepalivePacketDataParcelable.aidl
index 6f006d4..bfd1af4 100644
--- a/common/networkstackclient/src/android/net/NattKeepalivePacketDataParcelable.aidl
+++ b/common/networkstackclient/src/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable NattKeepalivePacketDataParcelable {
     byte[] srcAddress;
     int srcPort;
diff --git a/common/networkstackclient/src/android/net/NetworkMonitorManager.java b/common/networkstackclient/src/android/net/NetworkMonitorManager.java
new file mode 100644
index 0000000..0f66981
--- /dev/null
+++ b/common/networkstackclient/src/android/net/NetworkMonitorManager.java
@@ -0,0 +1,203 @@
+/*
+ * 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 android.net;
+
+import android.annotation.Hide;
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A convenience wrapper for INetworkMonitor.
+ *
+ * Wraps INetworkMonitor calls, making them a bit more friendly to use. Currently handles:
+ * - Clearing calling identity
+ * - Ignoring RemoteExceptions
+ * - Converting to stable parcelables
+ *
+ * By design, all methods on INetworkMonitor are asynchronous oneway IPCs and are thus void. All the
+ * wrapper methods in this class return a boolean that callers can use to determine whether
+ * RemoteException was thrown.
+ */
+@Hide
+public class NetworkMonitorManager {
+
+    @NonNull private final INetworkMonitor mNetworkMonitor;
+    @NonNull private final String mTag;
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager,
+            @NonNull String tag) {
+        mNetworkMonitor = networkMonitorManager;
+        mTag = tag;
+    }
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager) {
+        this(networkMonitorManager, NetworkMonitorManager.class.getSimpleName());
+    }
+
+    private void log(String s, Throwable e) {
+        Log.e(mTag, s, e);
+    }
+
+    // CHECKSTYLE:OFF Generated code
+
+    public boolean start() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.start();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in start", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean launchCaptivePortalApp() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.launchCaptivePortalApp();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in launchCaptivePortalApp", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyCaptivePortalAppFinished(int response) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyCaptivePortalAppFinished(response);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyCaptivePortalAppFinished", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean setAcceptPartialConnectivity() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.setAcceptPartialConnectivity();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in setAcceptPartialConnectivity", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean forceReevaluation(int uid) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.forceReevaluation(uid);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in forceReevaluation", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyPrivateDnsChanged(PrivateDnsConfigParcel config) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyPrivateDnsChanged(config);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyPrivateDnsChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyDnsResponse(int returnCode) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyDnsResponse(returnCode);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyDnsResponse", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkConnected(lp, nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkConnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkDisconnected() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkDisconnected();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkDisconnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyLinkPropertiesChanged(LinkProperties lp) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyLinkPropertiesChanged(lp);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyLinkPropertiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkCapabilitiesChanged(NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkCapabilitiesChanged(nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkCapabilitiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // CHECKSTYLE:ON Generated code
+}
diff --git a/common/networkstackclient/src/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/src/android/net/NetworkTestResultParcelable.aidl
index 93efd73..c41ed87 100644
--- a/common/networkstackclient/src/android/net/NetworkTestResultParcelable.aidl
+++ b/common/networkstackclient/src/android/net/NetworkTestResultParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable NetworkTestResultParcelable {
     /**
      * Timestamp of the evaluation, as determined by to SystemClock.elapsedRealtime().
@@ -42,4 +43,4 @@
      * portal. Otherwise null.
      */
     String redirectUrl;
-}
\ No newline at end of file
+}
diff --git a/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
index b52fce6..97bb697 100644
--- a/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
+++ b/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable PrivateDnsConfigParcel {
     String hostname;
     String[] ips;
diff --git a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
index 2d5d349..0aeebcb 100644
--- a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
@@ -23,7 +23,11 @@
 import android.net.ScanResultInfoParcelable;
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 
+import java.util.List;
+
+@JavaDerive(toString=true)
 parcelable ProvisioningConfigurationParcelable {
     boolean enableIPv4;
     boolean enableIPv6;
@@ -40,4 +44,5 @@
     boolean enablePreconnection;
     @nullable ScanResultInfoParcelable scanResultInfo;
     @nullable Layer2InformationParcelable layer2Info;
+    @nullable List<DhcpOption> options;
 }
diff --git a/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl
index a19729b..8385c14 100644
--- a/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl
@@ -18,6 +18,7 @@
 
 import android.net.InformationElementParcelable;
 
+@JavaDerive(toString=true)
 parcelable ScanResultInfoParcelable {
     String ssid;
     String bssid;
diff --git a/common/networkstackclient/src/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/src/android/net/TcpKeepalivePacketDataParcelable.aidl
index e25168d..51701f9 100644
--- a/common/networkstackclient/src/android/net/TcpKeepalivePacketDataParcelable.aidl
+++ b/common/networkstackclient/src/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net;
 
+@JavaDerive(toString=true)
 parcelable TcpKeepalivePacketDataParcelable {
     byte[] srcAddress;
     int srcPort;
diff --git a/common/networkstackclient/src/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/src/android/net/dhcp/DhcpLeaseParcelable.aidl
index ba3390d..9a05ad5 100644
--- a/common/networkstackclient/src/android/net/dhcp/DhcpLeaseParcelable.aidl
+++ b/common/networkstackclient/src/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -16,6 +16,7 @@
 
 package android.net.dhcp;
 
+@JavaDerive(toString=true)
 parcelable DhcpLeaseParcelable {
     // Client ID of the lease; may be null.
     byte[] clientId;
@@ -29,4 +30,4 @@
     long expTime;
     // Hostname provided by the client, if any, or null.
     String hostname;
-}
\ No newline at end of file
+}
diff --git a/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
index 3adef77..7154c55 100644
--- a/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
+++ b/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -17,6 +17,7 @@
 
 package android.net.dhcp;
 
+@JavaDerive(toString=true)
 parcelable DhcpServingParamsParcel {
     int serverAddr;
     int serverAddrPrefixLength;
diff --git a/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java b/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java
new file mode 100644
index 0000000..b17fcaa
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.net.ip;
+
+import android.net.DhcpResultsParcelable;
+import android.net.Layer2PacketParcelable;
+import android.net.LinkProperties;
+
+import java.util.List;
+
+/**
+ * Callbacks for handling IpClient events.
+ *
+ * This is a convenience class to allow clients not to override all methods of IIpClientCallbacks,
+ * and avoid unparceling arguments.
+ * These methods are called asynchronously on a Binder thread, as IpClient lives in a different
+ * process.
+ * @hide
+ */
+public class IpClientCallbacks {
+
+    /**
+     * Callback called upon IpClient creation.
+     *
+     * @param ipClient The Binder token to communicate with IpClient.
+     */
+    public void onIpClientCreated(IIpClient ipClient) {}
+
+    /**
+     * Callback called prior to DHCP discovery/renewal.
+     *
+     * <p>In order to receive onPreDhcpAction(), call #withPreDhcpAction() when constructing a
+     * ProvisioningConfiguration.
+     *
+     * <p>Implementations of onPreDhcpAction() must call IpClient#completedPreDhcpAction() to
+     * indicate that DHCP is clear to proceed.
+      */
+    public void onPreDhcpAction() {}
+
+    /**
+     * Callback called after DHCP discovery/renewal.
+     */
+    public void onPostDhcpAction() {}
+
+    /**
+     * Callback called when new DHCP results are available.
+     *
+     * <p>This is purely advisory and not an indication of provisioning success or failure.  This is
+     * only here for callers that want to expose DHCPv4 results to other APIs
+     * (e.g., WifiInfo#setInetAddress).
+     *
+     * <p>DHCPv4 or static IPv4 configuration failure or success can be determined by whether or not
+     * the passed-in DhcpResults object is null.
+     */
+    public void onNewDhcpResults(DhcpResultsParcelable dhcpResults) {
+        // In general callbacks would not use a parcelable directly (DhcpResultsParcelable), and
+        // would use a wrapper instead, because of the lack of safety of stable parcelables. But
+        // there are already two classes in the tree for DHCP information: DhcpInfo and DhcpResults,
+        // and neither of them exposes an appropriate API (they are bags of mutable fields and can't
+        // be changed because they are public API and @UnsupportedAppUsage, being no better than the
+        // stable parcelable). Adding a third class would cost more than the gain considering that
+        // the only client of this callback is WiFi, which will end up converting the results to
+        // DhcpInfo anyway.
+    }
+
+    /**
+     * Indicates that provisioning was successful.
+     */
+    public void onProvisioningSuccess(LinkProperties newLp) {}
+
+    /**
+     * Indicates that provisioning failed.
+     */
+    public void onProvisioningFailure(LinkProperties newLp) {}
+
+    /**
+     * Invoked on LinkProperties changes.
+     */
+    public void onLinkPropertiesChange(LinkProperties newLp) {}
+
+    /**Called when the internal IpReachabilityMonitor (if enabled) has
+     * detected the loss of a critical number of required neighbors.
+     */
+    public void onReachabilityLost(String logMsg) {}
+
+    /**
+     * Called when the IpClient state machine terminates.
+     */
+    public void onQuit() {}
+
+    /**
+     * Called to indicate that a new APF program must be installed to filter incoming packets.
+     */
+    public void installPacketFilter(byte[] filter) {}
+
+    /**
+     * Called to indicate that the APF Program & data buffer must be read asynchronously from the
+     * wifi driver.
+     *
+     * <p>Due to Wifi HAL limitations, the current implementation only supports dumping the entire
+     * buffer. In response to this request, the driver returns the data buffer asynchronously
+     * by sending an IpClient#EVENT_READ_PACKET_FILTER_COMPLETE message.
+     */
+    public void startReadPacketFilter() {}
+
+    /**
+     * If multicast filtering cannot be accomplished with APF, this function will be called to
+     * actuate multicast filtering using another means.
+     */
+    public void setFallbackMulticastFilter(boolean enabled) {}
+
+    /**
+     * Enabled/disable Neighbor Discover offload functionality. This is called, for example,
+     * whenever 464xlat is being started or stopped.
+     */
+    public void setNeighborDiscoveryOffload(boolean enable) {}
+
+    /**
+     * Invoked on starting preconnection process.
+     */
+    public void onPreconnectionStart(List<Layer2PacketParcelable> packets) {}
+}
diff --git a/common/networkstackclient/src/android/net/ip/IpClientManager.java b/common/networkstackclient/src/android/net/ip/IpClientManager.java
new file mode 100644
index 0000000..b45405f
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ip/IpClientManager.java
@@ -0,0 +1,326 @@
+/*
+ * 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 android.net.ip;
+
+import android.annotation.Hide;
+import android.annotation.NonNull;
+import android.net.NattKeepalivePacketData;
+import android.net.ProxyInfo;
+import android.net.TcpKeepalivePacketData;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.shared.Layer2Information;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.util.KeepalivePacketDataUtil;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A convenience wrapper for IpClient.
+ *
+ * Wraps IIpClient calls, making them a bit more friendly to use. Currently handles:
+ * - Clearing calling identity
+ * - Ignoring RemoteExceptions
+ * - Converting to stable parcelables
+ *
+ * By design, all methods on IIpClient are asynchronous oneway IPCs and are thus void. All the
+ * wrapper methods in this class return a boolean that callers can use to determine whether
+ * RemoteException was thrown.
+ */
+@Hide
+public class IpClientManager {
+    @NonNull private final IIpClient mIpClient;
+    @NonNull private final String mTag;
+
+    public IpClientManager(@NonNull IIpClient ipClient, @NonNull String tag) {
+        mIpClient = ipClient;
+        mTag = tag;
+    }
+
+    public IpClientManager(@NonNull IIpClient ipClient) {
+        this(ipClient, IpClientManager.class.getSimpleName());
+    }
+
+    private void log(String s, Throwable e) {
+        Log.e(mTag, s, e);
+    }
+
+    /**
+     * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be
+     * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to
+     * proceed.
+     */
+    public boolean completedPreDhcpAction() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.completedPreDhcpAction();
+            return true;
+        } catch (RemoteException e) {
+            log("Error completing PreDhcpAction", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Confirm the provisioning configuration.
+     */
+    public boolean confirmConfiguration() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.confirmConfiguration();
+            return true;
+        } catch (RemoteException e) {
+            log("Error confirming IpClient configuration", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Indicate that packet filter read is complete.
+     */
+    public boolean readPacketFilterComplete(byte[] data) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.readPacketFilterComplete(data);
+            return true;
+        } catch (RemoteException e) {
+            log("Error notifying IpClient of packet filter read", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Shut down this IpClient instance altogether.
+     */
+    public boolean shutdown() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.shutdown();
+            return true;
+        } catch (RemoteException e) {
+            log("Error shutting down IpClient", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Start provisioning with the provided parameters.
+     */
+    public boolean startProvisioning(ProvisioningConfiguration prov) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.startProvisioning(prov.toStableParcelable());
+            return true;
+        } catch (RemoteException e) {
+            log("Error starting IpClient provisioning", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Stop this IpClient.
+     *
+     * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
+     */
+    public boolean stop() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.stop();
+            return true;
+        } catch (RemoteException e) {
+            log("Error stopping IpClient", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the TCP buffer sizes to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public boolean setTcpBufferSizes(String tcpBufferSizes) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setTcpBufferSizes(tcpBufferSizes);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting IpClient TCP buffer sizes", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the HTTP Proxy configuration to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public boolean setHttpProxy(ProxyInfo proxyInfo) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setHttpProxy(proxyInfo);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting IpClient proxy", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Enable or disable the multicast filter.  Attempts to use APF to accomplish the filtering,
+     * if not, Callback.setFallbackMulticastFilter() is called.
+     */
+    public boolean setMulticastFilter(boolean enabled) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setMulticastFilter(enabled);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting multicast filter", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Add a TCP keepalive packet filter before setting up keepalive offload.
+     */
+    public boolean addKeepalivePacketFilter(int slot, TcpKeepalivePacketData pkt) {
+        return addKeepalivePacketFilter(slot, KeepalivePacketDataUtil.toStableParcelable(pkt));
+    }
+
+    /**
+     * Add a TCP keepalive packet filter before setting up keepalive offload.
+     * @deprecated This method is for use on pre-S platforms where TcpKeepalivePacketData is not
+     *             system API. On newer platforms use
+     *             addKeepalivePacketFilter(int, TcpKeepalivePacketData) instead.
+     */
+    @Deprecated
+    public boolean addKeepalivePacketFilter(int slot, TcpKeepalivePacketDataParcelable pkt) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.addKeepalivePacketFilter(slot, pkt);
+            return true;
+        } catch (RemoteException e) {
+            log("Error adding Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Add a NAT-T keepalive packet filter before setting up keepalive offload.
+     */
+    public boolean addKeepalivePacketFilter(int slot, NattKeepalivePacketData pkt) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.addNattKeepalivePacketFilter(
+                    slot, KeepalivePacketDataUtil.toStableParcelable(pkt));
+            return true;
+        } catch (RemoteException e) {
+            log("Error adding NAT-T Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Remove a keepalive packet filter after stopping keepalive offload.
+     */
+    public boolean removeKeepalivePacketFilter(int slot) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.removeKeepalivePacketFilter(slot);
+            return true;
+        } catch (RemoteException e) {
+            log("Error removing Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the L2 key and group hint for storing info into the memory store.
+     */
+    public boolean setL2KeyAndGroupHint(String l2Key, String groupHint) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setL2KeyAndGroupHint(l2Key, groupHint);
+            return true;
+        } catch (RemoteException e) {
+            log("Failed setL2KeyAndGroupHint", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Notify IpClient that preconnection is complete and that the link is ready for use.
+     * The success parameter indicates whether the packets passed in by 'onPreconnectionStart'
+     * were successfully sent to the network or not.
+     */
+    public boolean notifyPreconnectionComplete(boolean success) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.notifyPreconnectionComplete(success);
+            return true;
+        } catch (RemoteException e) {
+            log("Error notifying IpClient Preconnection completed", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Update the bssid, L2 key and group hint layer2 information.
+     */
+    public boolean updateLayer2Information(Layer2Information info) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.updateLayer2Information(info.toStableParcelable());
+            return true;
+        } catch (RemoteException e) {
+            log("Error updating layer2 information", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/common/networkstackclient/src/android/net/ip/IpClientUtil.java b/common/networkstackclient/src/android/net/ip/IpClientUtil.java
new file mode 100644
index 0000000..1b55776
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ip/IpClientUtil.java
@@ -0,0 +1,204 @@
+/*
+ * 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 android.net.ip;
+
+import android.content.Context;
+import android.net.DhcpResultsParcelable;
+import android.net.Layer2PacketParcelable;
+import android.net.LinkProperties;
+import android.net.networkstack.ModuleNetworkStackClient;
+import android.os.ConditionVariable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+
+/**
+ * Utilities and wrappers to simplify communication with IpClient, which lives in the NetworkStack
+ * process.
+ *
+ * @hide
+ */
+public class IpClientUtil {
+    // TODO: remove with its callers
+    public static final String DUMP_ARG = "ipclient";
+
+    /**
+     * Subclass of {@link IpClientCallbacks} allowing clients to block until provisioning is
+     * complete with {@link WaitForProvisioningCallbacks#waitForProvisioning()}.
+     */
+    public static class WaitForProvisioningCallbacks extends IpClientCallbacks {
+        private final ConditionVariable mCV = new ConditionVariable();
+        private LinkProperties mCallbackLinkProperties;
+
+        /**
+         * Block until either {@link #onProvisioningSuccess(LinkProperties)} or
+         * {@link #onProvisioningFailure(LinkProperties)} is called.
+         */
+        public LinkProperties waitForProvisioning() {
+            mCV.block();
+            return mCallbackLinkProperties;
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            mCallbackLinkProperties = newLp;
+            mCV.open();
+        }
+
+        @Override
+        public void onProvisioningFailure(LinkProperties newLp) {
+            mCallbackLinkProperties = null;
+            mCV.open();
+        }
+    }
+
+    /**
+     * Create a new IpClient.
+     *
+     * <p>This is a convenience method to allow clients to use {@link IpClientCallbacks} instead of
+     * {@link IIpClientCallbacks}.
+     * @see {@link ModuleNetworkStackClient#makeIpClient(String, IIpClientCallbacks)}
+     */
+    public static void makeIpClient(Context context, String ifName, IpClientCallbacks callback) {
+        ModuleNetworkStackClient.getInstance(context)
+                .makeIpClient(ifName, new IpClientCallbacksProxy(callback));
+    }
+
+    /**
+     * Wrapper to relay calls from {@link IIpClientCallbacks} to {@link IpClientCallbacks}.
+     */
+    private static class IpClientCallbacksProxy extends IIpClientCallbacks.Stub {
+        protected final IpClientCallbacks mCb;
+
+        /**
+         * Create a new IpClientCallbacksProxy.
+         */
+        IpClientCallbacksProxy(IpClientCallbacks cb) {
+            mCb = cb;
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            mCb.onIpClientCreated(ipClient);
+        }
+
+        @Override
+        public void onPreDhcpAction() {
+            mCb.onPreDhcpAction();
+        }
+
+        @Override
+        public void onPostDhcpAction() {
+            mCb.onPostDhcpAction();
+        }
+
+        // This is purely advisory and not an indication of provisioning
+        // success or failure.  This is only here for callers that want to
+        // expose DHCPv4 results to other APIs (e.g., WifiInfo#setInetAddress).
+        // DHCPv4 or static IPv4 configuration failure or success can be
+        // determined by whether or not the passed-in DhcpResults object is
+        // null or not.
+        @Override
+        public void onNewDhcpResults(DhcpResultsParcelable dhcpResults) {
+            mCb.onNewDhcpResults(dhcpResults);
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            mCb.onProvisioningSuccess(newLp);
+        }
+        @Override
+        public void onProvisioningFailure(LinkProperties newLp) {
+            mCb.onProvisioningFailure(newLp);
+        }
+
+        // Invoked on LinkProperties changes.
+        @Override
+        public void onLinkPropertiesChange(LinkProperties newLp) {
+            mCb.onLinkPropertiesChange(newLp);
+        }
+
+        // Called when the internal IpReachabilityMonitor (if enabled) has
+        // detected the loss of a critical number of required neighbors.
+        @Override
+        public void onReachabilityLost(String logMsg) {
+            mCb.onReachabilityLost(logMsg);
+        }
+
+        // Called when the IpClient state machine terminates.
+        @Override
+        public void onQuit() {
+            mCb.onQuit();
+        }
+
+        // Install an APF program to filter incoming packets.
+        @Override
+        public void installPacketFilter(byte[] filter) {
+            mCb.installPacketFilter(filter);
+        }
+
+        // Asynchronously read back the APF program & data buffer from the wifi driver.
+        // Due to Wifi HAL limitations, the current implementation only supports dumping the entire
+        // buffer. In response to this request, the driver returns the data buffer asynchronously
+        // by sending an IpClient#EVENT_READ_PACKET_FILTER_COMPLETE message.
+        @Override
+        public void startReadPacketFilter() {
+            mCb.startReadPacketFilter();
+        }
+
+        // If multicast filtering cannot be accomplished with APF, this function will be called to
+        // actuate multicast filtering using another means.
+        @Override
+        public void setFallbackMulticastFilter(boolean enabled) {
+            mCb.setFallbackMulticastFilter(enabled);
+        }
+
+        // Enabled/disable Neighbor Discover offload functionality. This is
+        // called, for example, whenever 464xlat is being started or stopped.
+        @Override
+        public void setNeighborDiscoveryOffload(boolean enable) {
+            mCb.setNeighborDiscoveryOffload(enable);
+        }
+
+        // Invoked on starting preconnection process.
+        @Override
+        public void onPreconnectionStart(List<Layer2PacketParcelable> packets) {
+            mCb.onPreconnectionStart(packets);
+        }
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() {
+            return this.HASH;
+        }
+    }
+
+    /**
+     * Dump logs for the specified IpClient.
+     * TODO: remove callers and delete
+     */
+    public static void dumpIpClient(
+            IIpClient connector, FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("IpClient logs have moved to dumpsys network_stack");
+    }
+}
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributes.java b/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributes.java
index 2e444fe..85bdcdc 100644
--- a/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributes.java
+++ b/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributes.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -83,6 +84,13 @@
     public final Integer mtu;
     private static final float WEIGHT_MTU = 50.0f;
 
+    // IPv6 provisioning quirk info about this network, if applicable.
+    @Nullable
+    public final IPv6ProvisioningLossQuirk ipv6ProvisioningLossQuirk;
+    // quirk information doesn't imply any correlation between "the same quirk detection count and
+    // expiry" and "the same L3 network".
+    private static final float WEIGHT_V6PROVLOSSQUIRK = 0.0f;
+
     // The sum of all weights in this class. Tests ensure that this stays equal to the total of
     // all weights.
     /** @hide */
@@ -91,7 +99,8 @@
             + WEIGHT_ASSIGNEDV4ADDREXPIRY
             + WEIGHT_CLUSTER
             + WEIGHT_DNSADDRESSES
-            + WEIGHT_MTU;
+            + WEIGHT_MTU
+            + WEIGHT_V6PROVLOSSQUIRK;
 
     /** @hide */
     @VisibleForTesting
@@ -100,7 +109,8 @@
             @Nullable final Long assignedV4AddressExpiry,
             @Nullable final String cluster,
             @Nullable final List<InetAddress> dnsAddresses,
-            @Nullable final Integer mtu) {
+            @Nullable final Integer mtu,
+            @Nullable final IPv6ProvisioningLossQuirk ipv6ProvisioningLossQuirk) {
         if (mtu != null && mtu < 0) throw new IllegalArgumentException("MTU can't be negative");
         if (assignedV4AddressExpiry != null && assignedV4AddressExpiry <= 0) {
             throw new IllegalArgumentException("lease expiry can't be negative or zero");
@@ -111,6 +121,7 @@
         this.dnsAddresses = null == dnsAddresses ? null :
                 Collections.unmodifiableList(new ArrayList<>(dnsAddresses));
         this.mtu = mtu;
+        this.ipv6ProvisioningLossQuirk = ipv6ProvisioningLossQuirk;
     }
 
     @VisibleForTesting
@@ -122,7 +133,9 @@
                         ? parcelable.assignedV4AddressExpiry : null,
                 parcelable.cluster,
                 blobArrayToInetAddressList(parcelable.dnsAddresses),
-                parcelable.mtu >= 0 ? parcelable.mtu : null);
+                parcelable.mtu >= 0 ? parcelable.mtu : null,
+                IPv6ProvisioningLossQuirk.fromStableParcelable(
+                        parcelable.ipv6ProvisioningLossQuirk));
     }
 
     @Nullable
@@ -171,6 +184,8 @@
         parcelable.cluster = cluster;
         parcelable.dnsAddresses = inetAddressListToBlobArray(dnsAddresses);
         parcelable.mtu = (null == mtu) ? -1 : mtu;
+        parcelable.ipv6ProvisioningLossQuirk = (null == ipv6ProvisioningLossQuirk)
+                ? null : ipv6ProvisioningLossQuirk.toStableParcelable();
         return parcelable;
     }
 
@@ -184,13 +199,16 @@
 
     /** @hide */
     public float getNetworkGroupSamenessConfidence(@NonNull final NetworkAttributes o) {
+        // TODO: Remove the useless comparison for members which are associated with 0 weight.
         final float samenessScore =
                 samenessContribution(WEIGHT_ASSIGNEDV4ADDR, assignedV4Address, o.assignedV4Address)
                 + samenessContribution(WEIGHT_ASSIGNEDV4ADDREXPIRY, assignedV4AddressExpiry,
                       o.assignedV4AddressExpiry)
                 + samenessContribution(WEIGHT_CLUSTER, cluster, o.cluster)
                 + samenessContribution(WEIGHT_DNSADDRESSES, dnsAddresses, o.dnsAddresses)
-                + samenessContribution(WEIGHT_MTU, mtu, o.mtu);
+                + samenessContribution(WEIGHT_MTU, mtu, o.mtu)
+                + samenessContribution(WEIGHT_V6PROVLOSSQUIRK, ipv6ProvisioningLossQuirk,
+                      o.ipv6ProvisioningLossQuirk);
         // The minimum is 0, the max is TOTAL_WEIGHT and should be represented by 1.0, and
         // TOTAL_WEIGHT_CUTOFF should represent 0.5, but there is no requirement that
         // TOTAL_WEIGHT_CUTOFF would be half of TOTAL_WEIGHT (indeed, it should not be).
@@ -216,6 +234,8 @@
         private List<InetAddress> mDnsAddresses;
         @Nullable
         private Integer mMtu;
+        @Nullable
+        private IPv6ProvisioningLossQuirk mIpv6ProvLossQuirk;
 
         /**
          * Constructs a new Builder.
@@ -231,6 +251,7 @@
             mCluster = attributes.cluster;
             mDnsAddresses = new ArrayList<>(attributes.dnsAddresses);
             mMtu = attributes.mtu;
+            mIpv6ProvLossQuirk = attributes.ipv6ProvisioningLossQuirk;
         }
 
         /**
@@ -298,19 +319,30 @@
         }
 
         /**
+         * Set the IPv6 Provisioning Loss Quirk information.
+         * @param quirk The IPv6 Provisioning Loss Quirk.
+         * @return This builder.
+         */
+        public Builder setIpv6ProvLossQuirk(@Nullable final IPv6ProvisioningLossQuirk quirk) {
+            mIpv6ProvLossQuirk = quirk;
+            return this;
+        }
+
+        /**
          * Return the built NetworkAttributes object.
          * @return The built NetworkAttributes object.
          */
         public NetworkAttributes build() {
             return new NetworkAttributes(mAssignedAddress, mAssignedAddressExpiry,
-                    mCluster, mDnsAddresses, mMtu);
+                    mCluster, mDnsAddresses, mMtu, mIpv6ProvLossQuirk);
         }
     }
 
     /** @hide */
     public boolean isEmpty() {
         return (null == assignedV4Address) && (null == assignedV4AddressExpiry)
-                && (null == cluster) && (null == dnsAddresses) && (null == mtu);
+                && (null == cluster) && (null == dnsAddresses) && (null == mtu)
+                && (null == ipv6ProvisioningLossQuirk);
     }
 
     @Override
@@ -321,13 +353,14 @@
                 && Objects.equals(assignedV4AddressExpiry, other.assignedV4AddressExpiry)
                 && Objects.equals(cluster, other.cluster)
                 && Objects.equals(dnsAddresses, other.dnsAddresses)
-                && Objects.equals(mtu, other.mtu);
+                && Objects.equals(mtu, other.mtu)
+                && Objects.equals(ipv6ProvisioningLossQuirk, other.ipv6ProvisioningLossQuirk);
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(assignedV4Address, assignedV4AddressExpiry,
-                cluster, dnsAddresses, mtu);
+                cluster, dnsAddresses, mtu, ipv6ProvisioningLossQuirk);
     }
 
     /** Pretty print */
@@ -374,6 +407,14 @@
             nullFields.add("mtu");
         }
 
+        if (null != ipv6ProvisioningLossQuirk) {
+            resultJoiner.add("ipv6ProvisioningLossQuirk : [");
+            resultJoiner.add(ipv6ProvisioningLossQuirk.toString());
+            resultJoiner.add("]");
+        } else {
+            nullFields.add("ipv6ProvisioningLossQuirk");
+        }
+
         if (!nullFields.isEmpty()) {
             resultJoiner.add("; Null fields : [");
             for (final String field : nullFields) {
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
index b710427..98f05ec 100644
--- a/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -19,6 +19,7 @@
 // Blob[] is used to represent an array of byte[], as structured AIDL does not support arrays
 // of arrays.
 import android.net.ipmemorystore.Blob;
+import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable;
 
 /**
  * An object to represent attributes of a single L2 network entry.
@@ -28,10 +29,12 @@
  * to convert the richer types to the parcelable types and back.
  * @hide
  */
+@JavaDerive(toString=true)
 parcelable NetworkAttributesParcelable {
     byte[] assignedV4Address;
     long assignedV4AddressExpiry;
     String cluster;
     Blob[] dnsAddresses;
     int mtu;
+    @nullable IPv6ProvisioningLossQuirkParcelable ipv6ProvisioningLossQuirk;
 }
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/src/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
index 7196699..28e563a 100644
--- a/common/networkstackclient/src/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -17,6 +17,7 @@
 package android.net.ipmemorystore;
 
 /** {@hide} */
+@JavaDerive(toString=true)
 parcelable SameL3NetworkResponseParcelable {
     String l2Key1;
     String l2Key2;
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/src/android/net/ipmemorystore/StatusParcelable.aidl
index fb36ef4..e693bcd 100644
--- a/common/networkstackclient/src/android/net/ipmemorystore/StatusParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ipmemorystore/StatusParcelable.aidl
@@ -17,6 +17,7 @@
 package android.net.ipmemorystore;
 
 /** {@hide} */
+@JavaDerive(toString=true)
 parcelable StatusParcelable {
   int resultCode;
 }
diff --git a/common/networkstackclient/src/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/src/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..f5a3328
--- /dev/null
+++ b/common/networkstackclient/src/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,29 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+
+package android.net.networkstack.aidl.dhcp;
+
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+    /** The type of the option. */
+    byte type;
+
+    /**
+     * The raw bytes of the DHCP option. When requesting a DHCP option, a null value
+     * indicates that the option should appear in the PRL and not in the options section.
+     */
+    @nullable byte[] value;
+}
diff --git a/common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirk.java b/common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirk.java
new file mode 100644
index 0000000..b4e66f0
--- /dev/null
+++ b/common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirk.java
@@ -0,0 +1,79 @@
+/*
+ * 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 android.net.networkstack.aidl.quirks;
+
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * An object representing the quirk information results in the IPv6 provisioning loss on the given
+ * network. Parcels as a IPv6ProvisioningLossQuirkParcelable object.
+ * @hide
+ */
+public final class IPv6ProvisioningLossQuirk {
+    public final int mDetectionCount;
+    public final long mQuirkExpiry;
+
+    /**
+     * Create an instance of {@link IPv6ProvisioningLossQuirk} with the specified members.
+     */
+    public IPv6ProvisioningLossQuirk(final int count, final long expiry) {
+        mDetectionCount = count;
+        mQuirkExpiry = expiry;
+    }
+
+    /**
+     * Convert IPv6ProvisioningLossQuirk to a {@link IPv6ProvisioningLossQuirkParcelable}.
+     */
+    public IPv6ProvisioningLossQuirkParcelable toStableParcelable() {
+        final IPv6ProvisioningLossQuirkParcelable p = new IPv6ProvisioningLossQuirkParcelable();
+        p.detectionCount = mDetectionCount;
+        p.quirkExpiry = mQuirkExpiry;
+        return p;
+    }
+
+    /**
+     * Create an instance of {@link IPv6ProvisioningLossQuirk} based on the contents of the
+     * specified {@link IPv6ProvisioningLossQuirkParcelable}.
+     */
+    public static IPv6ProvisioningLossQuirk fromStableParcelable(
+            @Nullable final IPv6ProvisioningLossQuirkParcelable p) {
+        if (p == null) return null;
+        return new IPv6ProvisioningLossQuirk(p.detectionCount, p.quirkExpiry);
+    }
+
+    @Override
+    public boolean equals(@Nullable final Object obj) {
+        if (null == obj || getClass() != obj.getClass()) return false;
+        final IPv6ProvisioningLossQuirk other = (IPv6ProvisioningLossQuirk) obj;
+        return mDetectionCount == other.mDetectionCount && mQuirkExpiry == other.mQuirkExpiry;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDetectionCount, mQuirkExpiry);
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+        str.append("detection count: ").append(mDetectionCount);
+        str.append(", quirk expiry: ").append(mQuirkExpiry);
+        return str.toString();
+    }
+}
diff --git a/tests/lib/multivariant/com/android/testutils/SkipPresubmit.kt b/common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
similarity index 70%
rename from tests/lib/multivariant/com/android/testutils/SkipPresubmit.kt
rename to common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
index 69ed048..1ee478c 100644
--- a/tests/lib/multivariant/com/android/testutils/SkipPresubmit.kt
+++ b/common/networkstackclient/src/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.testutils
+package android.net.networkstack.aidl.quirks;
 
-/**
- * Skip the test in presubmit runs for the reason specified in [reason].
- *
- * This annotation is typically used to document hardware or test bench limitations.
- */
-annotation class SkipPresubmit(val reason: String)
\ No newline at end of file
+@JavaDerive(toString=true)
+parcelable IPv6ProvisioningLossQuirkParcelable {
+    int detectionCount;
+    long quirkExpiry;
+}
diff --git a/common/moduleutils/src/android/net/shared/InitialConfiguration.java b/common/networkstackclient/src/android/net/shared/InitialConfiguration.java
similarity index 100%
rename from common/moduleutils/src/android/net/shared/InitialConfiguration.java
rename to common/networkstackclient/src/android/net/shared/InitialConfiguration.java
diff --git a/common/moduleutils/src/android/net/shared/Layer2Information.java b/common/networkstackclient/src/android/net/shared/Layer2Information.java
similarity index 100%
rename from common/moduleutils/src/android/net/shared/Layer2Information.java
rename to common/networkstackclient/src/android/net/shared/Layer2Information.java
diff --git a/common/moduleutils/src/android/net/shared/ParcelableUtil.java b/common/networkstackclient/src/android/net/shared/ParcelableUtil.java
similarity index 100%
rename from common/moduleutils/src/android/net/shared/ParcelableUtil.java
rename to common/networkstackclient/src/android/net/shared/ParcelableUtil.java
diff --git a/common/moduleutils/src/android/net/shared/PrivateDnsConfig.java b/common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java
similarity index 100%
rename from common/moduleutils/src/android/net/shared/PrivateDnsConfig.java
rename to common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java
diff --git a/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java b/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
similarity index 87%
rename from common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java
rename to common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
index 5f5699c..6ee9b73 100644
--- a/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java
+++ b/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
@@ -21,7 +21,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.net.INetd;
 import android.net.InformationElementParcelable;
 import android.net.Network;
 import android.net.ProvisioningConfigurationParcelable;
@@ -29,6 +28,7 @@
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
 import android.net.ip.IIpClient;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.util.Log;
 
 import java.nio.BufferUnderflowException;
@@ -75,6 +75,13 @@
     // allowing for 10% jitter.
     private static final int DEFAULT_TIMEOUT_MS = 18 * 1000;
 
+    // TODO: These cannot be imported from INetd.aidl, because networkstack-client cannot depend on
+    // INetd, as there are users of IpClient that depend on INetd directly (potentially at a
+    // different version, which is not allowed by the build system).
+    // Find a better way to express these constants.
+    public static final int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+    public static final int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+
     /**
      * Builder to create a {@link ProvisioningConfiguration}.
      */
@@ -179,7 +186,7 @@
          * Specify that IPv6 address generation should use a random MAC address.
          */
         public Builder withRandomMacAddress() {
-            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64;
+            mConfig.mIPv6AddrGenMode = IPV6_ADDR_GEN_MODE_EUI64;
             return this;
         }
 
@@ -187,7 +194,7 @@
          * Specify that IPv6 address generation should use a stable MAC address.
          */
         public Builder withStableMacAddress() {
-            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+            mConfig.mIPv6AddrGenMode = IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
             return this;
         }
 
@@ -227,6 +234,17 @@
         }
 
         /**
+         * Specify the customized DHCP options to be put in the PRL or in the DHCP packet. Options
+         * with null value will be put in the PRL.
+         *
+         * @param: options customized DHCP option stable parcelable list.
+         */
+        public Builder withDhcpOptions(List<DhcpOption> options) {
+            mConfig.mDhcpOptions = options;
+            return this;
+        }
+
+        /**
          * Build the configuration using previously specified parameters.
          */
         public ProvisioningConfiguration build() {
@@ -425,11 +443,12 @@
     public StaticIpConfiguration mStaticIpConfig;
     public ApfCapabilities mApfCapabilities;
     public int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
-    public int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+    public int mIPv6AddrGenMode = IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
     public Network mNetwork = null;
     public String mDisplayName = null;
     public ScanResultInfo mScanResultInfo;
     public Layer2Information mLayer2Info;
+    public List<DhcpOption> mDhcpOptions;
 
     public ProvisioningConfiguration() {} // used by Builder
 
@@ -451,6 +470,7 @@
         mDisplayName = other.mDisplayName;
         mScanResultInfo = other.mScanResultInfo;
         mLayer2Info = other.mLayer2Info;
+        mDhcpOptions = other.mDhcpOptions;
     }
 
     /**
@@ -464,8 +484,8 @@
         p.usingMultinetworkPolicyTracker = mUsingMultinetworkPolicyTracker;
         p.usingIpReachabilityMonitor = mUsingIpReachabilityMonitor;
         p.requestedPreDhcpActionMs = mRequestedPreDhcpActionMs;
-        p.initialConfig = mInitialConfig == null ? null : mInitialConfig.toStableParcelable();
-        p.staticIpConfig = mStaticIpConfig == null
+        p.initialConfig = (mInitialConfig == null) ? null : mInitialConfig.toStableParcelable();
+        p.staticIpConfig = (mStaticIpConfig == null)
                 ? null
                 : new StaticIpConfiguration(mStaticIpConfig);
         p.apfCapabilities = mApfCapabilities; // ApfCapabilities is immutable
@@ -473,8 +493,9 @@
         p.ipv6AddrGenMode = mIPv6AddrGenMode;
         p.network = mNetwork;
         p.displayName = mDisplayName;
-        p.scanResultInfo = mScanResultInfo == null ? null : mScanResultInfo.toStableParcelable();
-        p.layer2Info = mLayer2Info == null ? null : mLayer2Info.toStableParcelable();
+        p.scanResultInfo = (mScanResultInfo == null) ? null : mScanResultInfo.toStableParcelable();
+        p.layer2Info = (mLayer2Info == null) ? null : mLayer2Info.toStableParcelable();
+        p.options = (mDhcpOptions == null) ? null : new ArrayList<>(mDhcpOptions);
         return p;
     }
 
@@ -492,7 +513,7 @@
         config.mUsingIpReachabilityMonitor = p.usingIpReachabilityMonitor;
         config.mRequestedPreDhcpActionMs = p.requestedPreDhcpActionMs;
         config.mInitialConfig = InitialConfiguration.fromStableParcelable(p.initialConfig);
-        config.mStaticIpConfig = p.staticIpConfig == null
+        config.mStaticIpConfig = (p.staticIpConfig == null)
                 ? null
                 : new StaticIpConfiguration(p.staticIpConfig);
         config.mApfCapabilities = p.apfCapabilities; // ApfCapabilities is immutable
@@ -502,6 +523,7 @@
         config.mDisplayName = p.displayName;
         config.mScanResultInfo = ScanResultInfo.fromStableParcelable(p.scanResultInfo);
         config.mLayer2Info = Layer2Information.fromStableParcelable(p.layer2Info);
+        config.mDhcpOptions = (p.options == null) ? null : new ArrayList<>(p.options);
         return config;
     }
 
@@ -523,9 +545,32 @@
                 .add("mDisplayName: " + mDisplayName)
                 .add("mScanResultInfo: " + mScanResultInfo)
                 .add("mLayer2Info: " + mLayer2Info)
+                .add("mDhcpOptions: " + mDhcpOptions)
                 .toString();
     }
 
+    // TODO: mark DhcpOption stable parcelable with @JavaDerive(equals=true, toString=true)
+    // and @JavaOnlyImmutable.
+    private static boolean dhcpOptionEquals(@Nullable DhcpOption obj1, @Nullable DhcpOption obj2) {
+        if (obj1 == obj2) return true;
+        if (obj1 == null || obj2 == null) return false;
+        return obj1.type == obj2.type && Arrays.equals(obj1.value, obj2.value);
+    }
+
+    // TODO: use Objects.equals(List<DhcpOption>, List<DhcpOption>) method instead once
+    // auto-generated equals() method of stable parcelable is supported in mainline-prod.
+    private static boolean dhcpOptionListEquals(@Nullable List<DhcpOption> l1,
+            @Nullable List<DhcpOption> l2) {
+        if (l1 == l2) return true;
+        if (l1 == null || l2 == null) return false;
+        if (l1.size() != l2.size()) return false;
+
+        for (int i = 0; i < l1.size(); i++) {
+            if (!dhcpOptionEquals(l1.get(i), l2.get(i))) return false;
+        }
+        return true;
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (!(obj instanceof ProvisioningConfiguration)) return false;
@@ -544,7 +589,8 @@
                 && Objects.equals(mNetwork, other.mNetwork)
                 && Objects.equals(mDisplayName, other.mDisplayName)
                 && Objects.equals(mScanResultInfo, other.mScanResultInfo)
-                && Objects.equals(mLayer2Info, other.mLayer2Info);
+                && Objects.equals(mLayer2Info, other.mLayer2Info)
+                && dhcpOptionListEquals(mDhcpOptions, other.mDhcpOptions);
     }
 
     public boolean isValid() {
diff --git a/common/networkstackclient/src/android/net/util/KeepalivePacketDataUtil.java b/common/networkstackclient/src/android/net/util/KeepalivePacketDataUtil.java
new file mode 100644
index 0000000..5666985
--- /dev/null
+++ b/common/networkstackclient/src/android/net/util/KeepalivePacketDataUtil.java
@@ -0,0 +1,223 @@
+/*
+ * 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 android.net.util;
+
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InvalidPacketException;
+import android.net.KeepalivePacketData;
+import android.net.NattKeepalivePacketData;
+import android.net.NattKeepalivePacketDataParcelable;
+import android.net.TcpKeepalivePacketData;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.os.Build;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.IpUtils;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Utility class to convert to/from keepalive data parcelables.
+ *
+ * TODO: move to networkstack-client library when it is moved to frameworks/libs/net.
+ * This class cannot go into other shared libraries as it depends on NetworkStack AIDLs.
+ * @hide
+ */
+public final class KeepalivePacketDataUtil {
+    private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int IPV6_HEADER_LENGTH = 40;
+    private static final int TCP_HEADER_LENGTH = 20;
+
+    private static final String TAG = KeepalivePacketDataUtil.class.getSimpleName();
+
+    /**
+     * Convert a NattKeepalivePacketData to a NattKeepalivePacketDataParcelable.
+     */
+    @NonNull
+    public static NattKeepalivePacketDataParcelable toStableParcelable(
+            @NonNull NattKeepalivePacketData pkt) {
+        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
+        final InetAddress srcAddress = pkt.getSrcAddress();
+        final InetAddress dstAddress = pkt.getDstAddress();
+        parcel.srcAddress = srcAddress.getAddress();
+        parcel.srcPort = pkt.getSrcPort();
+        parcel.dstAddress = dstAddress.getAddress();
+        parcel.dstPort = pkt.getDstPort();
+        return parcel;
+    }
+
+    /**
+     * Convert a TcpKeepalivePacketData to a TcpKeepalivePacketDataParcelable.
+     */
+    @NonNull
+    public static TcpKeepalivePacketDataParcelable toStableParcelable(
+            @NonNull TcpKeepalivePacketData pkt) {
+        final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
+        final InetAddress srcAddress = pkt.getSrcAddress();
+        final InetAddress dstAddress = pkt.getDstAddress();
+        parcel.srcAddress = srcAddress.getAddress();
+        parcel.srcPort = pkt.getSrcPort();
+        parcel.dstAddress = dstAddress.getAddress();
+        parcel.dstPort = pkt.getDstPort();
+        parcel.seq = pkt.getTcpSeq();
+        parcel.ack = pkt.getTcpAck();
+        parcel.rcvWnd = pkt.getTcpWindow();
+        parcel.rcvWndScale = pkt.getTcpWindowScale();
+        parcel.tos = pkt.getIpTos();
+        parcel.ttl = pkt.getIpTtl();
+        return parcel;
+    }
+
+    /**
+     * Factory method to create tcp keepalive packet structure.
+     * @hide
+     */
+    public static TcpKeepalivePacketData fromStableParcelable(
+            TcpKeepalivePacketDataParcelable tcpDetails) throws InvalidPacketException {
+        final byte[] packet;
+        try {
+            if ((tcpDetails.srcAddress != null) && (tcpDetails.dstAddress != null)
+                    && (tcpDetails.srcAddress.length == 4 /* V4 IP length */)
+                    && (tcpDetails.dstAddress.length == 4 /* V4 IP length */)) {
+                packet = buildV4Packet(tcpDetails);
+            } else {
+                // TODO: support ipv6
+                throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+            }
+            return new TcpKeepalivePacketData(
+                    InetAddress.getByAddress(tcpDetails.srcAddress),
+                    tcpDetails.srcPort,
+                    InetAddress.getByAddress(tcpDetails.dstAddress),
+                    tcpDetails.dstPort,
+                    packet,
+                    tcpDetails.seq, tcpDetails.ack, tcpDetails.rcvWnd, tcpDetails.rcvWndScale,
+                    tcpDetails.tos, tcpDetails.ttl);
+        } catch (UnknownHostException e) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+    }
+
+    /**
+     * Build ipv4 tcp keepalive packet, not including the link-layer header.
+     */
+    // TODO : if this code is ever moved to the network stack, factorize constants with the ones
+    // over there.
+    private static byte[] buildV4Packet(TcpKeepalivePacketDataParcelable tcpDetails) {
+        final int length = IPV4_HEADER_LENGTH + TCP_HEADER_LENGTH;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.put((byte) 0x45);                       // IP version and IHL
+        buf.put((byte) tcpDetails.tos);             // TOS
+        buf.putShort((short) length);
+        buf.putInt(0x00004000);                     // ID, flags=DF, offset
+        buf.put((byte) tcpDetails.ttl);             // TTL
+        buf.put((byte) OsConstants.IPPROTO_TCP);
+        final int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // IP checksum
+        buf.put(tcpDetails.srcAddress);
+        buf.put(tcpDetails.dstAddress);
+        buf.putShort((short) tcpDetails.srcPort);
+        buf.putShort((short) tcpDetails.dstPort);
+        buf.putInt(tcpDetails.seq);                 // Sequence Number
+        buf.putInt(tcpDetails.ack);                 // ACK
+        buf.putShort((short) 0x5010);               // TCP length=5, flags=ACK
+        buf.putShort((short) (tcpDetails.rcvWnd >> tcpDetails.rcvWndScale));   // Window size
+        final int tcpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // TCP checksum
+        // URG is not set therefore the urgent pointer is zero.
+        buf.putShort((short) 0);                    // Urgent pointer
+
+        buf.putShort(ipChecksumOffset, com.android.net.module.util.IpUtils.ipChecksum(buf, 0));
+        buf.putShort(tcpChecksumOffset, IpUtils.tcpChecksum(
+                buf, 0, IPV4_HEADER_LENGTH, TCP_HEADER_LENGTH));
+
+        return buf.array();
+    }
+
+    // TODO: add buildV6Packet.
+
+    /**
+     * Get a {@link TcpKeepalivePacketDataParcelable} from {@link KeepalivePacketData}, if the
+     * generic class actually contains TCP keepalive data.
+     *
+     * @deprecated This method is used on R platforms where android.net.TcpKeepalivePacketData was
+     * not yet system API. Newer platforms should use android.net.TcpKeepalivePacketData directly.
+     *
+     * @param data A {@link KeepalivePacketData} that may contain TCP keepalive data.
+     * @return A parcelable containing TCP keepalive data, or null if the input data does not
+     *         contain TCP keepalive data.
+     */
+    @Deprecated
+    @SuppressWarnings("AndroidFrameworkCompatChange") // API version check used to Log.wtf
+    @Nullable
+    public static TcpKeepalivePacketDataParcelable parseTcpKeepalivePacketData(
+            @Nullable KeepalivePacketData data) {
+        if (data == null) return null;
+
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
+            Log.wtf(TAG, "parseTcpKeepalivePacketData should not be used after R, use "
+                    + "TcpKeepalivePacketData instead.");
+        }
+
+        // Reconstruct TcpKeepalivePacketData from the packet contained in KeepalivePacketData
+        final ByteBuffer buffer = ByteBuffer.wrap(data.getPacket());
+        buffer.order(ByteOrder.BIG_ENDIAN);
+
+        // Most of the fields are accessible from the KeepalivePacketData superclass: instead of
+        // using Struct to parse everything, just extract the extra fields necessary for
+        // TcpKeepalivePacketData.
+        final int tcpSeq;
+        final int tcpAck;
+        final int wndSize;
+        final int ipTos;
+        final int ttl;
+        try {
+            // This only support IPv4, because TcpKeepalivePacketData only supports IPv4 for R and
+            // below, and this method should not be used on newer platforms.
+            tcpSeq = buffer.getInt(IPV4_HEADER_LENGTH + 4);
+            tcpAck = buffer.getInt(IPV4_HEADER_LENGTH + 8);
+            wndSize = buffer.getShort(IPV4_HEADER_LENGTH + 14);
+            ipTos = buffer.get(1);
+            ttl = buffer.get(8);
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        }
+
+        final TcpKeepalivePacketDataParcelable p = new TcpKeepalivePacketDataParcelable();
+        p.srcAddress = data.getSrcAddress().getAddress();
+        p.srcPort = data.getSrcPort();
+        p.dstAddress = data.getDstAddress().getAddress();
+        p.dstPort = data.getDstPort();
+        p.seq = tcpSeq;
+        p.ack = tcpAck;
+        // TcpKeepalivePacketData could actually use non-zero wndScale, but this does not affect
+        // actual functionality as generated packets will be the same (no wndScale option added)
+        p.rcvWnd = wndSize;
+        p.rcvWndScale = 0;
+        p.tos = ipTos;
+        p.ttl = ttl;
+        return p;
+    }
+}
diff --git a/jarjar-rules-shared.txt b/jarjar-rules-shared.txt
index 048c976..e8c9c19 100644
--- a/jarjar-rules-shared.txt
+++ b/jarjar-rules-shared.txt
@@ -1,6 +1,3 @@
-# Classes from net-utils-framework-common
-rule com.android.net.module.util.** com.android.networkstack.util.@1
-
 rule com.android.internal.util.** android.net.networkstack.util.@1
 rule com.google.protobuf.** com.android.networkstack.protobuf.@1
 
@@ -12,3 +9,8 @@
 rule android.net.DhcpResultsParcelable* @0
 rule android.net.DhcpResults* android.net.networkstack.DhcpResults@1
 rule android.util.LocalLog* android.net.networkstack.util.LocalLog@1
+
+rule android.util.IndentingPrintWriter* android.net.networkstack.util.AndroidUtilIndentingPrintWriter@1
+
+# Classes from modules-utils-build_system
+rule com.android.modules.utils.build.** com.android.networkstack.utils.build.@1
\ No newline at end of file
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp
index f2ba575..6408856 100644
--- a/jni/network_stack_utils_jni.cpp
+++ b/jni/network_stack_utils_jni.cpp
@@ -16,6 +16,7 @@
 
 #define LOG_TAG "NetworkStackUtils-JNI"
 
+#include <dlfcn.h>
 #include <errno.h>
 #include <jni.h>
 #include <linux/filter.h>
@@ -27,10 +28,13 @@
 #include <netinet/ip6.h>
 #include <netinet/udp.h>
 #include <stdlib.h>
+#include <sys/system_properties.h>
 
 #include <string>
 
 #include <nativehelper/JNIHelp.h>
+#include <netjniutils/netjniutils.h>
+
 #include <android/log.h>
 
 namespace android {
@@ -82,7 +86,7 @@
     env->GetStringUTFRegion(ifname, 0, ifLen, req.arp_dev);
 
     req.arp_flags = ATF_COM;  // Completed entry (ha valid)
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (fd < 0) {
         jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
         return;
@@ -120,7 +124,7 @@
         filter_code,
     };
 
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
                 "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
@@ -153,7 +157,7 @@
         filter_code,
     };
 
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
                 "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
@@ -228,7 +232,7 @@
         filter_code,
     };
 
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
                 "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
diff --git a/lint-baseline-api-30-shims.xml b/lint-baseline-api-30-shims.xml
new file mode 100644
index 0000000..da541cd
--- /dev/null
+++ b/lint-baseline-api-30-shims.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="        return lp.getNat64Prefix();"
+        errorLine2="                  ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java"
+            line="85"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#setNat64Prefix`"
+        errorLine1="        lp.setNat64Prefix(prefix);"
+        errorLine2="           ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java"
+            line="90"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#setDhcpServerAddress`"
+        errorLine1="        lp.setDhcpServerAddress(serverAddress);"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/apishim/30/com/android/networkstack/apishim/api30/NetworkInformationShimImpl.java"
+            line="109"
+            column="12"/>
+    </issue>
+
+</issues>
diff --git a/lint-baseline-current-lib.xml b/lint-baseline-current-lib.xml
new file mode 100644
index 0000000..f7b00f2
--- /dev/null
+++ b/lint-baseline-current-lib.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1032"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1034"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1087"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1089"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#any`"
+        errorLine1="        final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,"
+        errorLine2="                                                               ~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1368"
+            column="64"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#toIntArray`"
+        errorLine1="                    CollectionUtils.toIntArray(mDnsReturnCode),"
+        errorLine2="                                    ~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/DataStallDetectionStats.java"
+            line="302"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#toLongArray`"
+        errorLine1="                    CollectionUtils.toLongArray(mDnsTimeStamp),"
+        errorLine2="                                    ~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/DataStallDetectionStats.java"
+            line="303"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="        newLp.setNat64Prefix(netlinkLinkProperties.getNat64Prefix());"
+        errorLine2="                                                   ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/ip/IpClient.java"
+            line="1337"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#setNat64Prefix`"
+        errorLine1="        newLp.setNat64Prefix(netlinkLinkProperties.getNat64Prefix());"
+        errorLine2="              ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/ip/IpClient.java"
+            line="1337"
+            column="15"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="            mStatsBuilder.setIpv4LatencyMicros(ConnectivityUtils.saturatedCast(mIpv4Watch.stop()));"
+        errorLine2="                                                                 ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/IpProvisioningMetrics.java"
+            line="74"
+            column="66"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="            mStatsBuilder.setIpv6LatencyMicros(ConnectivityUtils.saturatedCast(mIpv6Watch.stop()));"
+        errorLine2="                                                                 ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/IpProvisioningMetrics.java"
+            line="83"
+            column="66"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#isEmpty`"
+        errorLine1="        if (isEmpty(mCaptivePortalFallbackSpecs)) {"
+        errorLine2="            ~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="2279"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#isIPv6ULA`"
+        errorLine1="                    || isIPv6ULA(address)) {"
+        errorLine2="                       ~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="2447"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.telephony.NetworkRegistrationInfo#getCellIdentity`"
+        errorLine1="                    nri == null ? null : nri.getCellIdentity());"
+        errorLine2="                                             ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="3088"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="        int latencyUs = ConnectivityUtils.saturatedCast(durationUs);"
+        errorLine2="                                          ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="198"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="                .setRemainingTtlSecs(ConnectivityUtils.saturatedCast(secondsRemaining))"
+        errorLine2="                                                       ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="209"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="                        ConnectivityUtils.saturatedCast(capportData.getByteLimit() / 1000))"
+        errorLine2="                                          ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="212"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="        mStatsBuilder.setLatencyMicros(ConnectivityUtils.saturatedCast(mWatch.stop()));"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="237"
+            column="58"/>
+    </issue>
+
+</issues>
diff --git a/lint-baseline-stable-lib.xml b/lint-baseline-stable-lib.xml
new file mode 100644
index 0000000..f7b00f2
--- /dev/null
+++ b/lint-baseline-stable-lib.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1032"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1034"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1087"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#addressAndPortToString`"
+        errorLine1="                        ConnectivityUtils.addressAndPortToString("
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1089"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#any`"
+        errorLine1="        final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,"
+        errorLine2="                                                               ~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java"
+            line="1368"
+            column="64"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#toIntArray`"
+        errorLine1="                    CollectionUtils.toIntArray(mDnsReturnCode),"
+        errorLine2="                                    ~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/DataStallDetectionStats.java"
+            line="302"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#toLongArray`"
+        errorLine1="                    CollectionUtils.toLongArray(mDnsTimeStamp),"
+        errorLine2="                                    ~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/DataStallDetectionStats.java"
+            line="303"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="        newLp.setNat64Prefix(netlinkLinkProperties.getNat64Prefix());"
+        errorLine2="                                                   ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/ip/IpClient.java"
+            line="1337"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#setNat64Prefix`"
+        errorLine1="        newLp.setNat64Prefix(netlinkLinkProperties.getNat64Prefix());"
+        errorLine2="              ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/android/net/ip/IpClient.java"
+            line="1337"
+            column="15"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="            mStatsBuilder.setIpv4LatencyMicros(ConnectivityUtils.saturatedCast(mIpv4Watch.stop()));"
+        errorLine2="                                                                 ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/IpProvisioningMetrics.java"
+            line="74"
+            column="66"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="            mStatsBuilder.setIpv6LatencyMicros(ConnectivityUtils.saturatedCast(mIpv6Watch.stop()));"
+        errorLine2="                                                                 ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/IpProvisioningMetrics.java"
+            line="83"
+            column="66"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.CollectionUtils#isEmpty`"
+        errorLine1="        if (isEmpty(mCaptivePortalFallbackSpecs)) {"
+        errorLine2="            ~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="2279"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#isIPv6ULA`"
+        errorLine1="                    || isIPv6ULA(address)) {"
+        errorLine2="                       ~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="2447"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.telephony.NetworkRegistrationInfo#getCellIdentity`"
+        errorLine1="                    nri == null ? null : nri.getCellIdentity());"
+        errorLine2="                                             ~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java"
+            line="3088"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="        int latencyUs = ConnectivityUtils.saturatedCast(durationUs);"
+        errorLine2="                                          ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="198"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="                .setRemainingTtlSecs(ConnectivityUtils.saturatedCast(secondsRemaining))"
+        errorLine2="                                                       ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="209"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="                        ConnectivityUtils.saturatedCast(capportData.getByteLimit() / 1000))"
+        errorLine2="                                          ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="212"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 29): `com.android.net.module.util.ConnectivityUtils#saturatedCast`"
+        errorLine1="        mStatsBuilder.setLatencyMicros(ConnectivityUtils.saturatedCast(mWatch.stop()));"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkValidationMetrics.java"
+            line="237"
+            column="58"/>
+    </issue>
+
+</issues>
diff --git a/proguard.flags b/proguard.flags
index af4262a..7f8f207 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -8,6 +8,10 @@
     static final int EVENT_*;
 }
 
+-keepclassmembers public class * extends com.android.networkstack.util.Struct {
+    *;
+}
+
 # The lite proto runtime uses reflection to access fields based on the names in
 # the schema, keep all the fields.
 # This replicates the base proguard rule used by the build by default
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 4772691..2267cd4 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -18,9 +18,9 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="notification_channel_name_connected" msgid="1795068343200033922">"אימות של פורטל שבוי"</string>
     <string name="notification_channel_description_connected" msgid="7239184168268014518">"התראות המוצגות כשהמכשיר אומת בהצלחה וחובר לרשת של פורטל שבוי"</string>
-    <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"מידע על מקום רשת"</string>
+    <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"מידע על מקום הרשת"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"התראות המוצגות כדי לציין שלרשת יש דף מידע על מקום"</string>
-    <string name="connected" msgid="4563643884927480998">"מחובר"</string>
+    <string name="connected" msgid="4563643884927480998">"המכשיר מחובר"</string>
     <string name="tap_for_info" msgid="6849746325626883711">"מחוברת / יש להקיש כדי להציג את האתר"</string>
     <string name="application_label" msgid="1322847171305285454">"ניהול רשתות"</string>
 </resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 500d584..b7ff1bc 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -20,7 +20,7 @@
     <string name="notification_channel_description_connected" msgid="7239184168268014518">"यन्त्र क्याप्टिभ पोर्टल नेटवर्कमा सफलतापूर्वक जोडिएको कुरा प्रमाणित भएपछि देखाइने सूचनाहरू"</string>
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"नेटवर्कको स्थानसम्बन्धी जानकारी"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"नेटवर्कको स्थानसम्बन्धी जानकारी भएको पृष्ठ रहेको सङ्केत गर्न देखाइने सूचनाहरू"</string>
-    <string name="connected" msgid="4563643884927480998">"जोडिएको छ"</string>
+    <string name="connected" msgid="4563643884927480998">"कनेक्ट गरिएको छ"</string>
     <string name="tap_for_info" msgid="6849746325626883711">"जोडियो / वेबसाइट हेर्न ट्याप गर्नुहोस्"</string>
     <string name="application_label" msgid="1322847171305285454">"नेटवर्क व्यवस्थापक"</string>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index d6a11ab..805ca04 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -105,4 +105,20 @@
          increased until reaching the config_max_retry_timer. -->
     <integer name="config_evaluating_bandwidth_min_retry_timer_ms"></integer>
     <integer name="config_evaluating_bandwidth_max_retry_timer_ms"></integer>
+
+    <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
+         Those frames are identified by the field Eth-type having values
+         less than 0x600 -->
+    <bool name="config_apfDrop802_3Frames">true</bool>
+
+    <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
+         will be dropped
+         TODO: need to put proper values, these are for testing purposes only -->
+    <integer-array name="config_apfEthTypeDenyList">
+        <item>0x88A2</item>
+        <item>0x88A4</item>
+        <item>0x88B8</item>
+        <item>0x88CD</item>
+        <item>0x88E3</item>
+    </integer-array>
 </resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index b2967b9..bfb450e 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -77,6 +77,13 @@
             <item type="integer" name="config_evaluating_bandwidth_timeout_ms"/>
             <item type="integer" name="config_evaluating_bandwidth_min_retry_timer_ms"/>
             <item type="integer" name="config_evaluating_bandwidth_max_retry_timer_ms"/>
+
+            <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
+            Those frames are identified by the field Eth-type having values less than 0x600 -->
+            <item type="bool" name="config_apfDrop802_3Frames"/>
+            <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
+            will be dropped -->
+            <item type="array" name="config_apfEthTypeDenyList"/>
         </policy>
     </overlayable>
 </resources>
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 165e37b..7a13392 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -27,11 +27,12 @@
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_RAW;
 
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
-import static com.android.server.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+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_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -64,6 +65,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.ConnectivityUtils;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -283,8 +286,6 @@
     private static final int ETH_ETHERTYPE_OFFSET = 12;
     private static final int ETH_TYPE_MIN = 0x0600;
     private static final int ETH_TYPE_MAX = 0xFFFF;
-    private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
-            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
     // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
     private static final int IPV4_TOTAL_LENGTH_OFFSET = ETH_HEADER_LEN + 2;
     private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
@@ -778,7 +779,7 @@
             mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
             mLastSeen = currentTimeSeconds();
 
-            // Sanity check packet in case a packet arrives before we attach RA filter
+            // Check packet in case a packet arrives before we attach RA filter
             // to our packet socket. b/29586253
             if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
                     getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
@@ -1027,9 +1028,9 @@
         public String toString() {
             try {
                 return String.format("%s -> %s",
-                        NetworkStackUtils.addressAndPortToString(
+                        ConnectivityUtils.addressAndPortToString(
                                 InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
-                        NetworkStackUtils.addressAndPortToString(
+                        ConnectivityUtils.addressAndPortToString(
                                 InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort));
             } catch (UnknownHostException e) {
                 return "Unknown host";
@@ -1082,9 +1083,9 @@
         public String toString() {
             try {
                 return String.format("%s -> %s , seq=%d, ack=%d",
-                        NetworkStackUtils.addressAndPortToString(
+                        ConnectivityUtils.addressAndPortToString(
                                 InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
-                        NetworkStackUtils.addressAndPortToString(
+                        ConnectivityUtils.addressAndPortToString(
                                 InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort),
                         Integer.toUnsignedLong(mPacket.seq),
                         Integer.toUnsignedLong(mPacket.ack));
@@ -1252,7 +1253,7 @@
         // Pass if unicast reply.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
 
         // Either a unicast request, a unicast reply, or a broadcast reply.
         gen.defineLabel(checkTargetIPv4);
@@ -1348,7 +1349,7 @@
             // TODO: can we invert this condition to fall through to the common pass case below?
             maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
             gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+            gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
             gen.addJump(mCountAndDropLabel);
         } else {
@@ -1363,7 +1364,7 @@
 
     private void generateKeepaliveFilters(ApfGenerator gen, Class<?> filterType, int proto,
             int offset, String label) throws IllegalInstructionException {
-        final boolean haveKeepaliveResponses = NetworkStackUtils.any(mKeepalivePackets,
+        final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,
                 ack -> filterType.isInstance(ack));
 
         // If no keepalive packets of this type
@@ -1410,7 +1411,7 @@
         //     pass
         // if it's ICMPv6 RS to any:
         //   drop
-        // if it's ICMPv6 NA to ff02::1:
+        // if it's ICMPv6 NA to anything in ff02::/120
         //   drop
         // if keepalive ack
         //   drop
@@ -1464,11 +1465,14 @@
         gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel);
         // If not neighbor announcements, skip filter.
         gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
-        // If to ff02::1, drop.
+        // Drop all multicast NA to ff02::/120.
+        // This is a way to cover ff02::1 and ff02::2 with a single JNEBS.
         // TODO: Drop only if they don't contain the address of on-link neighbours.
+        final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
         gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, IPV6_ALL_NODES_ADDRESS,
+        gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix,
                 skipUnsolicitedMulticastNALabel);
+
         maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
         gen.addJump(mCountAndDropLabel);
         gen.defineLabel(skipUnsolicitedMulticastNALabel);
@@ -1491,7 +1495,7 @@
      * <li>Drop all broadcast non-IP non-ARP packets.
      * <li>Pass all non-ICMPv6 IPv6 packets,
      * <li>Pass all non-IPv4 and non-IPv6 packets,
-     * <li>Drop IPv6 ICMPv6 NAs to ff02::1.
+     * <li>Drop IPv6 ICMPv6 NAs to anything in ff02::/120.
      * <li>Drop IPv6 ICMPv6 RSs.
      * <li>Filter IPv4 packets (see generateIPv4FilterLocked())
      * <li>Filter IPv6 packets (see generateIPv6FilterLocked())
@@ -1567,7 +1571,7 @@
         // Drop non-IP non-ARP broadcasts, pass the rest
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
         maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
         gen.addJump(mCountAndDropLabel);
 
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
index 44ce2db..bf4d910 100644
--- a/src/android/net/apf/ApfGenerator.java
+++ b/src/android/net/apf/ApfGenerator.java
@@ -752,7 +752,8 @@
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if the bytes of the
-     * packet at an offset specified by {@code register} match {@code bytes}.
+     * packet at an offset specified by {@code register} don't match {@code bytes}, {@code register}
+     * must be R0.
      */
     public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target)
             throws IllegalInstructionException {
diff --git a/src/android/net/dhcp/DhcpAckPacket.java b/src/android/net/dhcp/DhcpAckPacket.java
index 9e981ef..225e447 100644
--- a/src/android/net/dhcp/DhcpAckPacket.java
+++ b/src/android/net/dhcp/DhcpAckPacket.java
@@ -46,10 +46,11 @@
             dnsServers += dnsServer.toString() + " ";
         }
 
-        return s + " ACK: your new IP " + mYourIp +
-                ", netmask " + mSubnetMask +
-                ", gateways " + mGateways + dnsServers +
-                ", lease time " + mLeaseTime;
+        return s + " ACK: your new IP " + mYourIp
+                + ", netmask " + mSubnetMask
+                + ", gateways " + mGateways + dnsServers
+                + ", lease time " + mLeaseTime
+                + (mIpv6OnlyWaitTime != null ? ", V6ONLY_WAIT " + mIpv6OnlyWaitTime : "");
     }
 
     /**
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 4fedf30..8e0e9d3 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -20,6 +20,7 @@
 import static android.net.dhcp.DhcpPacket.DHCP_CAPTIVE_PORTAL;
 import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
 import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
+import static android.net.dhcp.DhcpPacket.DHCP_IPV6_ONLY_PREFERRED;
 import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
 import static android.net.dhcp.DhcpPacket.DHCP_MTU;
 import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
@@ -31,6 +32,7 @@
 import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
 import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
 import static android.net.util.NetworkStackUtils.DHCP_INIT_REBOOT_VERSION;
+import static android.net.util.NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION;
 import static android.net.util.NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION;
 import static android.net.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION;
 import static android.net.util.NetworkStackUtils.closeSocketQuietly;
@@ -49,11 +51,11 @@
 import static android.system.OsConstants.SO_RCVBUF;
 import static android.system.OsConstants.SO_REUSEADDR;
 
-import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
-import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
-import static com.android.server.util.NetworkStackConstants.IPV4_CONFLICT_ANNOUNCE_NUM;
-import static com.android.server.util.NetworkStackConstants.IPV4_CONFLICT_PROBE_NUM;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_ANNOUNCE_NUM;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_PROBE_NUM;
 
 import android.content.Context;
 import android.net.DhcpResults;
@@ -69,10 +71,10 @@
 import android.net.metrics.DhcpClientEvent;
 import android.net.metrics.DhcpErrorEvent;
 import android.net.metrics.IpConnectivityLog;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.net.util.HostnameTransliterator;
 import android.net.util.InterfaceParams;
 import android.net.util.NetworkStackUtils;
-import android.net.util.PacketReader;
 import android.net.util.SocketUtils;
 import android.os.Build;
 import android.os.Handler;
@@ -95,8 +97,10 @@
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.internal.util.TrafficStatsConstants;
 import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.PacketReader;
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
 import com.android.networkstack.apishim.SocketUtilsShimImpl;
@@ -104,6 +108,7 @@
 import com.android.networkstack.arp.ArpPacket;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 
+import java.io.ByteArrayOutputStream;
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.Inet4Address;
@@ -111,6 +116,7 @@
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Random;
 
 /**
@@ -244,6 +250,7 @@
     /* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
     public static final int DHCP_SUCCESS = 1;
     public static final int DHCP_FAILURE = 2;
+    public static final int DHCP_IPV6_ONLY = 3;
 
     // Internal messages.
     private static final int PRIVATE_BASE         = IpClient.DHCPCLIENT_CMD_BASE + 100;
@@ -287,13 +294,22 @@
 
     @NonNull
     private byte[] getRequestedParams() {
+        // Set an initial size large enough for all optional parameters that we might request.
+        final int numOptionalParams = 2;
+        final ByteArrayOutputStream params =
+                new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams);
+        params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length);
         if (isCapportApiEnabled()) {
-            final byte[] params = Arrays.copyOf(DEFAULT_REQUESTED_PARAMS,
-                    DEFAULT_REQUESTED_PARAMS.length + 1);
-            params[params.length - 1] = DHCP_CAPTIVE_PORTAL;
-            return params;
+            params.write(DHCP_CAPTIVE_PORTAL);
         }
-        return DEFAULT_REQUESTED_PARAMS;
+        if (isIPv6OnlyPreferredModeEnabled()) {
+            params.write(DHCP_IPV6_ONLY_PREFERRED);
+        }
+        // Customized DHCP options to be put in PRL.
+        for (DhcpOption option : mConfiguration.options) {
+            if (option.value == null) params.write(option.type);
+        }
+        return params.toByteArray();
     }
 
     private static boolean isCapportApiEnabled() {
@@ -338,10 +354,10 @@
     private int mConflictCount;
     private long mLastAssignedIpv4AddressExpiry;
     private Dependencies mDependencies;
-    @NonNull
-    private final NetworkStackIpMemoryStore mIpMemoryStore;
     @Nullable
     private DhcpPacketHandler mDhcpPacketHandler;
+    @NonNull
+    private final NetworkStackIpMemoryStore mIpMemoryStore;
     @Nullable
     private final String mHostname;
 
@@ -349,6 +365,10 @@
     private long mLastInitEnterTime;
     private long mLastBoundExitTime;
 
+    // 32-bit unsigned integer used to indicate the number of milliseconds the DHCP client should
+    // disable DHCPv4.
+    private long mIpv6OnlyWaitTimeMs;
+
     // States.
     private State mStoppedState = new StoppedState();
     private State mDhcpState = new DhcpState();
@@ -370,6 +390,7 @@
             new WaitBeforeObtainingConfigurationState(mObtainingConfigurationState);
     private State mIpAddressConflictDetectingState = new IpAddressConflictDetectingState();
     private State mDhcpDecliningState = new DhcpDecliningState();
+    private State mIpv6OnlyWaitState = new Ipv6OnlyWaitState();
 
     private WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
         cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
@@ -421,11 +442,11 @@
 
         /**
          * Return whether a feature guarded by a feature flag is enabled.
-         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
+         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String)
          */
         public boolean isFeatureEnabled(final Context context, final String name,
                 boolean defaultEnabled) {
-            return NetworkStackUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
                     defaultEnabled);
         }
 
@@ -434,7 +455,7 @@
          */
         public int getIntDeviceConfig(final String name, int minimumValue, int maximumValue,
                 int defaultValue) {
-            return NetworkStackUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
                     name, minimumValue, maximumValue, defaultValue);
         }
 
@@ -470,6 +491,7 @@
             addState(mDhcpSelectingState, mDhcpState);
             addState(mDhcpRequestingState, mDhcpState);
             addState(mIpAddressConflictDetectingState, mDhcpState);
+            addState(mIpv6OnlyWaitState, mDhcpState);
             addState(mDhcpHaveLeaseState, mDhcpState);
                 addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
                 addState(mDhcpBoundState, mDhcpHaveLeaseState);
@@ -544,6 +566,17 @@
                 false /* defaultEnabled */);
     }
 
+    /**
+     * check whether or not to support IPv6-only preferred option.
+     *
+     * IPv6-only preferred option is enabled by default if there is no experiment flag set to
+     * disable this feature explicitly.
+     */
+    public boolean isIPv6OnlyPreferredModeEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION,
+                true /* defaultEnabled */);
+    }
+
     private void recordMetricEnabledFeatures() {
         if (isDhcpLeaseCacheEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_INITREBOOT);
         if (isDhcpRapidCommitEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_RAPIDCOMMIT);
@@ -578,7 +611,7 @@
 
     private boolean initUdpSocket() {
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_DHCP);
+                NetworkStackConstants.TAG_SYSTEM_DHCP);
         try {
             mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
             SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
@@ -605,6 +638,13 @@
         }
     }
 
+    private byte[] getOptionsToSkip() {
+        final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2);
+        if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL);
+        if (!isIPv6OnlyPreferredModeEnabled()) optionsToSkip.write(DHCP_IPV6_ONLY_PREFERRED);
+        return optionsToSkip.toByteArray();
+    }
+
     private class DhcpPacketHandler extends PacketReader {
         private FileDescriptor mPacketSock;
 
@@ -615,10 +655,8 @@
         @Override
         protected void handlePacket(byte[] recvbuf, int length) {
             try {
-                final byte[] optionsToSkip =
-                        isCapportApiEnabled() ? new byte[0] : new byte[] { DHCP_CAPTIVE_PORTAL };
                 final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf, length,
-                        DhcpPacket.ENCAP_L2, optionsToSkip);
+                        DhcpPacket.ENCAP_L2, getOptionsToSkip());
                 if (DBG) Log.d(TAG, "Received packet: " + packet);
                 sendMessage(CMD_RECEIVED_PACKET, packet);
             } catch (DhcpPacket.ParseException e) {
@@ -711,7 +749,8 @@
     private boolean sendDiscoverPacket() {
         final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                 DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
-                DO_UNICAST, getRequestedParams(), isDhcpRapidCommitEnabled(), mHostname);
+                DO_UNICAST, getRequestedParams(), isDhcpRapidCommitEnabled(), mHostname,
+                mConfiguration.options);
         mMetrics.incrementCountForDiscover();
         return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
     }
@@ -724,9 +763,9 @@
                 ? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;
 
         final ByteBuffer packet = DhcpPacket.buildRequestPacket(
-                encap, mTransactionId, getSecs(), clientAddress,
-                DO_UNICAST, mHwAddr, requestedAddress,
-                serverAddress, getRequestedParams(), mHostname);
+                encap, mTransactionId, getSecs(), clientAddress, DO_UNICAST, mHwAddr,
+                requestedAddress, serverAddress, getRequestedParams(), mHostname,
+                mConfiguration.options);
         String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
         String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
                              " request=" + requestedAddress.getHostAddress() +
@@ -944,12 +983,17 @@
         // This is part of the initial configuration because it is passed in on startup and
         // never updated.
         // TODO: decide what to do about L2 key changes while the client is connected.
+        @Nullable
         public final String l2Key;
         public final boolean isPreconnectionEnabled;
+        @NonNull
+        public final List<DhcpOption> options;
 
-        public Configuration(String l2Key, boolean isPreconnectionEnabled) {
+        public Configuration(@Nullable final String l2Key, final boolean isPreconnectionEnabled,
+                @NonNull final List<DhcpOption> options) {
             this.l2Key = l2Key;
             this.isPreconnectionEnabled = isPreconnectionEnabled;
+            this.options = options;
         }
     }
 
@@ -1052,7 +1096,7 @@
     }
 
     abstract class TimeoutState extends LoggingState {
-        protected int mTimeout = 0;
+        protected long mTimeout = 0;
 
         @Override
         public void enter() {
@@ -1223,6 +1267,15 @@
         }
     }
 
+    private boolean maybeTransitionToIpv6OnlyWaitState(@NonNull final DhcpPacket packet) {
+        if (!isIPv6OnlyPreferredModeEnabled()) return false;
+        if (packet.getIpv6OnlyWaitTimeMillis() == DhcpPacket.V6ONLY_PREFERRED_ABSENCE) return false;
+
+        mIpv6OnlyWaitTimeMs = packet.getIpv6OnlyWaitTimeMillis();
+        transitionTo(mIpv6OnlyWaitState);
+        return true;
+    }
+
     private void receiveOfferOrAckPacket(final DhcpPacket packet, final boolean acceptRapidCommit) {
         if (!isValidPacket(packet)) return;
 
@@ -1230,6 +1283,9 @@
         // 2. received the DHCPACK packet from DHCP Servers that support Rapid
         //    Commit option, process it by following RFC4039.
         if (packet instanceof DhcpOfferPacket) {
+            if (maybeTransitionToIpv6OnlyWaitState(packet)) {
+                return;
+            }
             mOffer = packet.toDhcpResults();
             if (mOffer != null) {
                 Log.d(TAG, "Got pending lease: " + mOffer);
@@ -1334,7 +1390,8 @@
             final Layer2PacketParcelable l2Packet = new Layer2PacketParcelable();
             final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                     DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
-                    DO_UNICAST, getRequestedParams(), true /* rapid commit */, mHostname);
+                    DO_UNICAST, getRequestedParams(), true /* rapid commit */, mHostname,
+                    mConfiguration.options);
 
             l2Packet.dstMacAddress = MacAddress.fromBytes(DhcpPacket.ETHER_BROADCAST);
             l2Packet.payload = Arrays.copyOf(packet.array(), packet.limit());
@@ -1362,7 +1419,10 @@
         protected void receivePacket(DhcpPacket packet) {
             if (!isValidPacket(packet)) return;
             if ((packet instanceof DhcpAckPacket)) {
-                DhcpResults results = packet.toDhcpResults();
+                if (maybeTransitionToIpv6OnlyWaitState(packet)) {
+                    return;
+                }
+                final DhcpResults results = packet.toDhcpResults();
                 if (results != null) {
                     confirmDhcpLease(packet, results);
                     transitionTo(isDhcpIpConflictDetectEnabled()
@@ -1762,6 +1822,9 @@
         protected void receivePacket(DhcpPacket packet) {
             if (!isValidPacket(packet)) return;
             if ((packet instanceof DhcpAckPacket)) {
+                if (maybeTransitionToIpv6OnlyWaitState(packet)) {
+                    return;
+                }
                 final DhcpResults results = packet.toDhcpResults();
                 if (results != null) {
                     if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
@@ -1905,6 +1968,32 @@
         }
     }
 
+    // This state is used for IPv6-only preferred mode defined in the draft-ietf-dhc-v6only.
+    // For IPv6-only capable host, it will forgo obtaining an IPv4 address for V6ONLY_WAIT
+    // period if the network indicates that it can provide IPv6 connectivity by replying
+    // with a valid IPv6-only preferred option in the DHCPOFFER or DHCPACK.
+    class Ipv6OnlyWaitState extends TimeoutState {
+        @Override
+        public void enter() {
+            mTimeout = mIpv6OnlyWaitTimeMs;
+            super.enter();
+
+            // Restore power save and suspend optimization if it was disabled before.
+            if (mRegisteredForPreDhcpNotification) {
+                mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_IPV6_ONLY, 0, null);
+            }
+        }
+
+        @Override
+        public void exit() {
+            mIpv6OnlyWaitTimeMs = 0;
+        }
+
+        protected void timeout() {
+            startInitRebootOrInit();
+        }
+    }
+
     private void logState(String name, int durationMs) {
         final DhcpClientEvent event = new DhcpClientEvent.Builder()
                 .setMsg(name)
diff --git a/src/android/net/dhcp/DhcpDiscoverPacket.java b/src/android/net/dhcp/DhcpDiscoverPacket.java
index 15c20cf..9c776b9 100644
--- a/src/android/net/dhcp/DhcpDiscoverPacket.java
+++ b/src/android/net/dhcp/DhcpDiscoverPacket.java
@@ -66,6 +66,7 @@
         if (mRapidCommit) {
             addTlv(buffer, DHCP_RAPID_COMMIT);
         }
+        addCustomizedClientTlvs(buffer);
         addTlvEnd(buffer);
     }
 }
diff --git a/src/android/net/dhcp/DhcpLeaseRepository.java b/src/android/net/dhcp/DhcpLeaseRepository.java
index 8420996..562d36c 100644
--- a/src/android/net/dhcp/DhcpLeaseRepository.java
+++ b/src/android/net/dhcp/DhcpLeaseRepository.java
@@ -22,8 +22,8 @@
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_BITS;
 
 import static java.lang.Math.min;
 
diff --git a/src/android/net/dhcp/DhcpOfferPacket.java b/src/android/net/dhcp/DhcpOfferPacket.java
index aae08a7..e3e5d0f 100644
--- a/src/android/net/dhcp/DhcpOfferPacket.java
+++ b/src/android/net/dhcp/DhcpOfferPacket.java
@@ -47,9 +47,12 @@
             }
         }
 
-        return s + " OFFER, ip " + mYourIp + ", mask " + mSubnetMask +
-                dnsServers + ", gateways " + mGateways +
-                " lease time " + mLeaseTime + ", domain " + mDomainName;
+        return s + " OFFER, ip " + mYourIp
+                + ", mask " + mSubnetMask + dnsServers
+                + ", gateways " + mGateways
+                + ", lease time " + mLeaseTime
+                + ", domain " + mDomainName
+                + (mIpv6OnlyWaitTime != null ? ", V6ONLY_WAIT " + mIpv6OnlyWaitTime : "");
     }
 
     /**
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
index 3915740..1331a24 100644
--- a/src/android/net/dhcp/DhcpPacket.java
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -16,17 +16,19 @@
 
 package android.net.dhcp;
 
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ALL;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
 
 import android.net.DhcpResults;
 import android.net.LinkAddress;
 import android.net.metrics.DhcpErrorEvent;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.system.OsConstants;
 import android.text.TextUtils;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -88,6 +90,10 @@
     public static final int HWADDR_LEN = 16;
     public static final int MAX_OPTION_LEN = 255;
 
+    // The lower boundary for V6ONLY_WAIT.
+    public static final long MIN_V6ONLY_WAIT_MS = 300_000;
+    public static final long V6ONLY_PREFERRED_ABSENCE = -1L;
+
     /**
      * The minimum and maximum MTU that we are prepared to use. We set the minimum to the minimum
      * IPv6 MTU because the IPv6 stack enters unusual codepaths when the link MTU drops below 1280,
@@ -181,7 +187,8 @@
      * DHCP Optional Type: DHCP Host Name
      */
     public static final byte DHCP_HOST_NAME = 12;
-    protected String mHostName;
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public String mHostName;
 
     /**
      * DHCP Optional Type: DHCP DOMAIN NAME
@@ -193,7 +200,8 @@
      * DHCP Optional Type: DHCP Interface MTU
      */
     public static final byte DHCP_MTU = 26;
-    protected Short mMtu;
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public Short mMtu;
 
     /**
      * DHCP Optional Type: DHCP BROADCAST ADDRESS
@@ -292,7 +300,8 @@
      * DHCP Optional Type: Vendor Class Identifier
      */
     public static final byte DHCP_VENDOR_CLASS_ID = 60;
-    protected String mVendorId;
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public String mVendorId;
 
     /**
      * DHCP Optional Type: DHCP Client Identifier
@@ -301,12 +310,29 @@
     protected byte[] mClientId;
 
     /**
+     * DHCP Optional Type: DHCP User Class option
+     */
+    public static final byte DHCP_USER_CLASS = 77;
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public byte[] mUserClass;
+
+    /**
      * DHCP zero-length Optional Type: Rapid Commit. Per RFC4039, both DHCPDISCOVER and DHCPACK
      * packet may include this option.
      */
     public static final byte DHCP_RAPID_COMMIT = 80;
     protected boolean mRapidCommit;
 
+    /**
+     * DHCP IPv6-Only Preferred Option(RFC 8925).
+     * Indicate that a host supports an IPv6-only mode and willing to forgo obtaining an IPv4
+     * address for V6ONLY_WAIT period if the network provides IPv6 connectivity. V6ONLY_WAIT
+     * is 32-bit unsigned integer, so the Integer value cannot be used as-is.
+     */
+    public static final byte DHCP_IPV6_ONLY_PREFERRED = (byte) 108;
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public Integer mIpv6OnlyWaitTime;
+
     public static final byte DHCP_CAPTIVE_PORTAL = (byte) 114;
     protected String mCaptivePortalUrl;
 
@@ -357,6 +383,12 @@
     protected String mServerHostName;
 
     /**
+     * The customized DHCP client options to be sent.
+     */
+    @Nullable
+    protected List<DhcpOption> mCustomizedClientOptions;
+
+    /**
      * Asks the packet object to create a ByteBuffer serialization of
      * the packet for transmission.
      */
@@ -740,9 +772,25 @@
         buf.put((byte) 0xFF);
     }
 
-    private String getVendorId() {
+    /**
+     * Get the DHCP Vendor Class Identifier.
+     *
+     * By default the vendor Id is "android-dhcp-<version>". The default value will be overwritten
+     * with the customized option value if any.
+     */
+    private static String getVendorId(@Nullable List<DhcpOption> customizedClientOptions) {
         if (testOverrideVendorId != null) return testOverrideVendorId;
-        return "android-dhcp-" + Build.VERSION.RELEASE;
+
+        String vendorId = "android-dhcp-" + Build.VERSION.RELEASE;
+        if (customizedClientOptions != null) {
+            for (DhcpOption option : customizedClientOptions) {
+                if (option.type == DHCP_VENDOR_CLASS_ID) {
+                    vendorId = readAsciiString(option.value, false);
+                    break;
+                }
+            }
+        }
+        return vendorId;
     }
 
     /**
@@ -765,11 +813,25 @@
      */
     protected void addCommonClientTlvs(ByteBuffer buf) {
         addTlv(buf, DHCP_MAX_MESSAGE_SIZE, (short) MAX_LENGTH);
-        addTlv(buf, DHCP_VENDOR_CLASS_ID, getVendorId());
+        addTlv(buf, DHCP_VENDOR_CLASS_ID, mVendorId);
         final String hn = getHostname();
         if (!TextUtils.isEmpty(hn)) addTlv(buf, DHCP_HOST_NAME, hn);
     }
 
+    /**
+     * Adds OEM's customized client TLVs, which will be appended before the End Tlv.
+     */
+    protected void addCustomizedClientTlvs(ByteBuffer buf) {
+        if (mCustomizedClientOptions == null) return;
+        for (DhcpOption option : mCustomizedClientOptions) {
+            // A null value means the option should only be put into the PRL.
+            if (option.value == null) continue;
+            // The vendor class ID was already added by addCommonClientTlvs.
+            if (option.type == DHCP_VENDOR_CLASS_ID) continue;
+            addTlv(buf, option.type, option.value);
+        }
+    }
+
     protected void addCommonServerTlvs(ByteBuffer buf) {
         addTlv(buf, DHCP_LEASE_TIME, mLeaseTime);
         if (mLeaseTime != null && mLeaseTime != INFINITE_LEASE) {
@@ -789,6 +851,9 @@
         if (mMtu != null && Short.toUnsignedInt(mMtu) >= IPV4_MIN_MTU) {
             addTlv(buf, DHCP_MTU, mMtu);
         }
+        if (mIpv6OnlyWaitTime != null) {
+            addTlv(buf, DHCP_IPV6_ONLY_PREFERRED, (int) Integer.toUnsignedLong(mIpv6OnlyWaitTime));
+        }
         addTlv(buf, DHCP_CAPTIVE_PORTAL, mCaptivePortalUrl);
     }
 
@@ -842,9 +907,15 @@
     /**
      * Reads a string of specified length from the buffer.
      */
-    private static String readAsciiString(ByteBuffer buf, int byteCount, boolean nullOk) {
-        byte[] bytes = new byte[byteCount];
+    private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount,
+            boolean nullOk) {
+        final byte[] bytes = new byte[byteCount];
         buf.get(bytes);
+        return readAsciiString(bytes, nullOk);
+    }
+
+    private static String readAsciiString(@NonNull final byte[] payload, boolean nullOk) {
+        final byte[] bytes = payload;
         int length = bytes.length;
         if (!nullOk) {
             // Stop at the first null byte. This is because some DHCP options (e.g., the domain
@@ -932,6 +1003,7 @@
         Inet4Address requestedIp = null;
         String serverHostName;
         byte optionOverload = 0;
+        byte[] userClass = null;
 
         // The following are all unsigned integers. Internally we store them as signed integers of
         // the same length because that way we're guaranteed that they can't be out of the range of
@@ -942,6 +1014,7 @@
         Integer leaseTime = null;
         Integer T1 = null;
         Integer T2 = null;
+        Integer ipv6OnlyWaitTime = null;
 
         // dhcp options
         byte dhcpType = (byte) 0xFF;
@@ -1196,6 +1269,11 @@
                             optionOverload = packet.get();
                             optionOverload &= OPTION_OVERLOAD_BOTH;
                             break;
+                        case DHCP_USER_CLASS:
+                            userClass = new byte[optionLen];
+                            packet.get(userClass);
+                            expectedLen = optionLen;
+                            break;
                         case DHCP_RAPID_COMMIT:
                             expectedLen = 0;
                             rapidCommit = true;
@@ -1204,6 +1282,10 @@
                             expectedLen = optionLen;
                             captivePortalUrl = readAsciiString(packet, optionLen, true);
                             break;
+                        case DHCP_IPV6_ONLY_PREFERRED:
+                            expectedLen = 4;
+                            ipv6OnlyWaitTime = Integer.valueOf(packet.getInt());
+                            break;
                         default:
                             expectedLen = skipOption(packet, optionLen);
                     }
@@ -1292,6 +1374,8 @@
         newPacket.mVendorId = vendorId;
         newPacket.mVendorInfo = vendorInfo;
         newPacket.mCaptivePortalUrl = captivePortalUrl;
+        newPacket.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
+        newPacket.mUserClass = userClass;
         if ((optionOverload & OPTION_OVERLOAD_SNAME) == 0) {
             newPacket.mServerHostName = serverHostName;
         } else {
@@ -1385,29 +1469,49 @@
     }
 
     /**
-     * Builds a DHCP-DISCOVER packet from the required specified
-     * parameters.
+     * Returns the IPv6-only wait time, in milliseconds, or -1 if the option is not present.
+     */
+    public long getIpv6OnlyWaitTimeMillis() {
+        if (mIpv6OnlyWaitTime == null) return V6ONLY_PREFERRED_ABSENCE;
+        return Math.max(MIN_V6ONLY_WAIT_MS, Integer.toUnsignedLong(mIpv6OnlyWaitTime) * 1000);
+    }
+
+    /**
+     * Builds a DHCP-DISCOVER packet from the required specified parameters.
      */
     public static ByteBuffer buildDiscoverPacket(int encap, int transactionId,
             short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams,
-            boolean rapidCommit, String hostname) {
+            boolean rapidCommit, String hostname, List<DhcpOption> options) {
         DhcpPacket pkt = new DhcpDiscoverPacket(transactionId, secs, INADDR_ANY /* relayIp */,
                 clientMac, broadcast, INADDR_ANY /* srcIp */, rapidCommit);
         pkt.mRequestedParams = expectedParams;
         pkt.mHostName = hostname;
+        pkt.mCustomizedClientOptions = options;
+        pkt.mVendorId = getVendorId(options);
         return pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
     }
 
     /**
-     * Builds a DHCP-OFFER packet from the required specified
-     * parameters.
+     * Builds a DHCP-DISCOVER packet from the required specified parameters.
+     *
+     * TODO: remove this method when automerger to mainline-prod is running.
+     */
+    public static ByteBuffer buildDiscoverPacket(int encap, int transactionId,
+            short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams,
+            boolean rapidCommit, String hostname) {
+        return buildDiscoverPacket(encap, transactionId, secs, clientMac, broadcast,
+                expectedParams, rapidCommit, hostname, new ArrayList<DhcpOption>());
+    }
+
+    /**
+     * Builds a DHCP-OFFER packet from the required specified parameters.
      */
     public static ByteBuffer buildOfferPacket(int encap, int transactionId,
             boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
             Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
             Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
             Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
-            short mtu, String captivePortalUrl) {
+            short mtu, String captivePortalUrl, Integer ipv6OnlyWaitTime) {
         DhcpPacket pkt = new DhcpOfferPacket(
                 transactionId, (short) 0, broadcast, serverIpAddr, relayIp,
                 INADDR_ANY /* clientIp */, yourIp, mac);
@@ -1424,10 +1528,27 @@
         if (metered) {
             pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
         }
+        if (ipv6OnlyWaitTime != null) {
+            pkt.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
+        }
         return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
     }
 
     /**
+     * Builds a DHCP-OFFER packet from the required specified parameters.
+     */
+    public static ByteBuffer buildOfferPacket(int encap, int transactionId,
+            boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
+            Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
+            Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+            Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+            short mtu, String captivePortalUrl) {
+        return buildOfferPacket(encap, transactionId, broadcast, serverIpAddr, relayIp, yourIp,
+                mac, timeout, netMask, bcAddr, gateways, dnsServers, dhcpServerIdentifier,
+                domainName, hostname, metered, mtu, captivePortalUrl, null /* V6ONLY_WAIT */);
+    }
+
+    /**
      * Builds a DHCP-ACK packet from the required specified parameters.
      */
     public static ByteBuffer buildAckPacket(int encap, int transactionId,
@@ -1435,7 +1556,7 @@
             Inet4Address requestClientIp, byte[] mac, Integer timeout, Inet4Address netMask,
             Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
             Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
-            short mtu, boolean rapidCommit, String captivePortalUrl) {
+            short mtu, boolean rapidCommit, String captivePortalUrl, Integer ipv6OnlyWaitTime) {
         DhcpPacket pkt = new DhcpAckPacket(
                 transactionId, (short) 0, broadcast, serverIpAddr, relayIp, requestClientIp, yourIp,
                 mac, rapidCommit);
@@ -1452,10 +1573,28 @@
         if (metered) {
             pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
         }
+        if (ipv6OnlyWaitTime != null) {
+            pkt.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
+        }
         return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
     }
 
     /**
+     * Builds a DHCP-ACK packet from the required specified parameters.
+     */
+    public static ByteBuffer buildAckPacket(int encap, int transactionId,
+            boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp, Inet4Address yourIp,
+            Inet4Address requestClientIp, byte[] mac, Integer timeout, Inet4Address netMask,
+            Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+            Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+            short mtu, boolean rapidCommit, String captivePortalUrl) {
+        return buildAckPacket(encap, transactionId, broadcast, serverIpAddr, relayIp, yourIp,
+                requestClientIp, mac, timeout, netMask, bcAddr, gateways, dnsServers,
+                dhcpServerIdentifier, domainName, hostname, metered, mtu, rapidCommit,
+                captivePortalUrl, null /* V6ONLY_WAIT */);
+    }
+
+    /**
      * Builds a DHCP-NAK packet from the required specified parameters.
      */
     public static ByteBuffer buildNakPacket(int encap, int transactionId, Inet4Address serverIpAddr,
@@ -1473,18 +1612,35 @@
     public static ByteBuffer buildRequestPacket(int encap,
             int transactionId, short secs, Inet4Address clientIp, boolean broadcast,
             byte[] clientMac, Inet4Address requestedIpAddress,
-            Inet4Address serverIdentifier, byte[] requestedParams, String hostName) {
+            Inet4Address serverIdentifier, byte[] requestedParams, String hostName,
+            List<DhcpOption> options) {
         DhcpPacket pkt = new DhcpRequestPacket(transactionId, secs, clientIp,
                 INADDR_ANY /* relayIp */, clientMac, broadcast);
         pkt.mRequestedIp = requestedIpAddress;
         pkt.mServerIdentifier = serverIdentifier;
         pkt.mHostName = hostName;
         pkt.mRequestedParams = requestedParams;
+        pkt.mCustomizedClientOptions = options;
+        pkt.mVendorId = getVendorId(options);
         ByteBuffer result = pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
         return result;
     }
 
     /**
+     * Builds a DHCP-REQUEST packet from the required specified parameters.
+     *
+     * TODO: remove this method when automerger to mainline-prod is running.
+     */
+    public static ByteBuffer buildRequestPacket(int encap,
+            int transactionId, short secs, Inet4Address clientIp, boolean broadcast,
+            byte[] clientMac, Inet4Address requestedIpAddress,
+            Inet4Address serverIdentifier, byte[] requestedParams, String hostName) {
+        return buildRequestPacket(encap, transactionId, secs, clientIp, broadcast, clientMac,
+                requestedIpAddress, serverIdentifier, requestedParams, hostName,
+                new ArrayList<DhcpOption>());
+    }
+
+    /**
      * Builds a DHCP-DECLINE packet from the required specified parameters.
      */
     public static ByteBuffer buildDeclinePacket(int encap, int transactionId, byte[] clientMac,
diff --git a/src/android/net/dhcp/DhcpPacketListener.java b/src/android/net/dhcp/DhcpPacketListener.java
index 656b642..17632a4 100644
--- a/src/android/net/dhcp/DhcpPacketListener.java
+++ b/src/android/net/dhcp/DhcpPacketListener.java
@@ -16,7 +16,6 @@
 
 package android.net.dhcp;
 
-import android.net.util.FdEventsReader;
 import android.os.Handler;
 import android.system.Os;
 import android.util.Log;
@@ -24,6 +23,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.FdEventsReader;
+
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
 import java.net.InetSocketAddress;
diff --git a/src/android/net/dhcp/DhcpRequestPacket.java b/src/android/net/dhcp/DhcpRequestPacket.java
index 0672871..dfd6f0e 100644
--- a/src/android/net/dhcp/DhcpRequestPacket.java
+++ b/src/android/net/dhcp/DhcpRequestPacket.java
@@ -64,6 +64,7 @@
         }
         addCommonClientTlvs(buffer);
         addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+        addCustomizedClientTlvs(buffer);
         addTlvEnd(buffer);
     }
 }
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java
index 6c95b5a..3465e72 100644
--- a/src/android/net/dhcp/DhcpServer.java
+++ b/src/android/net/dhcp/DhcpServer.java
@@ -33,12 +33,12 @@
 import static android.system.OsConstants.SO_BROADCAST;
 import static android.system.OsConstants.SO_REUSEADDR;
 
-import static com.android.internal.util.TrafficStatsConstants.TAG_SYSTEM_DHCP_SERVER;
 import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
 import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
-import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ALL;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_DHCP_SERVER;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import static java.lang.Integer.toUnsignedLong;
@@ -67,6 +67,7 @@
 import com.android.internal.util.HexDump;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.DeviceConfigUtils;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -99,6 +100,7 @@
     private static final int CMD_UPDATE_PARAMS = 3;
     @VisibleForTesting
     protected static final int CMD_RECEIVE_PACKET = 4;
+    private static final int CMD_TERMINATE_AFTER_STOP = 5;
 
     @NonNull
     private final Context mContext;
@@ -230,7 +232,7 @@
 
         @Override
         public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
-            return NetworkStackUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
         }
     }
 
@@ -362,10 +364,12 @@
      * Stop listening for packets.
      *
      * <p>As the server is stopped asynchronously, some packets may still be processed shortly after
-     * calling this method.
+     * calling this method. The server will also be cleaned up and can't be started again, even if
+     * it was already stopped.
      */
     void stop(@Nullable INetworkStackStatusCallback cb) {
         sendMessage(CMD_STOP_DHCP_SERVER, cb);
+        sendMessage(CMD_TERMINATE_AFTER_STOP);
     }
 
     private void maybeNotifyStatus(@Nullable INetworkStackStatusCallback cb, int statusCode) {
@@ -407,6 +411,9 @@
                     mEventCallbacks = obj.second;
                     transitionTo(mRunningState);
                     return HANDLED;
+                case CMD_TERMINATE_AFTER_STOP:
+                    quit();
+                    return HANDLED;
                 default:
                     return NOT_HANDLED;
             }
diff --git a/src/android/net/dhcp/DhcpServingParams.java b/src/android/net/dhcp/DhcpServingParams.java
index 97db6ae..0fd649d 100644
--- a/src/android/net/dhcp/DhcpServingParams.java
+++ b/src/android/net/dhcp/DhcpServingParams.java
@@ -18,9 +18,9 @@
 
 import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE;
-import static com.android.server.util.NetworkStackConstants.IPV4_MAX_MTU;
-import static com.android.server.util.NetworkStackConstants.IPV4_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_MAX_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
 
 import static java.lang.Integer.toUnsignedLong;
 
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
index aa85f3a..c02f7d3 100644
--- a/src/android/net/ip/ConnectivityPacketTracker.java
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -26,7 +26,6 @@
 import android.net.util.ConnectivityPacketSummary;
 import android.net.util.InterfaceParams;
 import android.net.util.NetworkStackUtils;
-import android.net.util.PacketReader;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.system.ErrnoException;
@@ -37,6 +36,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.internal.util.TokenBucket;
+import com.android.net.module.util.PacketReader;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index c4f46ae..c0ae611 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -18,12 +18,25 @@
 
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
+import static android.net.util.NetworkStackUtils.IPCLIENT_DISABLE_ACCEPT_RA_VERSION;
+import static android.net.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
+import static android.net.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
+import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
 
-import static com.android.server.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.DhcpResults;
 import android.net.INetd;
@@ -46,7 +59,9 @@
 import android.net.dhcp.DhcpPacket;
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.IpManagerEvent;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.net.shared.InitialConfiguration;
+import android.net.shared.Layer2Information;
 import android.net.shared.ProvisioningConfiguration;
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo.InformationElement;
@@ -55,12 +70,16 @@
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.ConditionVariable;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.stats.connectivity.DisconnectCode;
+import android.stats.connectivity.NetworkQuirkEvent;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
@@ -77,17 +96,27 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.networkstack.R;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.apishim.SocketUtilsShimImpl;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
+import com.android.networkstack.arp.ArpPacket;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
+import com.android.networkstack.packets.NeighborAdvertisement;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.MalformedURLException;
+import java.net.SocketAddress;
+import java.net.SocketException;
 import java.net.URL;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -97,6 +126,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -119,6 +149,7 @@
  * @hide
  */
 public class IpClient extends StateMachine {
+    private static final String TAG = IpClient.class.getSimpleName();
     private static final boolean DBG = false;
 
     // For message logging.
@@ -132,6 +163,7 @@
     private final NetworkStackIpMemoryStore mIpMemoryStore;
     private final NetworkInformationShim mShim = NetworkInformationShimImpl.newInstance();
     private final IpProvisioningMetrics mIpProvisioningMetrics = new IpProvisioningMetrics();
+    private final NetworkQuirkMetrics mNetworkQuirkMetrics;
 
     /**
      * Dump all state machine and connectivity packet logs to the specified writer.
@@ -432,6 +464,17 @@
                     new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xf2, (byte) 0x06 }
     ));
 
+    // Allows Wi-Fi to pass in DHCP options when particular vendor-specific IEs are present.
+    // Maps each DHCP option code to a list of IEs, any of which will allow that option.
+    private static final Map<Byte, List<byte[]>> DHCP_OPTIONS_ALLOWED = Map.of(
+            (byte) 60, Arrays.asList(
+                    // KT OUI: 00:17:C3, type: 17. See b/170928882.
+                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x17 }),
+            (byte) 77, Arrays.asList(
+                    // KT OUI: 00:17:C3, type: 17. See b/170928882.
+                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x17 })
+    );
+
     // Initialize configurable particular SSID set supporting DHCP Roaming feature. See
     // b/131797393 for more details.
     private static final Set<String> DHCP_ROAMING_SSID_SET = new HashSet<>(
@@ -466,6 +509,8 @@
     private final MessageHandlingLogger mMsgStateLogger;
     private final IpConnectivityLog mMetricsLog;
     private final InterfaceController mInterfaceCtrl;
+    // Set of IPv6 addresses for which unsolicited gratuitous NA packets have been sent.
+    private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
 
     // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
     private final int mMinRdnssLifetimeSec;
@@ -488,7 +533,7 @@
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
     private MacAddress mCurrentBssid;
-    private boolean mHasDisabledIPv6OnProvLoss;
+    private boolean mHasDisabledIpv6OrAcceptRaOnProvLoss;
 
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
@@ -541,7 +586,7 @@
          * Read an integer DeviceConfig property.
          */
         public int getDeviceConfigPropertyInt(String name, int defaultValue) {
-            return NetworkStackUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, name,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, name,
                     defaultValue);
         }
 
@@ -551,6 +596,52 @@
         public IpConnectivityLog getIpConnectivityLog() {
             return new IpConnectivityLog();
         }
+
+        /**
+         * Get a NetworkQuirkMetrics instance.
+         */
+        public NetworkQuirkMetrics getNetworkQuirkMetrics() {
+            return new NetworkQuirkMetrics();
+        }
+
+        /**
+         * Get a IpReachabilityMonitor instance.
+         */
+        public IpReachabilityMonitor getIpReachabilityMonitor(Context context,
+                InterfaceParams ifParams, Handler h, SharedLog log,
+                IpReachabilityMonitor.Callback callback, boolean usingMultinetworkPolicyTracker,
+                IpReachabilityMonitor.Dependencies deps, final INetd netd) {
+            return new IpReachabilityMonitor(context, ifParams, h, log, callback,
+                    usingMultinetworkPolicyTracker, deps, netd);
+        }
+
+        /**
+         * Get a IpReachabilityMonitor dependencies instance.
+         */
+        public IpReachabilityMonitor.Dependencies getIpReachabilityMonitorDeps(Context context,
+                String name) {
+            return IpReachabilityMonitor.Dependencies.makeDefault(context, name);
+        }
+
+        /**
+         * Return whether a feature guarded by a feature flag is enabled.
+         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
+         */
+        public boolean isFeatureEnabled(final Context context, final String name,
+                boolean defaultEnabled) {
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
+                    defaultEnabled);
+        }
+
+        /**
+         * Create an APF filter if apfCapabilities indicates support for packet filtering using
+         * APF programs.
+         * @see ApfFilter#maybeCreate
+         */
+        public ApfFilter maybeCreateApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                InterfaceParams ifParams, IpClientCallbacksWrapper cb) {
+            return ApfFilter.maybeCreate(context, config, ifParams, cb);
+        }
     }
 
     public IpClient(Context context, String ifName, IIpClientCallbacks callback,
@@ -573,6 +664,7 @@
         mClatInterfaceName = CLAT_PREFIX + ifName;
         mDependencies = deps;
         mMetricsLog = deps.getIpConnectivityLog();
+        mNetworkQuirkMetrics = deps.getNetworkQuirkMetrics();
         mShutdownLatch = new CountDownLatch(1);
         mCm = mContext.getSystemService(ConnectivityManager.class);
         mObserverRegistry = observerRegistry;
@@ -634,6 +726,21 @@
                 logMsg(msg);
             }
 
+            @Override
+            public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
+                super.onInterfaceAddressRemoved(address, iface);
+                if (!mInterfaceName.equals(iface)) return;
+                if (!address.isIpv6()) return;
+                final Inet6Address targetIp = (Inet6Address) address.getAddress();
+                if (mGratuitousNaTargetAddresses.contains(targetIp)) {
+                    mGratuitousNaTargetAddresses.remove(targetIp);
+
+                    final String msg = "Global IPv6 address: " + targetIp
+                            + " has removed from the set of gratuitous NA target address.";
+                    logMsg(msg);
+                }
+            }
+
             private void logMsg(String msg) {
                 Log.d(mTag, msg);
                 getHandler().post(() -> mLog.log("OBSERVED " + msg));
@@ -777,6 +884,46 @@
 
     private void stopStateMachineUpdaters() {
         mObserverRegistry.unregisterObserver(mLinkObserver);
+        mLinkObserver.shutdown();
+    }
+
+    private boolean isGratuitousNaEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION,
+                false /* defaultEnabled */);
+    }
+
+    private boolean isGratuitousArpNaRoamingEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION,
+                false /* defaultEnabled */);
+    }
+
+    @VisibleForTesting
+    static MacAddress getInitialBssid(final Layer2Information layer2Info,
+            final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
+        MacAddress bssid = null;
+        // http://b/185202634
+        // ScanResultInfo is not populated in some situations.
+        // On S and above, prefer getting the BSSID from the Layer2Info.
+        // On R and below, get the BSSID from the ScanResultInfo and fall back to
+        // getting it from the Layer2Info. This ensures no regressions if any R
+        // devices pass in a null or meaningless BSSID in the Layer2Info.
+        if (!isAtLeastS && scanResultInfo != null) {
+            try {
+                bssid = MacAddress.fromString(scanResultInfo.getBssid());
+            } catch (IllegalArgumentException e) {
+                Log.wtf(TAG, "Invalid BSSID: " + scanResultInfo.getBssid()
+                        + " in provisioning configuration", e);
+            }
+        }
+        if (bssid == null && layer2Info != null) {
+            bssid = layer2Info.mBssid;
+        }
+        return bssid;
+    }
+
+    private boolean shouldDisableAcceptRaOnProvisioningLoss() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_DISABLE_ACCEPT_RA_VERSION,
+                true /* defaultEnabled */);
     }
 
     @Override
@@ -802,17 +949,8 @@
             return;
         }
 
-        final ScanResultInfo scanResultInfo = req.mScanResultInfo;
-        mCurrentBssid = null;
-        if (scanResultInfo != null) {
-            try {
-                mCurrentBssid = MacAddress.fromString(scanResultInfo.getBssid());
-            } catch (IllegalArgumentException e) {
-                Log.wtf(mTag, "Invalid BSSID: " + scanResultInfo.getBssid()
-                        + " in provisioning configuration", e);
-            }
-        }
-
+        mCurrentBssid = getInitialBssid(req.mLayer2Info, req.mScanResultInfo,
+                ShimUtils.isAtLeastS());
         if (req.mLayer2Info != null) {
             mL2Key = req.mLayer2Info.mL2Key;
             mCluster = req.mLayer2Info.mCluster;
@@ -1106,6 +1244,21 @@
         return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
     }
 
+    private void setIpv6AcceptRa(int acceptRa) {
+        try {
+            mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceParams.name, "accept_ra",
+                    Integer.toString(acceptRa));
+        } catch (Exception e) {
+            Log.e(mTag, "Failed to set accept_ra to " + acceptRa);
+        }
+    }
+
+    private void restartIpv6WithAcceptRaDisabled() {
+        mInterfaceCtrl.disableIPv6();
+        setIpv6AcceptRa(0 /* accept_ra */);
+        startIPv6();
+    }
+
     // TODO: Investigate folding all this into the existing static function
     // LinkProperties.compareProvisioning() or some other single function that
     // takes two LinkProperties objects and returns a ProvisioningChange
@@ -1155,7 +1308,7 @@
         // Note that we can still be disconnected by IpReachabilityMonitor
         // if the IPv6 default gateway (but not the IPv6 DNS servers; see
         // accompanying code in IpReachabilityMonitor) is unreachable.
-        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledIPv6OnProvLoss
+        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledIpv6OrAcceptRaOnProvLoss
                 || (mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
                         && !mCm.shouldAvoidBadWifi());
 
@@ -1183,18 +1336,31 @@
         if (oldLp.hasGlobalIpv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
             // Although link properties have lost IPv6 default route in this case, if IPv4 is still
             // working with appropriate routes and DNS servers, we can keep the current connection
-            // without disconnecting from the network, just disable IPv6 on that given network until
-            // to the next provisioning. Disabling IPv6 will result in all IPv6 connectivity torn
-            // down and all IPv6 sockets being closed, the non-routable IPv6 DNS servers will be
-            // stripped out, so applications will be able to reconnect immediately over IPv4. See
-            // b/131781810.
+            // without disconnecting from the network, just disable IPv6 or accept_ra parameter on
+            // that given network until to the next provisioning.
+            //
+            // Disabling IPv6 stack will result in all IPv6 connectivity torn down and all IPv6
+            // sockets being closed, the non-routable IPv6 DNS servers will be stripped out, so
+            // applications will be able to reconnect immediately over IPv4. See b/131781810.
+            //
+            // Sometimes disabling IPv6 stack might introduce other issues(see b/179222860),
+            // instead disabling accept_ra will result in only IPv4 provisioning and IPv6 link
+            // local address left on the interface, so applications will be able to reconnect
+            // immediately over IPv4 and keep IPv6 link-local capable.
             if (newLp.isIpv4Provisioned()) {
-                mInterfaceCtrl.disableIPv6();
-                mHasDisabledIPv6OnProvLoss = true;
-                delta = PROV_CHANGE_STILL_PROVISIONED;
-                if (DBG) {
-                    mLog.log("Disable IPv6 stack completely when the default router has gone");
+                if (shouldDisableAcceptRaOnProvisioningLoss()) {
+                    restartIpv6WithAcceptRaDisabled();
+                } else {
+                    mInterfaceCtrl.disableIPv6();
                 }
+                mNetworkQuirkMetrics.setEvent(NetworkQuirkEvent.QE_IPV6_PROVISIONING_ROUTER_LOST);
+                mNetworkQuirkMetrics.statsWrite();
+                mHasDisabledIpv6OrAcceptRaOnProvLoss = true;
+                delta = PROV_CHANGE_STILL_PROVISIONED;
+                mLog.log(shouldDisableAcceptRaOnProvisioningLoss()
+                        ? "Disabled accept_ra parameter "
+                        : "Disabled IPv6 stack completely "
+                        + "when the IPv6 default router has gone");
             } else {
                 delta = PROV_CHANGE_LOST_PROVISIONING;
             }
@@ -1363,6 +1529,92 @@
         }
     }
 
+    private void transmitPacket(final ByteBuffer packet, final SocketAddress sockAddress,
+            final String msg) {
+        FileDescriptor sock = null;
+        try {
+            sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
+            Os.sendto(sock, packet.array(), 0 /* byteOffset */, packet.limit() /* byteCount */,
+                    0 /* flags */, sockAddress);
+        } catch (SocketException | ErrnoException e) {
+            logError(msg, e);
+        } finally {
+            NetworkStackUtils.closeSocketQuietly(sock);
+        }
+    }
+
+    private void sendGratuitousNA(final Inet6Address srcIp, final Inet6Address targetIp) {
+        final int flags = 0; // R=0, S=0, O=0
+        final Inet6Address dstIp = IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+        // Ethernet multicast destination address: 33:33:00:00:00:02.
+        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
+        final ByteBuffer packet = NeighborAdvertisement.build(mInterfaceParams.macAddr, dstMac,
+                srcIp, dstIp, flags, targetIp);
+        final SocketAddress sockAddress =
+                SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
+                        mInterfaceParams.index, dstMac.toByteArray());
+
+        transmitPacket(packet, sockAddress, "Failed to send Gratuitous Neighbor Advertisement");
+    }
+
+    private void sendGratuitousARP(final Inet4Address srcIp) {
+        final ByteBuffer packet = ArpPacket.buildArpPacket(ETHER_BROADCAST /* dstMac */,
+                mInterfaceParams.macAddr.toByteArray() /* srcMac */,
+                srcIp.getAddress() /* targetIp */,
+                ETHER_BROADCAST /* targetHwAddress */,
+                srcIp.getAddress() /* senderIp */, (short) ARP_REPLY);
+        final SocketAddress sockAddress =
+                makePacketSocketAddress(ETH_P_ARP, mInterfaceParams.index);
+
+        transmitPacket(packet, sockAddress, "Failed to send GARP");
+    }
+
+    private static Inet6Address getIpv6LinkLocalAddress(final LinkProperties newLp) {
+        for (LinkAddress la : newLp.getLinkAddresses()) {
+            if (!la.isIpv6()) continue;
+            final Inet6Address ip = (Inet6Address) la.getAddress();
+            if (ip.isLinkLocalAddress()) return ip;
+        }
+        return null;
+    }
+
+    private void maybeSendGratuitousNAs(final LinkProperties lp, boolean afterRoaming) {
+        if (!lp.hasGlobalIpv6Address()) return;
+
+        final Inet6Address srcIp = getIpv6LinkLocalAddress(lp);
+        if (srcIp == null) return;
+
+        // TODO: add experiment with sending only one gratuitous NA packet instead of one
+        // packet per address.
+        for (LinkAddress la : lp.getLinkAddresses()) {
+            if (!la.isIpv6() || !la.isGlobalPreferred()) continue;
+            final Inet6Address targetIp = (Inet6Address) la.getAddress();
+            // Already sent gratuitous NA with this target global IPv6 address. But for
+            // the L2 roaming case, device should always (re)transmit Gratuitous NA for
+            // each IPv6 global unicast address respectively after roaming.
+            if (!afterRoaming && mGratuitousNaTargetAddresses.contains(targetIp)) continue;
+            if (DBG) {
+                mLog.log("send Gratuitous NA from " + srcIp.getHostAddress() + " for "
+                        + targetIp.getHostAddress() + (afterRoaming ? " after roaming" : ""));
+            }
+            sendGratuitousNA(srcIp, targetIp);
+            if (!afterRoaming) mGratuitousNaTargetAddresses.add(targetIp);
+        }
+    }
+
+    private void maybeSendGratuitousARP(final LinkProperties lp) {
+        for (LinkAddress address : lp.getLinkAddresses()) {
+            if (address.getAddress() instanceof Inet4Address) {
+                final Inet4Address srcIp = (Inet4Address) address.getAddress();
+                if (DBG) {
+                    mLog.log("send GARP for " + srcIp.getHostAddress() + " HW address: "
+                            + mInterfaceParams.macAddr);
+                }
+                sendGratuitousARP(srcIp);
+            }
+        }
+    }
+
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
@@ -1370,6 +1622,13 @@
             return true;
         }
 
+        // Check if new assigned IPv6 GUA is available in the LinkProperties now. If so, initiate
+        // gratuitous multicast unsolicited Neighbor Advertisements as soon as possible to inform
+        // first-hop routers that the new GUA host is goning to use.
+        if (isGratuitousNaEnabled()) {
+            maybeSendGratuitousNAs(newLp, false /* isGratuitousNaAfterRoaming */);
+        }
+
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
         mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
@@ -1395,7 +1654,7 @@
         return ssid;
     }
 
-    private List<ByteBuffer> getVendorSpecificIEs(@NonNull ScanResultInfo scanResultInfo) {
+    private static List<ByteBuffer> getVendorSpecificIEs(@NonNull ScanResultInfo scanResultInfo) {
         ArrayList<ByteBuffer> vendorSpecificPayloadList = new ArrayList<>();
         for (InformationElement ie : scanResultInfo.getInformationElements()) {
             if (ie.getId() == VENDOR_SPECIFIC_IE_ID) {
@@ -1405,17 +1664,10 @@
         return vendorSpecificPayloadList;
     }
 
-    private boolean detectUpstreamHotspotFromVendorIe() {
-        if (mConfiguration.mScanResultInfo == null) return false;
-        final ScanResultInfo scanResultInfo = mConfiguration.mScanResultInfo;
-        final String ssid = scanResultInfo.getSsid();
+    private boolean checkIfOuiAndTypeMatched(@NonNull ScanResultInfo scanResultInfo,
+            @NonNull List<byte[]> patternList) {
         final List<ByteBuffer> vendorSpecificPayloadList = getVendorSpecificIEs(scanResultInfo);
 
-        if (mConfiguration.mDisplayName == null
-                || !removeDoubleQuotes(mConfiguration.mDisplayName).equals(ssid)) {
-            return false;
-        }
-
         for (ByteBuffer payload : vendorSpecificPayloadList) {
             byte[] ouiAndType = new byte[4];
             try {
@@ -1424,11 +1676,10 @@
                 Log.e(mTag, "Couldn't parse vendor specific IE, buffer underflow");
                 return false;
             }
-            for (byte[] pattern : METERED_IE_PATTERN_LIST) {
+            for (byte[] pattern : patternList) {
                 if (Arrays.equals(pattern, ouiAndType)) {
                     if (DBG) {
-                        Log.d(mTag, "detected upstream hotspot that matches OUI:"
-                                + HexDump.toHexString(ouiAndType));
+                        Log.d(mTag, "match pattern: " + HexDump.toHexString(ouiAndType));
                     }
                     return true;
                 }
@@ -1437,6 +1688,18 @@
         return false;
     }
 
+    private boolean detectUpstreamHotspotFromVendorIe() {
+        final ScanResultInfo scanResultInfo = mConfiguration.mScanResultInfo;
+        if (scanResultInfo == null) return false;
+        final String ssid = scanResultInfo.getSsid();
+
+        if (mConfiguration.mDisplayName == null
+                || !removeDoubleQuotes(mConfiguration.mDisplayName).equals(ssid)) {
+            return false;
+        }
+        return checkIfOuiAndTypeMatched(scanResultInfo, METERED_IE_PATTERN_LIST);
+    }
+
     private void handleIPv4Success(DhcpResults dhcpResults) {
         mDhcpResults = new DhcpResults(dhcpResults);
         final LinkProperties newLp = assembleLinkProperties();
@@ -1536,7 +1799,7 @@
 
     private boolean startIpReachabilityMonitor() {
         try {
-            mIpReachabilityMonitor = new IpReachabilityMonitor(
+            mIpReachabilityMonitor = mDependencies.getIpReachabilityMonitor(
                     mContext,
                     mInterfaceParams,
                     getHandler(),
@@ -1548,6 +1811,7 @@
                         }
                     },
                     mConfiguration.mUsingMultinetworkPolicyTracker,
+                    mDependencies.getIpReachabilityMonitorDeps(mContext, mInterfaceParams.name),
                     mNetd);
         } catch (IllegalArgumentException iae) {
             // Failed to start IpReachabilityMonitor. Log it and call
@@ -1582,7 +1846,11 @@
             return;
         }
 
-        if (params.index != mInterfaceParams.index) {
+        // Check whether "mInterfaceParams" is null or not to prevent the potential NPE
+        // introduced if the interface was initially not found, but came back before this
+        // method was called. See b/162808916 for more details. TODO: query the new interface
+        // parameters by the interface index instead and check that the index has not changed.
+        if (mInterfaceParams == null || params.index != mInterfaceParams.index) {
             Log.w(mTag, "interface: " + mInterfaceName + " has a different index: " + params.index);
             return;
         }
@@ -1601,14 +1869,29 @@
         mL2Key = info.l2Key;
         mCluster = info.cluster;
 
+        // Sometimes the wifi code passes in a null BSSID. Don't use Log.wtf in R because
+        // it's a known bug that will not be fixed in R.
         if (info.bssid == null || mCurrentBssid == null) {
-            Log.wtf(mTag, "bssid in the parcelable or current tracked bssid should be non-null");
+            final String msg = "bssid in the parcelable: " + info.bssid + " or "
+                    + "current tracked bssid: " + mCurrentBssid + " is null";
+            if (ShimUtils.isAtLeastS()) {
+                Log.wtf(mTag, msg);
+            } else {
+                Log.w(mTag, msg);
+            }
             return;
         }
 
         // If the BSSID has not changed, there is nothing to do.
         if (info.bssid.equals(mCurrentBssid)) return;
 
+        // Before trigger probing to the interesting neighbors, send Gratuitous ARP
+        // and Neighbor Advertisment in advance to propgate host's IPv4/v6 addresses.
+        if (isGratuitousArpNaRoamingEnabled()) {
+            maybeSendGratuitousARP(mLinkProperties);
+            maybeSendGratuitousNAs(mLinkProperties, true /* isGratuitousNaAfterRoaming */);
+        }
+
         if (mIpReachabilityMonitor != null) {
             mIpReachabilityMonitor.probeAll();
         }
@@ -1631,7 +1914,9 @@
         @Override
         public void enter() {
             stopAllIP();
-            mHasDisabledIPv6OnProvLoss = false;
+            setIpv6AcceptRa(2 /* accept_ra */);
+            mHasDisabledIpv6OrAcceptRaOnProvLoss = false;
+            mGratuitousNaTargetAddresses.clear();
 
             mLinkObserver.clearInterfaceParams();
             resetLinkProperties();
@@ -1743,11 +2028,36 @@
         return mConfiguration.mEnablePreconnection && mConfiguration.mStaticIpConfig == null;
     }
 
+    /**
+     * Check if the customized DHCP client options passed from Wi-Fi are allowed to be put
+     * in PRL or in the DHCP packet.
+     */
+    private List<DhcpOption> maybeFilterCustomizedDhcpOptions() {
+        final List<DhcpOption> options = new ArrayList<DhcpOption>();
+        if (mConfiguration.mDhcpOptions == null
+                || mConfiguration.mScanResultInfo == null) return options; // empty DhcpOption list
+
+        for (DhcpOption option : mConfiguration.mDhcpOptions) {
+            final List<byte[]> patternList = DHCP_OPTIONS_ALLOWED.get(option.type);
+            // requested option won't be added if no vendor-specific IE oui/type allows this option.
+            if (patternList == null) continue;
+            if (checkIfOuiAndTypeMatched(mConfiguration.mScanResultInfo, patternList)) {
+                options.add(option);
+            }
+        }
+        Collections.sort(options, (o1, o2) ->
+                Integer.compare(Byte.toUnsignedInt(o1.type), Byte.toUnsignedInt(o2.type)));
+        return options;
+    }
+
     private void startDhcpClient() {
         // Start DHCPv4.
         mDhcpClient = mDependencies.makeDhcpClient(mContext, IpClient.this, mInterfaceParams,
                 mDependencies.getDhcpClientDependencies(mIpMemoryStore, mIpProvisioningMetrics));
 
+        // Check if the vendor-specific IE oui/type matches and filters the customized DHCP options.
+        final List<DhcpOption> options = maybeFilterCustomizedDhcpOptions();
+
         // If preconnection is enabled, there is no need to ask Wi-Fi to disable powersaving
         // during DHCP, because the DHCP handshake will happen during association. In order to
         // ensure that future renews still do the DHCP action (if configured),
@@ -1755,7 +2065,7 @@
         // messages.
         if (!isUsingPreconnection()) mDhcpClient.registerForPreDhcpNotification();
         mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP, new DhcpClient.Configuration(mL2Key,
-                isUsingPreconnection()));
+                isUsingPreconnection(), options));
     }
 
     class ClearingIpAddressesState extends State {
@@ -1926,10 +2236,19 @@
             apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;
             apfConfig.multicastFilter = mMulticastFiltering;
             // Get the Configuration for ApfFilter from Context
-            apfConfig.ieee802_3Filter = ApfCapabilities.getApfDrop8023Frames();
-            apfConfig.ethTypeBlackList = ApfCapabilities.getApfEtherTypeBlackList();
+            // Resource settings were moved from ApfCapabilities APIs to NetworkStack resources in S
+            if (ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R)) {
+                final Resources res = mContext.getResources();
+                apfConfig.ieee802_3Filter = res.getBoolean(R.bool.config_apfDrop802_3Frames);
+                apfConfig.ethTypeBlackList = res.getIntArray(R.array.config_apfEthTypeDenyList);
+            } else {
+                apfConfig.ieee802_3Filter = ApfCapabilities.getApfDrop8023Frames();
+                apfConfig.ethTypeBlackList = ApfCapabilities.getApfEtherTypeBlackList();
+            }
+
             apfConfig.minRdnssLifetimeSec = mMinRdnssLifetimeSec;
-            mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
+            mApfFilter = mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
+                    mCallback);
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
             // rest of this IP configuration startup.
             if (mApfFilter == null) {
@@ -2156,7 +2475,8 @@
                 //     a) initial address acquisition succeeds,
                 //     b) renew succeeds or is NAK'd,
                 //     c) rebind succeeds or is NAK'd, or
-                //     c) the lease expires,
+                //     d) the lease expires, or
+                //     e) the IPv6-only preferred option is enabled and entering Ipv6OnlyWaitState.
                 //
                 // but never when initial address acquisition fails. The latter
                 // condition is now governed by the provisioning timeout.
@@ -2170,6 +2490,8 @@
                         case DhcpClient.DHCP_FAILURE:
                             handleIPv4Failure();
                             break;
+                        case DhcpClient.DHCP_IPV6_ONLY:
+                            break;
                         default:
                             logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
                     }
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 82f8d5d..cc4a851 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -18,7 +18,7 @@
 
 import static android.system.OsConstants.AF_INET6;
 
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 
 import android.app.AlarmManager;
 import android.content.Context;
@@ -114,6 +114,7 @@
     private DnsServerRepository mDnsServerRepository;
     private final AlarmManager mAlarmManager;
     private final Configuration mConfig;
+    private final Handler mHandler;
 
     private final MyNetlinkMonitor mNetlinkMonitor;
 
@@ -127,11 +128,16 @@
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
         mConfig = config;
+        mHandler = h;
         mInterfaceLinkState = true; // Assume up by default
         mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
-        h.post(mNetlinkMonitor::start);
+        mHandler.post(mNetlinkMonitor::start);
+    }
+
+    public void shutdown() {
+        mHandler.post(mNetlinkMonitor::stop);
     }
 
     private void maybeLog(String operation, String iface, LinkAddress address) {
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 3dbe662..89c70a9 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -20,6 +20,8 @@
 import static android.net.metrics.IpReachabilityEvent.NUD_FAILED_ORGANIC;
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST;
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC;
+import static android.net.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -43,8 +45,12 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.R;
 
 import java.io.PrintWriter;
@@ -143,6 +149,8 @@
     protected static final int MIN_NUD_SOLICIT_NUM = 5;
     protected static final int MAX_NUD_SOLICIT_INTERVAL_MS = 1000;
     protected static final int MIN_NUD_SOLICIT_INTERVAL_MS = 750;
+    protected static final int NUD_MCAST_RESOLICIT_NUM = 3;
+    private static final int INVALID_NUD_MCAST_RESOLICIT_NUM = -1;
 
     public interface Callback {
         /**
@@ -161,6 +169,7 @@
     interface Dependencies {
         void acquireWakeLock(long durationMs);
         IpNeighborMonitor makeIpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb);
+        boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled);
 
         static Dependencies makeDefault(Context context, String iface) {
             final String lockName = TAG + "." + iface;
@@ -176,6 +185,12 @@
                         NeighborEventConsumer cb) {
                     return new IpNeighborMonitor(h, log, cb);
                 }
+
+                public boolean isFeatureEnabled(final Context context, final String name,
+                        boolean defaultEnabled) {
+                    return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
+                            defaultEnabled);
+                }
             };
         }
     }
@@ -183,7 +198,6 @@
     private final InterfaceParams mInterfaceParams;
     private final IpNeighborMonitor mIpNeighborMonitor;
     private final SharedLog mLog;
-    private final Callback mCallback;
     private final Dependencies mDependencies;
     private final boolean mUsingMultinetworkPolicyTracker;
     private final ConnectivityManager mCm;
@@ -196,12 +210,14 @@
     private volatile long mLastProbeTimeMs;
     private int mNumSolicits;
     private int mInterSolicitIntervalMs;
+    @NonNull
+    private final Callback mCallback;
 
     public IpReachabilityMonitor(
             Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
-            boolean usingMultinetworkPolicyTracker, final INetd netd) {
-        this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker,
-                Dependencies.makeDefault(context, ifParams.name), new IpConnectivityLog(), netd);
+            boolean usingMultinetworkPolicyTracker, Dependencies dependencies, final INetd netd) {
+        this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker, dependencies,
+                new IpConnectivityLog(), netd);
     }
 
     @VisibleForTesting
@@ -225,7 +241,10 @@
         // In case the overylaid parameters specify an invalid configuration, set the parameters
         // to the hardcoded defaults first, then set them to the values used in the steady state.
         try {
-            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS);
+            int numResolicits = isMulticastResolicitEnabled()
+                    ? NUD_MCAST_RESOLICIT_NUM
+                    : INVALID_NUD_MCAST_RESOLICIT_NUM;
+            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, numResolicits);
         } catch (Exception e) {
             Log.e(TAG, "Failed to adjust neighbor parameters with hardcoded defaults");
         }
@@ -241,10 +260,12 @@
                     // TODO: Consider what to do with other states that are not within
                     // NeighborEvent#isValid() (i.e. NUD_NONE, NUD_INCOMPLETE).
                     if (event.nudState == StructNdMsg.NUD_FAILED) {
+                        // After both unicast probe and multicast probe(if mcast_resolicit is not 0)
+                        // attempts fail, trigger the neighbor lost event and disconnect.
                         mLog.w("ALERT neighbor went from: " + prev + " to: " + event);
                         handleNeighborLost(event);
                     } else if (event.nudState == StructNdMsg.NUD_REACHABLE) {
-                        maybeRestoreNeighborParameters();
+                        handleNeighborReachable(prev, event);
                     }
                 });
         mIpNeighborMonitor.start();
@@ -296,6 +317,26 @@
         return false;
     }
 
+    private boolean hasDefaultRouterNeighborMacAddressChanged(
+            @Nullable final NeighborEvent prev, @NonNull final NeighborEvent event) {
+        if (prev == null || !isNeighborDefaultRouter(event)) return false;
+        return !event.macAddr.equals(prev.macAddr);
+    }
+
+    private boolean isNeighborDefaultRouter(@NonNull final NeighborEvent event) {
+        // For the IPv6 link-local scoped address, equals() works because the NeighborEvent.ip
+        // doesn't have a scope id and Inet6Address#equals doesn't consider scope id neither.
+        for (RouteInfo route : mLinkProperties.getRoutes()) {
+            if (route.isDefaultRoute() && event.ip.equals(route.getGateway())) return true;
+        }
+        return false;
+    }
+
+    private boolean isMulticastResolicitEnabled() {
+        return mDependencies.isFeatureEnabled(mContext, IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
+                false /* defaultEnabled */);
+    }
+
     public void updateLinkProperties(LinkProperties lp) {
         if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
             // TODO: figure out whether / how to cope with interface changes.
@@ -333,6 +374,24 @@
         if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); }
     }
 
+    private void handleNeighborReachable(@Nullable final NeighborEvent prev,
+            @NonNull final NeighborEvent event) {
+        if (isMulticastResolicitEnabled()
+                && hasDefaultRouterNeighborMacAddressChanged(prev, event)) {
+            // This implies device has confirmed the neighbor's reachability from
+            // other states(e.g., NUD_PROBE or NUD_STALE), checking if the mac
+            // address hasn't changed is required. If Mac address does change, then
+            // trigger a new neighbor lost event and disconnect.
+            final String logMsg = "ALERT neighbor: " + event.ip
+                    + " MAC address changed from: " + prev.macAddr
+                    + " to: " + event.macAddr;
+            mLog.w(logMsg);
+            mCallback.notifyLost(event.ip, logMsg);
+            return;
+        }
+        maybeRestoreNeighborParameters();
+    }
+
     private void handleNeighborLost(NeighborEvent event) {
         final LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
 
@@ -370,11 +429,9 @@
         if (lostProvisioning) {
             final String logMsg = "FAILURE: LOST_PROVISIONING, " + event;
             Log.w(TAG, logMsg);
-            if (mCallback != null) {
-                // TODO: remove |ip| when the callback signature no longer has
-                // an InetAddress argument.
-                mCallback.notifyLost(ip, logMsg);
-            }
+            // TODO: remove |ip| when the callback signature no longer has
+            // an InetAddress argument.
+            mCallback.notifyLost(ip, logMsg);
         }
         logNudFailed(lostProvisioning);
     }
@@ -450,6 +507,12 @@
 
     private void setNeighborParameters(int numSolicits, int interSolicitIntervalMs)
             throws RemoteException, IllegalArgumentException {
+        // Do not set mcast_resolicit param by default.
+        setNeighborParameters(numSolicits, interSolicitIntervalMs, INVALID_NUD_MCAST_RESOLICIT_NUM);
+    }
+
+    private void setNeighborParameters(int numSolicits, int interSolicitIntervalMs,
+            int numResolicits) throws RemoteException, IllegalArgumentException {
         Preconditions.checkArgument(numSolicits >= MIN_NUD_SOLICIT_NUM,
                 "numSolicits must be at least " + MIN_NUD_SOLICIT_NUM);
         Preconditions.checkArgument(numSolicits <= MAX_NUD_SOLICIT_NUM,
@@ -464,6 +527,10 @@
                     Integer.toString(interSolicitIntervalMs));
             mNetd.setProcSysNet(family, INetd.NEIGH, mInterfaceParams.name, "ucast_solicit",
                     Integer.toString(numSolicits));
+            if (numResolicits != INVALID_NUD_MCAST_RESOLICIT_NUM) {
+                mNetd.setProcSysNet(family, INetd.NEIGH, mInterfaceParams.name, "mcast_resolicit",
+                        Integer.toString(numResolicits));
+            }
         }
 
         mNumSolicits = numSolicits;
diff --git a/src/android/net/util/ConnectivityPacketSummary.java b/src/android/net/util/ConnectivityPacketSummary.java
index 8164c00..c1b6c4f 100644
--- a/src/android/net/util/ConnectivityPacketSummary.java
+++ b/src/android/net/util/ConnectivityPacketSummary.java
@@ -19,42 +19,42 @@
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
-import static com.android.server.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
-import static com.android.server.util.NetworkStackConstants.ARP_PAYLOAD_LEN;
-import static com.android.server.util.NetworkStackConstants.ARP_REPLY;
-import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
-import static com.android.server.util.NetworkStackConstants.DHCP4_CLIENT_PORT;
-import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
-import static com.android.server.util.NetworkStackConstants.ETHER_HEADER_LEN;
-import static com.android.server.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_ARP;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV4;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV6;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MIN_LENGTH;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV4_DST_ADDR_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV4_FLAGS_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV4_FRAGMENT_MASK;
-import static com.android.server.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV4_IHL_MASK;
-import static com.android.server.util.NetworkStackConstants.IPV4_PROTOCOL_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV4_SRC_ADDR_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV6_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV6_HEADER_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV6_SRC_ADDR_OFFSET;
-import static com.android.server.util.NetworkStackConstants.UDP_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
+import static com.android.net.module.util.NetworkStackConstants.ARP_PAYLOAD_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.DHCP4_CLIENT_PORT;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_ARP;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MIN_LENGTH;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+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;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_DST_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_FLAGS_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_FRAGMENT_MASK;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IHL_MASK;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_PROTOCOL_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_SRC_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_SRC_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
 
 import android.net.MacAddress;
 import android.net.dhcp.DhcpPacket;
diff --git a/src/android/net/util/DataStallUtils.java b/src/android/net/util/DataStallUtils.java
index 2c5de57..4b25967 100644
--- a/src/android/net/util/DataStallUtils.java
+++ b/src/android/net/util/DataStallUtils.java
@@ -31,11 +31,12 @@
     /** Detect data stall using tcp connection fail rate. */
     public static final int DATA_STALL_EVALUATION_TYPE_TCP = 1 << 1;
 
-    @IntDef(prefix = { "DATA_STALL_EVALUATION_TYPE_" }, value = {
-        DATA_STALL_EVALUATION_TYPE_NONE,
-        DATA_STALL_EVALUATION_TYPE_DNS,
-        DATA_STALL_EVALUATION_TYPE_TCP,
-    })
+    @IntDef(prefix = { "DATA_STALL_EVALUATION_TYPE_" },
+        flag = true,
+        value = {
+            DATA_STALL_EVALUATION_TYPE_NONE,
+            DATA_STALL_EVALUATION_TYPE_DNS,
+            DATA_STALL_EVALUATION_TYPE_TCP, })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EvaluationType {
     }
diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index 94de7c3..e06cdca 100755
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -17,22 +17,17 @@
 package android.net.util;
 
 import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.provider.DeviceConfig;
-import android.util.Log;
-import android.util.SparseArray;
+import android.net.MacAddress;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import com.android.net.module.util.DeviceConfigUtils;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
-import java.net.InetAddress;
 import java.net.SocketException;
-import java.util.List;
-import java.util.function.Predicate;
 
 /**
  * Collection of utilities for the network stack.
@@ -210,6 +205,12 @@
     public static final String DHCP_IP_CONFLICT_DETECT_VERSION = "dhcp_ip_conflict_detect_version";
 
     /**
+     * Minimum module version at which to enable the IPv6-Only preferred option.
+     */
+    public static final String DHCP_IPV6_ONLY_PREFERRED_VERSION =
+            "dhcp_ipv6_only_preferred_version";
+
+    /**
      * Minimum module version at which to enable dismissal CaptivePortalLogin app in validated
      * network feature. CaptivePortalLogin app will also use validation facilities in
      * {@link NetworkMonitor} to perform portal validation if feature is enabled.
@@ -231,19 +232,38 @@
      *
      * Metrics are sent by default. They can be disabled by setting the flag to a number greater
      * than the APK version (for example 999999999).
-     * @see #isFeatureEnabled(Context, String, String, boolean)
+     * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String, boolean)
      */
     public static final String VALIDATION_METRICS_VERSION = "validation_metrics_version";
 
-    static {
-        System.loadLibrary("networkstackutilsjni");
-    }
+    /**
+     * Experiment flag to enable sending gratuitous multicast unsolicited Neighbor Advertisements
+     * to propagate new assigned IPv6 GUA as quickly as possible.
+     */
+    public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";
 
     /**
-     * @return True if the array is null or 0-length.
+     * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for
+     * all assigned IPv4 and IPv6 GUAs after completing L2 roaming.
      */
-    public static <T> boolean isEmpty(T[] array) {
-        return array == null || array.length == 0;
+    public static final String IPCLIENT_GARP_NA_ROAMING_VERSION =
+            "ipclient_garp_na_roaming_version";
+
+    /**
+     * Experiment flag to disable accept_ra parameter when IPv6 provisioning loss happens due to
+     * the default route has gone.
+     */
+    public static final String IPCLIENT_DISABLE_ACCEPT_RA_VERSION = "ipclient_disable_accept_ra";
+
+    /**
+     * Experiment flag to enable "mcast_resolicit" neighbor parameter in IpReachabilityMonitor,
+     * set it to 3 by default.
+     */
+    public static final String IP_REACHABILITY_MCAST_RESOLICIT_VERSION =
+            "ip_reachability_mcast_resolicit_version";
+
+    static {
+        System.loadLibrary("networkstackutilsjni");
     }
 
     /**
@@ -257,147 +277,18 @@
     }
 
     /**
-     * Returns an int array from the given Integer list.
+     * Convert IPv6 multicast address to ethernet multicast address in network order.
      */
-    public static int[] convertToIntArray(@NonNull List<Integer> list) {
-        int[] array = new int[list.size()];
-        for (int i = 0; i < list.size(); i++) {
-            array[i] = list.get(i);
-        }
-        return array;
-    }
-
-    /**
-     * Returns a long array from the given long list.
-     */
-    public static long[] convertToLongArray(@NonNull List<Long> list) {
-        long[] array = new long[list.size()];
-        for (int i = 0; i < list.size(); i++) {
-            array[i] = list.get(i);
-        }
-        return array;
-    }
-
-    /**
-     * @return True if there exists at least one element in the sparse array for which
-     * condition {@code predicate}
-     */
-    public static <T> boolean any(SparseArray<T> array, Predicate<T> predicate) {
-        for (int i = 0; i < array.size(); ++i) {
-            if (predicate.test(array.valueAt(i))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @param defaultValue The value to return if the property does not exist or has no valid value.
-     * @return the corresponding value, or defaultValue if none exists.
-     */
-    @Nullable
-    public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
-            @Nullable String defaultValue) {
-        String value = DeviceConfig.getProperty(namespace, name);
-        return value != null ? value : defaultValue;
-    }
-
-    /**
-     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @param defaultValue The value to return if the property does not exist or its value is null.
-     * @return the corresponding value, or defaultValue if none exists.
-     */
-    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
-            int defaultValue) {
-        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
-        try {
-            return (value != null) ? Integer.parseInt(value) : defaultValue;
-        } catch (NumberFormatException e) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @param minimumValue The minimum value of a property.
-     * @param maximumValue The maximum value of a property.
-     * @param defaultValue The value to return if the property does not exist or its value is null.
-     * @return the corresponding value, or defaultValue if none exists or the fetched value is
-     *         greater than maximumValue.
-     */
-    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
-            int minimumValue, int maximumValue, int defaultValue) {
-        int value = getDeviceConfigPropertyInt(namespace, name, defaultValue);
-        if (value < minimumValue || value > maximumValue) return defaultValue;
-        return value;
-    }
-
-    /**
-     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @param defaultValue The value to return if the property does not exist or its value is null.
-     * @return the corresponding value, or defaultValue if none exists.
-     */
-    public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace,
-            @NonNull String name, boolean defaultValue) {
-        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
-        return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
-    }
-
-    /**
-     * Check whether or not one specific experimental feature for a particular namespace from
-     * {@link DeviceConfig} is enabled by comparing NetworkStack module version {@link NetworkStack}
-     * with current version of property. If this property version is valid, the corresponding
-     * experimental feature would be enabled, otherwise disabled.
-     *
-     * This is useful to ensure that if a module install is rolled back, flags are not left fully
-     * rolled out on a version where they have not been well tested.
-     * @param context The global context information about an app environment.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @return true if this feature is enabled, or false if disabled.
-     */
-    public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-            @NonNull String name) {
-        return isFeatureEnabled(context, namespace, name, false /* defaultEnabled */);
-    }
-
-    /**
-     * Check whether or not one specific experimental feature for a particular namespace from
-     * {@link DeviceConfig} is enabled by comparing NetworkStack module version {@link NetworkStack}
-     * with current version of property. If this property version is valid, the corresponding
-     * experimental feature would be enabled, otherwise disabled.
-     *
-     * This is useful to ensure that if a module install is rolled back, flags are not left fully
-     * rolled out on a version where they have not been well tested.
-     * @param context The global context information about an app environment.
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @param defaultEnabled The value to return if the property does not exist or its value is
-     *                       null.
-     * @return true if this feature is enabled, or false if disabled.
-     */
-    public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-            @NonNull String name, boolean defaultEnabled) {
-        try {
-            final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
-                    0 /* default value */);
-            final long packageVersion = context.getPackageManager().getPackageInfo(
-                    context.getPackageName(), 0).getLongVersionCode();
-            return (propertyVersion == 0 && defaultEnabled)
-                    || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "Could not find the package name", e);
-            return false;
-        }
+    public static MacAddress ipv6MulticastToEthernetMulticast(@NonNull final Inet6Address addr) {
+        final byte[] etherMulticast = new byte[6];
+        final byte[] ipv6Multicast = addr.getAddress();
+        etherMulticast[0] = (byte) 0x33;
+        etherMulticast[1] = (byte) 0x33;
+        etherMulticast[2] = ipv6Multicast[12];
+        etherMulticast[3] = ipv6Multicast[13];
+        etherMulticast[4] = ipv6Multicast[14];
+        etherMulticast[5] = ipv6Multicast[15];
+        return MacAddress.fromBytes(etherMulticast);
     }
 
     /**
@@ -435,38 +326,4 @@
     private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname,
             FileDescriptor fd) throws IOException;
 
-    /**
-     * Return IP address and port in a string format.
-     */
-    public static String addressAndPortToString(InetAddress address, int port) {
-        return String.format(
-                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
-                        address.getHostAddress(), port);
-    }
-
-    /**
-     * Return true if the provided address is non-null and an IPv6 Unique Local Address (RFC4193).
-     */
-    public static boolean isIPv6ULA(@Nullable InetAddress addr) {
-        return addr instanceof Inet6Address
-                && ((addr.getAddress()[0] & 0xfe) == 0xfc);
-    }
-
-    /**
-     * Returns the {@code int} nearest in value to {@code value}.
-     *
-     * @param value any {@code long} value
-     * @return the same value cast to {@code int} if it is in the range of the {@code int}
-     * type, {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if
-     * it is too small
-     */
-    public static int saturatedCast(long value) {
-        if (value > Integer.MAX_VALUE) {
-            return Integer.MAX_VALUE;
-        }
-        if (value < Integer.MIN_VALUE) {
-            return Integer.MIN_VALUE;
-        }
-        return (int) value;
-    }
 }
diff --git a/src/com/android/networkstack/NetworkStackNotifier.java b/src/com/android/networkstack/NetworkStackNotifier.java
index dbb62b1..acf3c95 100644
--- a/src/com/android/networkstack/NetworkStackNotifier.java
+++ b/src/com/android/networkstack/NetworkStackNotifier.java
@@ -198,7 +198,6 @@
         // Don't show the notification when SSID is unknown to prevent sending something vague to
         // the user.
         final boolean hasSsid = !TextUtils.isEmpty(getSsid(networkStatus));
-
         final CaptivePortalDataShim capportData = getCaptivePortalData(networkStatus);
         final boolean showVenueInfo = capportData != null && capportData.getVenueInfoUrl() != null
                 // Only show venue info on validated networks, to prevent misuse of the notification
@@ -235,7 +234,14 @@
             // channel even if the notification contains venue info: the "venue info" notification
             // then doubles as a "connected" notification.
             final String channel = showValidated ? CHANNEL_CONNECTED : CHANNEL_VENUE_INFO;
-            builder = getNotificationBuilder(channel, networkStatus, res, getSsid(networkStatus))
+
+            // If the venue friendly name is available (in Passpoint use-case), display it.
+            // Otherwise, display the SSID.
+            final CharSequence friendlyName = capportData.getVenueFriendlyName();
+            final CharSequence venueDisplayName = TextUtils.isEmpty(friendlyName)
+                    ? getSsid(networkStatus) : friendlyName;
+
+            builder = getNotificationBuilder(channel, networkStatus, res, venueDisplayName)
                     .setContentText(res.getString(R.string.tap_for_info))
                     .setContentIntent(mDependencies.getActivityPendingIntent(
                             getContextAsUser(mContext, UserHandle.CURRENT),
@@ -278,9 +284,9 @@
 
     private Notification.Builder getNotificationBuilder(@NonNull String channelId,
             @NonNull TrackedNetworkStatus networkStatus, @NonNull Resources res,
-            @NonNull String ssid) {
+            @NonNull CharSequence networkIdentifier) {
         return new Notification.Builder(mContext, channelId)
-                .setContentTitle(ssid)
+                .setContentTitle(networkIdentifier)
                 .setSmallIcon(R.drawable.icon_wifi);
     }
 
diff --git a/src/com/android/networkstack/arp/ArpPacket.java b/src/com/android/networkstack/arp/ArpPacket.java
index b84ff2b..a25d7bf 100644
--- a/src/com/android/networkstack/arp/ArpPacket.java
+++ b/src/com/android/networkstack/arp/ArpPacket.java
@@ -19,12 +19,12 @@
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IP;
 
-import static com.android.server.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN;
-import static com.android.server.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
-import static com.android.server.util.NetworkStackConstants.ARP_REPLY;
-import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
-import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
 
 import android.net.MacAddress;
 
diff --git a/src/com/android/networkstack/metrics/DataStallDetectionStats.java b/src/com/android/networkstack/metrics/DataStallDetectionStats.java
index 4de2ec0..751d98d 100644
--- a/src/com/android/networkstack/metrics/DataStallDetectionStats.java
+++ b/src/com/android/networkstack/metrics/DataStallDetectionStats.java
@@ -16,7 +16,6 @@
 
 package com.android.networkstack.metrics;
 
-import android.net.util.NetworkStackUtils;
 import android.net.wifi.WifiInfo;
 
 import androidx.annotation.IntRange;
@@ -25,6 +24,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.util.HexDump;
+import com.android.net.module.util.CollectionUtils;
 import com.android.server.connectivity.nano.CellularData;
 import com.android.server.connectivity.nano.DataStallEventProto;
 import com.android.server.connectivity.nano.DnsEvent;
@@ -194,9 +194,9 @@
         }
 
         /**
-         * Set the dns evaluation type into Builder.
+         * Set the data stall evaluation type into Builder.
          *
-         * @param type the return code of the dns event.
+         * @param type the signal type causing a data stall to be suspected.
          * @return {@code this} {@link Builder} instance.
          */
         public Builder setEvaluationType(int type) {
@@ -299,8 +299,8 @@
          */
         public DataStallDetectionStats build() {
             return new DataStallDetectionStats(mCellularInfo, mWifiInfo,
-                    NetworkStackUtils.convertToIntArray(mDnsReturnCode),
-                    NetworkStackUtils.convertToLongArray(mDnsTimeStamp),
+                    CollectionUtils.toIntArray(mDnsReturnCode),
+                    CollectionUtils.toLongArray(mDnsTimeStamp),
                     mEvaluationType, mNetworkType, mTcpFailRate, mTcpSentSinceLastRecv);
         }
     }
diff --git a/src/com/android/networkstack/metrics/IpProvisioningMetrics.java b/src/com/android/networkstack/metrics/IpProvisioningMetrics.java
index 64e173d..b015a51 100644
--- a/src/com/android/networkstack/metrics/IpProvisioningMetrics.java
+++ b/src/com/android/networkstack/metrics/IpProvisioningMetrics.java
@@ -16,13 +16,14 @@
 
 package com.android.networkstack.metrics;
 
-import android.net.util.NetworkStackUtils;
 import android.net.util.Stopwatch;
 import android.stats.connectivity.DhcpErrorCode;
 import android.stats.connectivity.DhcpFeature;
 import android.stats.connectivity.DisconnectCode;
 import android.stats.connectivity.HostnameTransResult;
 
+import com.android.net.module.util.ConnectivityUtils;
+
 import java.util.HashSet;
 import java.util.Set;
 
@@ -70,7 +71,7 @@
      */
     public void setIPv4ProvisionedLatencyOnFirstTime(final boolean isIpv4Provisioned) {
         if (isIpv4Provisioned && !mStatsBuilder.hasIpv4LatencyMicros()) {
-            mStatsBuilder.setIpv4LatencyMicros(NetworkStackUtils.saturatedCast(mIpv4Watch.stop()));
+            mStatsBuilder.setIpv4LatencyMicros(ConnectivityUtils.saturatedCast(mIpv4Watch.stop()));
         }
     }
 
@@ -79,7 +80,7 @@
      */
     public void setIPv6ProvisionedLatencyOnFirstTime(final boolean isIpv6Provisioned) {
         if (isIpv6Provisioned && !mStatsBuilder.hasIpv6LatencyMicros()) {
-            mStatsBuilder.setIpv6LatencyMicros(NetworkStackUtils.saturatedCast(mIpv6Watch.stop()));
+            mStatsBuilder.setIpv6LatencyMicros(ConnectivityUtils.saturatedCast(mIpv6Watch.stop()));
         }
     }
 
diff --git a/src/com/android/networkstack/metrics/NetworkQuirkMetrics.java b/src/com/android/networkstack/metrics/NetworkQuirkMetrics.java
new file mode 100644
index 0000000..dee4504
--- /dev/null
+++ b/src/com/android/networkstack/metrics/NetworkQuirkMetrics.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.metrics;
+
+import android.stats.connectivity.NetworkQuirkEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Class to record the network Quirk event into statsd.
+ * @hide
+ */
+public class NetworkQuirkMetrics {
+    private final Dependencies mDependencies;
+    private final NetworkStackQuirkReported.Builder mStatsBuilder =
+            NetworkStackQuirkReported.newBuilder();
+    /**
+     * Dependencies of {@link NetworkQuirkMetrics}, useful for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see NetworkStackStatsLog#write.
+         */
+        public void writeStats(int event) {
+            NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_STACK_QUIRK_REPORTED,
+                    0, event);
+        }
+    }
+
+    /**
+     * Get a NetworkQuirkMetrics instance.
+     */
+    public NetworkQuirkMetrics() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting
+    public NetworkQuirkMetrics(Dependencies deps) {
+        mDependencies = deps;
+    }
+
+    /**
+     * Write the network Quirk Event into mStatsBuilder.
+     */
+    public void setEvent(NetworkQuirkEvent event) {
+        mStatsBuilder.setEvent(event);
+    }
+
+    /**
+     * Write the NetworkStackQuirkReported proto into statsd.
+     */
+    public NetworkStackQuirkReported statsWrite() {
+        final NetworkStackQuirkReported stats = mStatsBuilder.build();
+        mDependencies.writeStats(stats.getEvent().getNumber());
+        return stats;
+    }
+}
diff --git a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
index f27a939..76bd5fc 100644
--- a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
+++ b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
@@ -30,7 +30,6 @@
 import android.net.NetworkCapabilities;
 import android.net.captiveportal.CaptivePortalProbeResult;
 import android.net.metrics.ValidationProbeEvent;
-import android.net.util.NetworkStackUtils;
 import android.net.util.Stopwatch;
 import android.stats.connectivity.ProbeResult;
 import android.stats.connectivity.ProbeType;
@@ -40,6 +39,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.net.module.util.ConnectivityUtils;
 import com.android.networkstack.apishim.common.CaptivePortalDataShim;
 
 /**
@@ -195,7 +195,7 @@
         // many probes are skipped.
         if (mProbeEventsBuilder.getProbeEventCount() >= MAX_PROBE_EVENTS_COUNT) return;
 
-        int latencyUs = NetworkStackUtils.saturatedCast(durationUs);
+        int latencyUs = ConnectivityUtils.saturatedCast(durationUs);
 
         final ProbeEvent.Builder probeEventBuilder = ProbeEvent.newBuilder()
                 .setLatencyMicros(latencyUs)
@@ -206,10 +206,10 @@
             final long secondsRemaining =
                     (capportData.getExpiryTimeMillis() - currentTimeMillis()) / 1000;
             mCapportApiDataBuilder
-                .setRemainingTtlSecs(NetworkStackUtils.saturatedCast(secondsRemaining))
+                .setRemainingTtlSecs(ConnectivityUtils.saturatedCast(secondsRemaining))
                 // TODO: rename this field to setRemainingKBytes, or use a long
                 .setRemainingBytes(
-                        NetworkStackUtils.saturatedCast(capportData.getByteLimit() / 1000))
+                        ConnectivityUtils.saturatedCast(capportData.getByteLimit() / 1000))
                 .setHasPortalUrl((capportData.getUserPortalUrl() != null))
                 .setHasVenueInfo((capportData.getVenueInfoUrl() != null));
             probeEventBuilder.setCapportApiData(mCapportApiDataBuilder);
@@ -234,7 +234,7 @@
     public NetworkValidationReported maybeStopCollectionAndSend() {
         if (!mWatch.isStarted()) return null;
         mStatsBuilder.setProbeEvents(mProbeEventsBuilder);
-        mStatsBuilder.setLatencyMicros(NetworkStackUtils.saturatedCast(mWatch.stop()));
+        mStatsBuilder.setLatencyMicros(ConnectivityUtils.saturatedCast(mWatch.stop()));
         mStatsBuilder.setValidationIndex(mValidationIndex);
         // write a random value(0 ~ 999) for sampling.
         mStatsBuilder.setRandomNumber((int) (Math.random() * 1000));
diff --git a/src/com/android/networkstack/metrics/stats.proto b/src/com/android/networkstack/metrics/stats.proto
index 8a94db9..2b0a704 100644
--- a/src/com/android/networkstack/metrics/stats.proto
+++ b/src/com/android/networkstack/metrics/stats.proto
@@ -19,7 +19,7 @@
 
 package com.android.networkstack.metrics;
 
-import "frameworks/base/core/proto/android/stats/connectivity/network_stack.proto";
+import "frameworks/proto_logging/stats/enums/stats/connectivity/network_stack.proto";
 
 message CapportApiData {
     // The TTL of the network connection provided by captive portal
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java
index ef33f13..770e85a 100644
--- a/src/com/android/networkstack/netlink/TcpSocketTracker.java
+++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -65,6 +65,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.apishim.NetworkShimImpl;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
@@ -569,7 +570,7 @@
          */
         public int getDeviceConfigPropertyInt(@NonNull final String namespace,
                 @NonNull final String name, final int defaultValue) {
-            return NetworkStackUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
         }
 
         /**
diff --git a/src/com/android/networkstack/packets/NeighborAdvertisement.java b/src/com/android/networkstack/packets/NeighborAdvertisement.java
new file mode 100644
index 0000000..ef38314
--- /dev/null
+++ b/src/com/android/networkstack/packets/NeighborAdvertisement.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.packets;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+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.LlaOption;
+import com.android.net.module.util.structs.NaHeader;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * Defines basic data and operations needed to build and parse Neighbor Advertisement packet.
+ *
+ * @hide
+ */
+public class NeighborAdvertisement {
+    @NonNull
+    public final EthernetHeader ethHdr;
+    @NonNull
+    public final Ipv6Header ipv6Hdr;
+    @NonNull
+    public final Icmpv6Header icmpv6Hdr;
+    @NonNull
+    public final NaHeader naHdr;
+    @Nullable
+    public final LlaOption tlla;
+
+    public NeighborAdvertisement(@NonNull final EthernetHeader ethHdr,
+            @NonNull final Ipv6Header ipv6Hdr, @NonNull final Icmpv6Header icmpv6Hdr,
+            @NonNull final NaHeader naHdr, @Nullable final LlaOption tlla) {
+        this.ethHdr = ethHdr;
+        this.ipv6Hdr = ipv6Hdr;
+        this.icmpv6Hdr = icmpv6Hdr;
+        this.naHdr = naHdr;
+        this.tlla = tlla;
+    }
+
+    /**
+     * Convert a Neighbor Advertisement instance to ByteBuffer.
+     */
+    public ByteBuffer toByteBuffer() {
+        final int etherHeaderLen = Struct.getSize(EthernetHeader.class);
+        final int ipv6HeaderLen = Struct.getSize(Ipv6Header.class);
+        final int icmpv6HeaderLen = Struct.getSize(Icmpv6Header.class);
+        final int naHeaderLen = Struct.getSize(NaHeader.class);
+        final int tllaOptionLen = (tlla == null) ? 0 : Struct.getSize(LlaOption.class);
+        final ByteBuffer packet = ByteBuffer.allocate(etherHeaderLen + ipv6HeaderLen
+                + icmpv6HeaderLen + naHeaderLen + tllaOptionLen);
+
+        ethHdr.writeToByteBuffer(packet);
+        ipv6Hdr.writeToByteBuffer(packet);
+        icmpv6Hdr.writeToByteBuffer(packet);
+        naHdr.writeToByteBuffer(packet);
+        if (tlla != null) {
+            tlla.writeToByteBuffer(packet);
+        }
+        packet.flip();
+
+        return packet;
+    }
+
+    /**
+     * Build a Neighbor Advertisement packet from the required specified parameters.
+     */
+    public static ByteBuffer build(@NonNull final MacAddress srcMac,
+            @NonNull final MacAddress dstMac, @NonNull final Inet6Address srcIp,
+            @NonNull final Inet6Address dstIp, int flags, @NonNull final Inet6Address target) {
+        final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, srcMac);
+        return Ipv6Utils.buildNaPacket(srcMac, dstMac, srcIp, dstIp, flags, target, tlla);
+    }
+
+    /**
+     * Parse a Neighbor Advertisement packet from ByteBuffer.
+     */
+    public static NeighborAdvertisement parse(@NonNull final byte[] recvbuf, final int length)
+            throws ParseException {
+        if (length < ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NA_HEADER_LEN
+                || recvbuf.length < length) {
+            throw new ParseException("Invalid packet length: " + length);
+        }
+        final ByteBuffer packet = ByteBuffer.wrap(recvbuf, 0, length);
+
+        // Parse each header and option in Neighbor Advertisement packet in order.
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, packet);
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, packet);
+        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, packet);
+        final NaHeader naHdr = Struct.parse(NaHeader.class, packet);
+        final LlaOption tlla = (packet.remaining() == 0)
+                ? null
+                : Struct.parse(LlaOption.class, packet);
+
+        return new NeighborAdvertisement(ethHdr, ipv6Hdr, icmpv6Hdr, naHdr, tlla);
+    }
+
+    /**
+     * Thrown when parsing Neighbor Advertisement packet failed.
+     */
+    public static class ParseException extends Exception {
+        ParseException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/src/com/android/networkstack/packets/NeighborSolicitation.java b/src/com/android/networkstack/packets/NeighborSolicitation.java
new file mode 100644
index 0000000..5c3e40a
--- /dev/null
+++ b/src/com/android/networkstack/packets/NeighborSolicitation.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.packets;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NS_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+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.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * Defines basic data and operations needed to build and parse Neighbor Solicitation packet.
+ *
+ * @hide
+ */
+public class NeighborSolicitation {
+    @NonNull
+    public final EthernetHeader ethHdr;
+    @NonNull
+    public final Ipv6Header ipv6Hdr;
+    @NonNull
+    public final Icmpv6Header icmpv6Hdr;
+    @NonNull
+    public final NsHeader nsHdr;
+    @Nullable
+    public final LlaOption slla;
+
+    public NeighborSolicitation(@NonNull final EthernetHeader ethHdr,
+            @NonNull final Ipv6Header ipv6Hdr, @NonNull final Icmpv6Header icmpv6Hdr,
+            @NonNull final NsHeader nsHdr, @Nullable final LlaOption slla) {
+        this.ethHdr = ethHdr;
+        this.ipv6Hdr = ipv6Hdr;
+        this.icmpv6Hdr = icmpv6Hdr;
+        this.nsHdr = nsHdr;
+        this.slla = slla;
+    }
+
+    /**
+     * Convert a Neighbor Solicitation instance to ByteBuffer.
+     */
+    public ByteBuffer toByteBuffer() {
+        final int etherHeaderLen = Struct.getSize(EthernetHeader.class);
+        final int ipv6HeaderLen = Struct.getSize(Ipv6Header.class);
+        final int icmpv6HeaderLen = Struct.getSize(Icmpv6Header.class);
+        final int nsHeaderLen = Struct.getSize(NsHeader.class);
+        final int sllaOptionLen = (slla == null) ? 0 : Struct.getSize(LlaOption.class);
+        final ByteBuffer packet = ByteBuffer.allocate(etherHeaderLen + ipv6HeaderLen
+                + icmpv6HeaderLen + nsHeaderLen + sllaOptionLen);
+
+        ethHdr.writeToByteBuffer(packet);
+        ipv6Hdr.writeToByteBuffer(packet);
+        icmpv6Hdr.writeToByteBuffer(packet);
+        nsHdr.writeToByteBuffer(packet);
+        if (slla != null) {
+            slla.writeToByteBuffer(packet);
+        }
+        packet.flip();
+
+        return packet;
+    }
+
+    /**
+     * Build a Neighbor Solicitation packet from the required specified parameters.
+     */
+    public static ByteBuffer build(@NonNull final MacAddress srcMac,
+            @NonNull final MacAddress dstMac, @NonNull final Inet6Address srcIp,
+            @NonNull final Inet6Address dstIp, @NonNull final Inet6Address target) {
+        final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+        return Ipv6Utils.buildNsPacket(srcMac, dstMac, srcIp, dstIp, target, slla);
+    }
+
+    /**
+     * Parse a Neighbor Solicitation packet from ByteBuffer.
+     */
+    public static NeighborSolicitation parse(@NonNull final byte[] recvbuf, final int length)
+            throws ParseException {
+        if (length < ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NS_HEADER_LEN
+                || recvbuf.length < length) {
+            throw new ParseException("Invalid packet length: " + length);
+        }
+        final ByteBuffer packet = ByteBuffer.wrap(recvbuf, 0, length);
+
+        // Parse each header and option in Neighbor Solicitation packet in order.
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, packet);
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, packet);
+        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, packet);
+        final NsHeader nsHdr = Struct.parse(NsHeader.class, packet);
+        final LlaOption slla = (packet.remaining() == 0)
+                ? null
+                : Struct.parse(LlaOption.class, packet);
+
+        return new NeighborSolicitation(ethHdr, ipv6Hdr, icmpv6Hdr, nsHdr, slla);
+    }
+
+    /**
+     * Thrown when parsing Neighbor Solicitation packet failed.
+     */
+    public static class ParseException extends Exception {
+        ParseException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/src/com/android/networkstack/util/DnsUtils.java b/src/com/android/networkstack/util/DnsUtils.java
index 83f2daf..622f56a 100644
--- a/src/com/android/networkstack/util/DnsUtils.java
+++ b/src/com/android/networkstack/util/DnsUtils.java
@@ -29,7 +29,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.internal.util.TrafficStatsConstants;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.server.connectivity.NetworkMonitor.DnsLogFunc;
 
 import java.net.InetAddress;
@@ -126,7 +126,7 @@
         // look at the tag at all. Given that this is a library, the tag should be passed in by the
         // caller.
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_PROBE);
+                NetworkStackConstants.TAG_SYSTEM_PROBE);
 
         if (type == TYPE_ADDRCONFIG) {
             dnsResolver.query(network, host, flag, r -> r.run(), null /* cancellationSignal */,
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index 8710e67..903a5a8 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -20,6 +20,7 @@
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
 
+import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
 import static com.android.server.util.PermissionUtil.checkDumpPermission;
 
 import android.app.Service;
@@ -31,6 +32,7 @@
 import android.net.INetworkMonitor;
 import android.net.INetworkMonitorCallbacks;
 import android.net.INetworkStackConnector;
+import android.net.INetworkStackStatusCallback;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -58,6 +60,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.networkstack.NetworkStackNotifier;
+import com.android.networkstack.R;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.server.connectivity.NetworkMonitor;
 import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService;
@@ -409,6 +412,15 @@
         }
 
         @Override
+        public void allowTestUid(int uid, @Nullable INetworkStackStatusCallback cb)
+                throws RemoteException {
+            // setTestUid does its own permission checks
+            PermissionUtil.setTestUid(mContext, uid);
+            mLog.i("Allowing test uid " + uid);
+            if (cb != null) cb.onStatusAvailable(0);
+        }
+
+        @Override
         protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
                 @Nullable String[] args) {
             checkDumpPermission();
@@ -462,6 +474,11 @@
                     pw.decreaseIndent();
                 }
             }
+
+            pw.println();
+            pw.print("useNeighborResource: ");
+            pw.println(getResBooleanConfig(mContext,
+                    R.bool.config_no_sim_card_uses_neighbor_mcc, false));
         }
 
         /**
diff --git a/src/com/android/server/TestNetworkStackService.java b/src/com/android/server/TestNetworkStackService.java
new file mode 100644
index 0000000..23981e5
--- /dev/null
+++ b/src/com/android/server/TestNetworkStackService.java
@@ -0,0 +1,39 @@
+/*
+ * 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.server;
+
+import static com.android.server.util.PermissionUtil.isDebuggableBuild;
+
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link NetworkStackService} that can only be bound to on debuggable builds.
+ */
+public class TestNetworkStackService extends NetworkStackService {
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (!isDebuggableBuild()) {
+            throw new SecurityException(
+                    "TestNetworkStackService is only available on debuggable builds");
+        }
+        return super.onBind(intent);
+    }
+}
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index fe393f9..ce0374d 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -31,8 +31,10 @@
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.captiveportal.CaptivePortalProbeSpec.parseCaptivePortalProbeSpecs;
 import static android.net.metrics.ValidationProbeEvent.DNS_FAILURE;
 import static android.net.metrics.ValidationProbeEvent.DNS_SUCCESS;
@@ -73,10 +75,11 @@
 import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
 import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static android.net.util.NetworkStackUtils.TEST_URL_EXPIRATION_TIME;
-import static android.net.util.NetworkStackUtils.isEmpty;
-import static android.net.util.NetworkStackUtils.isIPv6ULA;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
+import static com.android.net.module.util.CollectionUtils.isEmpty;
+import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
 import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_DNS_EVENTS;
 import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_TCP_METRICS;
 import static com.android.networkstack.apishim.ConstantsShim.TRANSPORT_TEST;
@@ -122,6 +125,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.stats.connectivity.ProbeResult;
@@ -142,7 +146,6 @@
 import android.util.SparseArray;
 
 import androidx.annotation.ArrayRes;
-import androidx.annotation.BoolRes;
 import androidx.annotation.IntegerRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -153,12 +156,15 @@
 import com.android.internal.util.RingBufferIndices;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.internal.util.TrafficStatsConstants;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.networkstack.NetworkStackNotifier;
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.apishim.api29.ConstantsShim;
 import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.metrics.DataStallDetectionStats;
@@ -215,6 +221,9 @@
     private static final String TAG = NetworkMonitor.class.getSimpleName();
     private static final boolean DBG  = true;
     private static final boolean VDBG = false;
+    // TODO(b/185082309): For flaky test debug only, remove it after fixing.
+    private static final boolean DDBG_STALL = "cf_x86_auto-userdebug".equals(
+            SystemProperties.get("ro.build.flavor", ""));
     private static final boolean VDBG_STALL = Log.isLoggable(TAG, Log.DEBUG);
     private static final String DEFAULT_USER_AGENT    = "Mozilla/5.0 (X11; Linux x86_64) "
                                                       + "AppleWebKit/537.36 (KHTML, like Gecko) "
@@ -407,6 +416,7 @@
     private String mPrivateDnsProviderHostname = "";
 
     private final Context mContext;
+    private final Context mCustomizedContext;
     private final INetworkMonitorCallbacks mCallback;
     private final int mCallbackVersion;
     private final Network mCleartextDnsNetwork;
@@ -496,7 +506,8 @@
     @Nullable
     private final DnsStallDetector mDnsStallDetector;
     private long mLastProbeTime;
-    // The signal causing a data stall to be suspected. Reset to 0 after metrics are sent to statsd.
+    // A bitmask of signals causing a data stall to be suspected. Reset to
+    // {@link DataStallUtils#DATA_STALL_EVALUATION_TYPE_NONE} after metrics are sent to statsd.
     private @EvaluationType int mDataStallTypeToCollect;
     private boolean mAcceptPartialConnectivity = false;
     private final EvaluationState mEvaluationState = new EvaluationState();
@@ -504,6 +515,8 @@
     private final boolean mPrivateIpNoInternetEnabled;
 
     private final boolean mMetricsEnabled;
+    @NonNull
+    private final NetworkInformationShim mInfoShim = NetworkInformationShimImpl.newInstance();
 
     // The validation metrics are accessed by individual probe threads, and by the StateMachine
     // thread. All accesses must be synchronized to make sure the StateMachine thread can see
@@ -562,6 +575,7 @@
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         mNotifier = serviceManager.getNotifier();
+        mCustomizedContext = getCustomizedContextOrDefault();
 
         // CHECKSTYLE:OFF IndentationCheck
         addState(mDefaultState);
@@ -659,6 +673,7 @@
                 (Pair<LinkProperties, NetworkCapabilities>) connectedMsg.obj;
         mLinkProperties = attrs.first;
         mNetworkCapabilities = attrs.second;
+        suppressNotificationIfNetworkRestricted();
     }
 
     /**
@@ -723,6 +738,12 @@
         return NetworkMonitorUtils.isPrivateDnsValidationRequired(mNetworkCapabilities);
     }
 
+    private void suppressNotificationIfNetworkRestricted() {
+        if (!mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+            mDontDisplaySigninNotification = true;
+        }
+    }
+
     private void notifyNetworkTested(NetworkTestResultParcelable result) {
         try {
             if (mCallbackVersion <= 5) {
@@ -972,6 +993,7 @@
                     break;
                 case EVENT_NETWORK_CAPABILITIES_CHANGED:
                     mNetworkCapabilities = (NetworkCapabilities) message.obj;
+                    suppressNotificationIfNetworkRestricted();
                     break;
                 default:
                     break;
@@ -1215,6 +1237,22 @@
             if (!mEvaluationTimer.isStarted()) {
                 mEvaluationTimer.start();
             }
+
+            // Check if the network is captive with Terms & Conditions page. The first network
+            // evaluation for captive networks with T&Cs returns early but NetworkMonitor will then
+            // keep checking for connectivity to determine when the T&Cs are cleared.
+            if (isTermsAndConditionsCaptive(mInfoShim.getCaptivePortalData(mLinkProperties))
+                    && mValidations == 0) {
+                mLastPortalProbeResult = new CaptivePortalProbeResult(
+                        CaptivePortalProbeResult.PORTAL_CODE,
+                        mLinkProperties.getCaptivePortalData().getUserPortalUrl()
+                                .toString(), null,
+                        CaptivePortalProbeResult.PROBE_UNKNOWN);
+                mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
+                        mLastPortalProbeResult.redirectUrl);
+                transitionTo(mCaptivePortalState);
+                return;
+            }
             sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
             if (mUidResponsibleForReeval != INVALID_UID) {
                 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
@@ -1525,7 +1563,7 @@
             }
 
             final int token = ++mProbeToken;
-            final EvaluationThreadDeps deps = new EvaluationThreadDeps(mNetworkCapabilities);
+            final ValidationProperties deps = new ValidationProperties(mNetworkCapabilities);
             mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
                     isCaptivePortal(deps))));
             mThread.start();
@@ -1550,6 +1588,16 @@
                         // Transit EvaluatingPrivateDnsState to get to Validated
                         // state (even if no Private DNS validation required).
                         transitionTo(mEvaluatingPrivateDnsState);
+                    } else if (isTermsAndConditionsCaptive(
+                            mInfoShim.getCaptivePortalData(mLinkProperties))) {
+                        mLastPortalProbeResult = new CaptivePortalProbeResult(
+                                CaptivePortalProbeResult.PORTAL_CODE,
+                                mLinkProperties.getCaptivePortalData().getUserPortalUrl()
+                                        .toString(), null,
+                                CaptivePortalProbeResult.PROBE_UNKNOWN);
+                        mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
+                                mLastPortalProbeResult.redirectUrl);
+                        transitionTo(mCaptivePortalState);
                     } else if (probeResult.isPortal()) {
                         mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
                                 probeResult.redirectUrl);
@@ -1823,7 +1871,10 @@
         }
         try {
             final List<CellInfo> cells = mTelephonyManager.getAllCellInfo();
-            if (cells == null) return null;
+            if (cells == null) {
+                log("CellInfo is null");
+                return null;
+            }
             final Map<String, Integer> countryCodeMap = new HashMap<>();
             int maxCount = 0;
             for (final CellInfo cell : cells) {
@@ -1866,6 +1917,7 @@
         // Return customized context if carrier id can match a record in sCarrierIdToMccMnc.
         final MccMncOverrideInfo overrideInfo = getMccMncOverrideInfo();
         if (overrideInfo != null) {
+            log("Return customized context by MccMncOverrideInfo.");
             return getContextByMccMnc(overrideInfo.mcc, overrideInfo.mnc);
         }
 
@@ -1875,11 +1927,13 @@
                 getResBooleanConfig(mContext, R.bool.config_no_sim_card_uses_neighbor_mcc, false);
         if (!useNeighborResource
                 || TelephonyManager.SIM_STATE_READY == mTelephonyManager.getSimState()) {
+            if (useNeighborResource) log("Sim state is ready, return original context.");
             return mContext;
         }
 
         final String mcc = getLocationMcc();
         if (TextUtils.isEmpty(mcc)) {
+            log("Return original context due to getting mcc failed.");
             return mContext;
         }
 
@@ -1887,7 +1941,7 @@
     }
 
     @Nullable
-    private String getTestUrl(@NonNull String key) {
+    private URL getTestUrl(@NonNull String key) {
         final String strExpiration = mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
                 TEST_URL_EXPIRATION_TIME, null);
         if (strExpiration == null) return null;
@@ -1903,17 +1957,17 @@
         final long now = System.currentTimeMillis();
         if (expTime < now || (expTime - now) > TEST_URL_EXPIRATION_MS) return null;
 
-        return mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
+        final String strUrl = mDependencies.getDeviceConfigProperty(NAMESPACE_CONNECTIVITY,
                 key, null /* defaultValue */);
+        if (!isValidTestUrl(strUrl)) return null;
+        return makeURL(strUrl);
     }
 
     private String getCaptivePortalServerHttpsUrl() {
-        final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
-        if (isValidTestUrl(testUrl)) return testUrl;
-        final Context targetContext = getCustomizedContextOrDefault();
-        return getSettingFromResource(targetContext,
+        return getSettingFromResource(mCustomizedContext,
                 R.string.config_captive_portal_https_url, CAPTIVE_PORTAL_HTTPS_URL,
-                targetContext.getResources().getString(R.string.default_captive_portal_https_url));
+                mCustomizedContext.getResources().getString(
+                R.string.default_captive_portal_https_url));
     }
 
     private static boolean isValidTestUrl(@Nullable String url) {
@@ -1954,17 +2008,6 @@
         }
     }
 
-    @VisibleForTesting
-    boolean getResBooleanConfig(@NonNull final Context context,
-            @BoolRes int configResource, final boolean defaultValue) {
-        final Resources res = context.getResources();
-        try {
-            return res.getBoolean(configResource);
-        } catch (Resources.NotFoundException e) {
-            return defaultValue;
-        }
-    }
-
     /**
      * Gets integer config from resources.
      */
@@ -2001,12 +2044,10 @@
      * on one URL that can be used, while NetworkMonitor may implement more complex logic.
      */
     public String getCaptivePortalServerHttpUrl() {
-        final String testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
-        if (isValidTestUrl(testUrl)) return testUrl;
-        final Context targetContext = getCustomizedContextOrDefault();
-        return getSettingFromResource(targetContext,
+        return getSettingFromResource(mCustomizedContext,
                 R.string.config_captive_portal_http_url, CAPTIVE_PORTAL_HTTP_URL,
-                targetContext.getResources().getString(R.string.default_captive_portal_http_url));
+                mCustomizedContext.getResources().getString(
+                R.string.default_captive_portal_http_url));
     }
 
     private int getConsecutiveDnsTimeoutThreshold() {
@@ -2080,6 +2121,9 @@
     }
 
     private URL[] makeCaptivePortalHttpsUrls() {
+        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
+        if (testUrl != null) return new URL[] { testUrl };
+
         final String firstUrl = getCaptivePortalServerHttpsUrl();
         try {
             final URL[] settingProviderUrls =
@@ -2098,6 +2142,9 @@
     }
 
     private URL[] makeCaptivePortalHttpUrls() {
+        final URL testUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
+        if (testUrl != null) return new URL[] { testUrl };
+
         final String firstUrl = getCaptivePortalServerHttpUrl();
         try {
             final URL[] settingProviderUrls =
@@ -2164,7 +2211,7 @@
      */
     private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
             @ArrayRes int defaultResId, @NonNull Function<String, T> resourceConverter) {
-        final Resources res = getCustomizedContextOrDefault().getResources();
+        final Resources res = mCustomizedContext.getResources();
         return getProbeUrlArrayConfig(providerValue, configResId, res.getStringArray(defaultResId),
                 resourceConverter);
     }
@@ -2182,7 +2229,7 @@
      */
     private <T> T[] getProbeUrlArrayConfig(@NonNull T[] providerValue, @ArrayRes int configResId,
             String[] defaultConfig, @NonNull Function<String, T> resourceConverter) {
-        final Resources res = getCustomizedContextOrDefault().getResources();
+        final Resources res = mCustomizedContext.getResources();
         String[] configValue = res.getStringArray(configResId);
 
         if (configValue.length == 0) {
@@ -2244,24 +2291,24 @@
     }
 
     /**
-     * Parameters that can be accessed by the evaluation thread in a thread-safe way.
+     * Validation properties that can be accessed by the evaluation thread in a thread-safe way.
      *
      * Parameters such as LinkProperties and NetworkCapabilities cannot be accessed by the
      * evaluation thread directly, as they are managed in the state machine thread and not
      * synchronized. This class provides a copy of the required data that is not modified and can be
      * used safely by the evaluation thread.
      */
-    private static class EvaluationThreadDeps {
-        // TODO: add parameters that are accessed in a non-thread-safe way from the evaluation
-        // thread (read from LinkProperties, NetworkCapabilities, useHttps, validationStage)
+    private static class ValidationProperties {
+        // TODO: add other properties that are needed for evaluation and currently extracted in a
+        // non-thread-safe way from LinkProperties, NetworkCapabilities, etc.
         private final boolean mIsTestNetwork;
 
-        EvaluationThreadDeps(NetworkCapabilities nc) {
+        ValidationProperties(NetworkCapabilities nc) {
             this.mIsTestNetwork = nc.hasTransport(TRANSPORT_TEST);
         }
     }
 
-    private CaptivePortalProbeResult isCaptivePortal(EvaluationThreadDeps deps) {
+    private CaptivePortalProbeResult isCaptivePortal(ValidationProperties properties) {
         if (!mIsCaptivePortalCheckEnabled) {
             validationLog("Validation disabled.");
             return CaptivePortalProbeResult.success(CaptivePortalProbeResult.PROBE_UNKNOWN);
@@ -2309,11 +2356,12 @@
             reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
         } else if (mUseHttps && httpsUrls.length == 1 && httpUrls.length == 1) {
             // Probe results are reported inside sendHttpAndHttpsParallelWithFallbackProbes.
-            result = sendHttpAndHttpsParallelWithFallbackProbes(deps, proxyInfo,
+            result = sendHttpAndHttpsParallelWithFallbackProbes(properties, proxyInfo,
                     httpsUrls[0], httpUrls[0]);
         } else if (mUseHttps) {
             // Support result aggregation from multiple Urls.
-            result = sendMultiParallelHttpAndHttpsProbes(deps, proxyInfo, httpsUrls, httpUrls);
+            result = sendMultiParallelHttpAndHttpsProbes(properties, proxyInfo, httpsUrls,
+                    httpUrls);
         } else {
             result = sendDnsAndHttpProbes(proxyInfo, httpUrls[0], ValidationProbeEvent.PROBE_HTTP);
             reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, result);
@@ -2417,7 +2465,7 @@
         String redirectUrl = null;
         final Stopwatch probeTimer = new Stopwatch().start();
         final int oldTag = TrafficStats.getAndSetThreadStatsTag(
-                TrafficStatsConstants.TAG_SYSTEM_PROBE);
+                NetworkStackConstants.TAG_SYSTEM_PROBE);
         try {
             // Follow redirects for PAC probes as such probes verify connectivity by fetching the
             // PAC proxy file, which may be configured behind a redirect.
@@ -2503,8 +2551,17 @@
 
         final CaptivePortalProbeResult probeResult;
         if (probeSpec == null) {
+            if (CaptivePortalProbeResult.isPortalCode(httpResponseCode)
+                    && TextUtils.isEmpty(redirectUrl)
+                    && ShimUtils.isAtLeastS()) {
+                // If a portal is a non-redirect portal (often portals that return HTTP 200 with a
+                // login page for all HTTP requests), report the probe URL as the login URL starting
+                // from S (b/172048052). This avoids breaking assumptions that
+                // [is a portal] is equivalent to [there is a login URL].
+                redirectUrl = url.toString();
+            }
             probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl,
-                    url.toString(),   1 << probeType);
+                    url.toString(), 1 << probeType);
         } else {
             probeResult = probeSpec.getResult(httpResponseCode, redirectUrl);
         }
@@ -2601,12 +2658,12 @@
         private final CountDownLatch mLatch;
         private final Probe mProbe;
 
-        ProbeThread(CountDownLatch latch, EvaluationThreadDeps deps, ProxyInfo proxy, URL url,
+        ProbeThread(CountDownLatch latch, ValidationProperties properties, ProxyInfo proxy, URL url,
                 int probeType, Uri captivePortalApiUrl) {
             mLatch = latch;
             mProbe = (probeType == ValidationProbeEvent.PROBE_HTTPS)
-                    ? new HttpsProbe(deps, proxy, url, captivePortalApiUrl)
-                    : new HttpProbe(deps, proxy, url, captivePortalApiUrl);
+                    ? new HttpsProbe(properties, proxy, url, captivePortalApiUrl)
+                    : new HttpProbe(properties, proxy, url, captivePortalApiUrl);
             mResult = CaptivePortalProbeResult.failed(probeType);
         }
 
@@ -2631,14 +2688,14 @@
     }
 
     private abstract static class Probe {
-        protected final EvaluationThreadDeps mDeps;
+        protected final ValidationProperties mProperties;
         protected final ProxyInfo mProxy;
         protected final URL mUrl;
         protected final Uri mCaptivePortalApiUrl;
 
-        protected Probe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url,
+        protected Probe(ValidationProperties properties, ProxyInfo proxy, URL url,
                 Uri captivePortalApiUrl) {
-            mDeps = deps;
+            mProperties = properties;
             mProxy = proxy;
             mUrl = url;
             mCaptivePortalApiUrl = captivePortalApiUrl;
@@ -2648,8 +2705,9 @@
     }
 
     final class HttpsProbe extends Probe {
-        HttpsProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
-            super(deps, proxy, url, captivePortalApiUrl);
+        HttpsProbe(ValidationProperties properties, ProxyInfo proxy, URL url,
+                Uri captivePortalApiUrl) {
+            super(properties, proxy, url, captivePortalApiUrl);
         }
 
         @Override
@@ -2659,8 +2717,9 @@
     }
 
     final class HttpProbe extends Probe {
-        HttpProbe(EvaluationThreadDeps deps, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
-            super(deps, proxy, url, captivePortalApiUrl);
+        HttpProbe(ValidationProperties properties, ProxyInfo proxy, URL url,
+                Uri captivePortalApiUrl) {
+            super(properties, proxy, url, captivePortalApiUrl);
         }
 
         private CaptivePortalDataShim sendCapportApiProbe() {
@@ -2674,7 +2733,7 @@
                 // Protocol must be HTTPS
                 // (as per https://www.ietf.org/id/draft-ietf-capport-api-07.txt, #4).
                 // Only allow HTTP on localhost, for testing.
-                final boolean isTestLocalhostHttp = mDeps.mIsTestNetwork
+                final boolean isTestLocalhostHttp = mProperties.mIsTestNetwork
                         && "localhost".equals(url.getHost()) && "http".equals(url.getProtocol());
                 if (!"https".equals(url.getProtocol()) && !isTestLocalhostHttp) {
                     validationLog("Invalid captive portal API protocol: " + url.getProtocol());
@@ -2772,8 +2831,8 @@
     }
 
     private CaptivePortalProbeResult sendMultiParallelHttpAndHttpsProbes(
-            @NonNull EvaluationThreadDeps deps, @Nullable ProxyInfo proxy, @NonNull URL[] httpsUrls,
-            @NonNull URL[] httpUrls) {
+            @NonNull ValidationProperties properties, @Nullable ProxyInfo proxy,
+            @NonNull URL[] httpsUrls, @NonNull URL[] httpUrls) {
         // If multiple URLs are required to ensure the correctness of validation, send parallel
         // probes to explore the result in separate probe threads and aggregate those results into
         // one as the final result for either HTTP or HTTPS.
@@ -2798,12 +2857,12 @@
             // TODO: Have the capport probe as a different probe for cleanliness.
             final URL urlMaybeWithCapport = httpUrls[0];
             for (final URL url : httpUrls) {
-                futures.add(ecs.submit(() -> new HttpProbe(deps, proxy, url,
+                futures.add(ecs.submit(() -> new HttpProbe(properties, proxy, url,
                         url.equals(urlMaybeWithCapport) ? capportApiUrl : null).sendProbe()));
             }
 
             for (final URL url : httpsUrls) {
-                futures.add(ecs.submit(() -> new HttpsProbe(deps, proxy, url, capportApiUrl)
+                futures.add(ecs.submit(() -> new HttpsProbe(properties, proxy, url, capportApiUrl)
                         .sendProbe()));
             }
 
@@ -2900,15 +2959,15 @@
     }
 
     private CaptivePortalProbeResult sendHttpAndHttpsParallelWithFallbackProbes(
-            EvaluationThreadDeps deps, ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
+            ValidationProperties properties, ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
         // Number of probes to wait for. If a probe completes with a conclusive answer
         // it shortcuts the latch immediately by forcing the count to 0.
         final CountDownLatch latch = new CountDownLatch(2);
 
         final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
-        final ProbeThread httpsProbe = new ProbeThread(latch, deps, proxy, httpsUrl,
+        final ProbeThread httpsProbe = new ProbeThread(latch, properties, proxy, httpsUrl,
                 ValidationProbeEvent.PROBE_HTTPS, capportApiUrl);
-        final ProbeThread httpProbe = new ProbeThread(latch, deps, proxy, httpUrl,
+        final ProbeThread httpProbe = new ProbeThread(latch, properties, proxy, httpUrl,
                 ValidationProbeEvent.PROBE_HTTP, capportApiUrl);
 
         try {
@@ -2967,10 +3026,7 @@
             httpsProbe.join();
             reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsProbe.result());
 
-            final boolean isHttpSuccessful =
-                    (httpProbe.result().isSuccessful()
-                    || (fallbackProbeResult != null && fallbackProbeResult.isSuccessful()));
-            if (httpsProbe.result().isFailed() && isHttpSuccessful) {
+            if (httpsProbe.result().isFailed() && httpProbe.result().isSuccessful()) {
                 return CaptivePortalProbeResult.PARTIAL;
             }
             return httpsProbe.result();
@@ -3076,7 +3132,7 @@
         @Nullable
         public String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
                 @Nullable String defaultValue) {
-            return NetworkStackUtils.getDeviceConfigProperty(namespace, name, defaultValue);
+            return DeviceConfigUtils.getDeviceConfigProperty(namespace, name, defaultValue);
         }
 
         /**
@@ -3089,17 +3145,17 @@
          */
         public int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
                 int defaultValue) {
-            return NetworkStackUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(namespace, name, defaultValue);
         }
 
         /**
          * Check whether or not one experimental feature in the connectivity namespace is
          * enabled.
          * @param name Flag name of the experiment in the connectivity namespace.
-         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
+         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String)
          */
         public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
-            return NetworkStackUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
         }
 
         /**
@@ -3116,7 +3172,7 @@
          */
         public boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
                 @NonNull String name, boolean defaultEnabled) {
-            return NetworkStackUtils.isFeatureEnabled(context, namespace, name, defaultEnabled);
+            return DeviceConfigUtils.isFeatureEnabled(context, namespace, name, defaultEnabled);
         }
 
         /**
@@ -3185,6 +3241,10 @@
             // considered in the evaluation happened in defined threshold time.
             final long now = SystemClock.elapsedRealtime();
             final long firstTimeoutTime = now - mDnsEvents[firstConsecutiveTimeoutIndex].mTimeStamp;
+            if (DDBG_STALL) {
+                Log.d(TAG, "DSD.isDataStallSuspected, first="
+                        + firstTimeoutTime + ", valid=" + validTime);
+            }
             return (firstTimeoutTime < validTime);
         }
 
@@ -3237,13 +3297,19 @@
             return false;
         }
 
-        Boolean result = null;
-        final StringJoiner msg = (DBG || VDBG_STALL) ? new StringJoiner(", ") : null;
+        int typeToCollect = 0;
+        final int notStall = -1;
+        final StringJoiner msg = (DBG || VDBG_STALL || DDBG_STALL) ? new StringJoiner(", ") : null;
         // Reevaluation will generate traffic. Thus, set a minimal reevaluation timer to limit the
         // possible traffic cost in metered network.
+        final long currentTime = SystemClock.elapsedRealtime();
         if (!mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
-                && (SystemClock.elapsedRealtime() - getLastProbeTime()
-                < mDataStallMinEvaluateTime)) {
+                && (currentTime - getLastProbeTime() < mDataStallMinEvaluateTime)) {
+            if (DDBG_STALL) {
+                Log.d(TAG, "isDataStall: false, currentTime=" + currentTime
+                        + ", lastProbeTime=" + getLastProbeTime()
+                        + ", MinEvaluateTime=" + mDataStallMinEvaluateTime);
+            }
             return false;
         }
         // Check TCP signal. Suspect it may be a data stall if :
@@ -3252,20 +3318,11 @@
         final TcpSocketTracker tst = getTcpSocketTracker();
         if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_TCP) && tst != null) {
             if (tst.getLatestReceivedCount() > 0) {
-                result = false;
+                typeToCollect = notStall;
             } else if (tst.isDataStallSuspected()) {
-                result = true;
-                mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_TCP;
-
-                final DataStallReportParcelable p = new DataStallReportParcelable();
-                p.detectionMethod = DETECTION_METHOD_TCP_METRICS;
-                p.timestampMillis = SystemClock.elapsedRealtime();
-                p.tcpPacketFailRate = tst.getLatestPacketFailPercentage();
-                p.tcpMetricsCollectionPeriodMillis = getTcpPollingInterval();
-
-                notifyDataStallSuspected(p);
+                typeToCollect |= DATA_STALL_EVALUATION_TYPE_TCP;
             }
-            if (DBG || VDBG_STALL) {
+            if (DBG || VDBG_STALL || DDBG_STALL) {
                 msg.add("tcp packets received=" + tst.getLatestReceivedCount())
                     .add("latest tcp fail rate=" + tst.getLatestPacketFailPercentage());
             }
@@ -3275,32 +3332,48 @@
         // 1. The number of consecutive DNS query timeouts >= mConsecutiveDnsTimeoutThreshold.
         // 2. Those consecutive DNS queries happened in the last mValidDataStallDnsTimeThreshold ms.
         final DnsStallDetector dsd = getDnsStallDetector();
-        if ((result == null) && (dsd != null)
+        if ((typeToCollect != notStall) && (dsd != null)
                 && dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_DNS)) {
-            if (dsd.isDataStallSuspected(mConsecutiveDnsTimeoutThreshold,
-                    mDataStallValidDnsTimeThreshold)) {
-                result = true;
-                mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_DNS;
+            if (dsd.isDataStallSuspected(
+                    mConsecutiveDnsTimeoutThreshold, mDataStallValidDnsTimeThreshold)) {
+                typeToCollect |= DATA_STALL_EVALUATION_TYPE_DNS;
                 logNetworkEvent(NetworkEvent.NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND);
-
-                final DataStallReportParcelable p = new DataStallReportParcelable();
-                p.detectionMethod = DETECTION_METHOD_DNS_EVENTS;
-                p.timestampMillis = SystemClock.elapsedRealtime();
-                p.dnsConsecutiveTimeouts = mDnsStallDetector.getConsecutiveTimeoutCount();
-                notifyDataStallSuspected(p);
             }
-            if (DBG || VDBG_STALL) {
+            if (DBG || VDBG_STALL || DDBG_STALL) {
                 msg.add("consecutive dns timeout count=" + dsd.getConsecutiveTimeoutCount());
             }
         }
-        // log only data stall suspected.
-        if ((DBG && Boolean.TRUE.equals(result)) || VDBG_STALL) {
-            log("isDataStall: result=" + result + ", " + msg);
+
+        if (typeToCollect > 0) {
+            mDataStallTypeToCollect = typeToCollect;
+            final DataStallReportParcelable p = new DataStallReportParcelable();
+            int detectionMethod = 0;
+            p.timestampMillis = SystemClock.elapsedRealtime();
+            if (isDataStallTypeDetected(typeToCollect, DATA_STALL_EVALUATION_TYPE_DNS)) {
+                detectionMethod |= DETECTION_METHOD_DNS_EVENTS;
+                p.dnsConsecutiveTimeouts = mDnsStallDetector.getConsecutiveTimeoutCount();
+            }
+
+            if (isDataStallTypeDetected(typeToCollect, DATA_STALL_EVALUATION_TYPE_TCP)) {
+                detectionMethod |= DETECTION_METHOD_TCP_METRICS;
+                p.tcpPacketFailRate = tst.getLatestPacketFailPercentage();
+                p.tcpMetricsCollectionPeriodMillis = getTcpPollingInterval();
+            }
+            p.detectionMethod = detectionMethod;
+            notifyDataStallSuspected(p);
         }
 
-        return (result == null) ? false : result;
+        // log only data stall suspected.
+        if ((DBG && (typeToCollect > 0)) || VDBG_STALL || DDBG_STALL) {
+            log("isDataStall: result=" + typeToCollect + ", " + msg);
+        }
+
+        return typeToCollect > 0;
     }
 
+    private static boolean isDataStallTypeDetected(int typeToCollect, int evaluationType) {
+        return (typeToCollect & evaluationType) != 0;
+    }
     // Class to keep state of evaluation results and probe results.
     //
     // The main purpose was to ensure NetworkMonitor can notify ConnectivityService of probe results
@@ -3363,6 +3436,14 @@
         }
 
         protected void reportEvaluationResult(int result, @Nullable String redirectUrl) {
+            if (!isValidationRequired() && mProbeCompleted == 0 && ShimUtils.isAtLeastS()) {
+                // If validation is not required AND no probes were attempted, the validation was
+                // skipped. Report this to ConnectivityService for ConnectivityDiagnostics, but only
+                // if the platform is Android S+, as ConnectivityService must also know how to
+                // understand this bit.
+                result |= NETWORK_VALIDATION_RESULT_SKIPPED;
+            }
+
             mEvaluationResult = result;
             mRedirectUrl = redirectUrl;
             final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
@@ -3466,4 +3547,17 @@
     private static Uri getCaptivePortalApiUrl(LinkProperties lp) {
         return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp);
     }
+
+    /**
+     * Check if the network is captive with terms and conditions page
+     * @return true if network is captive with T&C page, false otherwise
+     */
+    private boolean isTermsAndConditionsCaptive(CaptivePortalDataShim captivePortalDataShim) {
+        return captivePortalDataShim != null
+                && captivePortalDataShim.getUserPortalUrl() != null
+                && !TextUtils.isEmpty(captivePortalDataShim.getUserPortalUrl().toString())
+                && captivePortalDataShim.isCaptive()
+                && captivePortalDataShim.getUserPortalUrlSource()
+                == ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT;
+    }
 }
diff --git a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
index 9b1be06..f9b6365 100644
--- a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
@@ -147,6 +147,8 @@
     /** The SQLite DB helper */
     public static class DbHelper extends SQLiteOpenHelper {
         // Update this whenever changing the schema.
+        // DO NOT CHANGE without solid testing for downgrades, and checking onDowngrade
+        // below: b/171340630
         private static final int SCHEMA_VERSION = 4;
         private static final String DATABASE_FILENAME = "IpMemoryStore.db";
         private static final String TRIGGER_NAME = "delete_cascade_to_private";
@@ -202,6 +204,10 @@
             // Downgrades always nuke all data and recreate an empty table.
             db.execSQL(NetworkAttributesContract.DROP_TABLE);
             db.execSQL(PrivateDataContract.DROP_TABLE);
+            // TODO: add test for downgrades. Triggers should already be dropped
+            // when the table is dropped, so this may be a bug.
+            // Note that fixing this code does not affect how older versions
+            // will handle downgrades.
             db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
             onCreate(db);
         }
diff --git a/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java b/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java
index 70688ad..c8559c8 100644
--- a/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java
+++ b/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java
@@ -29,7 +29,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 
-import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -65,7 +65,8 @@
         }
     }
 
-    private static final ArrayList<InterruptMaintenance> sInterruptList = new ArrayList<>();
+    private static final CopyOnWriteArrayList<InterruptMaintenance> sInterruptList =
+            new CopyOnWriteArrayList<>();
     private static IpMemoryStoreService sIpMemoryStoreService;
 
     @Override
diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java
deleted file mode 100644
index 6ecc84c..0000000
--- a/src/com/android/server/util/NetworkStackConstants.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2018 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.server.util;
-
-import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-
-import java.net.Inet4Address;
-
-/**
- * Network constants used by the network stack.
- */
-public final class NetworkStackConstants {
-
-    /**
-     * Ethernet constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc894
-     *     - https://tools.ietf.org/html/rfc2464
-     *     - https://tools.ietf.org/html/rfc7042
-     *     - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
-     *     - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
-     */
-    public static final int ETHER_DST_ADDR_OFFSET = 0;
-    public static final int ETHER_SRC_ADDR_OFFSET = 6;
-    public static final int ETHER_ADDR_LEN = 6;
-    public static final int ETHER_TYPE_OFFSET = 12;
-    public static final int ETHER_TYPE_LENGTH = 2;
-    public static final int ETHER_TYPE_ARP  = 0x0806;
-    public static final int ETHER_TYPE_IPV4 = 0x0800;
-    public static final int ETHER_TYPE_IPV6 = 0x86dd;
-    public static final int ETHER_HEADER_LEN = 14;
-    public static final int ETHER_MTU = 1500;
-
-    /**
-     * ARP constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc826
-     *     - http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
-     */
-    public static final int ARP_PAYLOAD_LEN = 28;  // For Ethernet+IPv4.
-    public static final int ARP_ETHER_IPV4_LEN = ARP_PAYLOAD_LEN + ETHER_HEADER_LEN;
-    public static final int ARP_REQUEST = 1;
-    public static final int ARP_REPLY   = 2;
-    public static final int ARP_HWTYPE_RESERVED_LO = 0;
-    public static final int ARP_HWTYPE_ETHER       = 1;
-    public static final int ARP_HWTYPE_RESERVED_HI = 0xffff;
-
-    /**
-     * IPv4 Address Conflict Detection constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc5227
-     */
-    public static final int IPV4_CONFLICT_PROBE_NUM = 3;
-    public static final int IPV4_CONFLICT_ANNOUNCE_NUM = 2;
-
-    /**
-     * IPv4 constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc791
-     */
-    public static final int IPV4_ADDR_BITS = 32;
-    public static final int IPV4_MIN_MTU = 68;
-    public static final int IPV4_MAX_MTU = 65_535;
-    public static final int IPV4_HEADER_MIN_LEN = 20;
-    public static final int IPV4_IHL_MASK = 0xf;
-    public static final int IPV4_FLAGS_OFFSET = 6;
-    public static final int IPV4_FRAGMENT_MASK = 0x1fff;
-    public static final int IPV4_PROTOCOL_OFFSET = 9;
-    public static final int IPV4_SRC_ADDR_OFFSET = 12;
-    public static final int IPV4_DST_ADDR_OFFSET = 16;
-    public static final int IPV4_ADDR_LEN = 4;
-    public static final Inet4Address IPV4_ADDR_ALL = intToInet4AddressHTH(0xffffffff);
-    public static final Inet4Address IPV4_ADDR_ANY = intToInet4AddressHTH(0x0);
-
-    /**
-     * IPv6 constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc2460
-     */
-    public static final int IPV6_ADDR_LEN = 16;
-    public static final int IPV6_HEADER_LEN = 40;
-    public static final int IPV6_LEN_OFFSET = 4;
-    public static final int IPV6_PROTOCOL_OFFSET = 6;
-    public static final int IPV6_SRC_ADDR_OFFSET = 8;
-    public static final int IPV6_DST_ADDR_OFFSET = 24;
-    public static final int IPV6_MIN_MTU = 1280;
-
-    /**
-     * ICMPv6 constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc4443
-     *     - https://tools.ietf.org/html/rfc4861
-     */
-    public static final int ICMPV6_HEADER_MIN_LEN = 4;
-    public static final int ICMPV6_CHECKSUM_OFFSET = 2;
-    public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
-    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
-    public static final int ICMPV6_ROUTER_SOLICITATION    = 133;
-    public static final int ICMPV6_ROUTER_ADVERTISEMENT   = 134;
-    public static final int ICMPV6_NEIGHBOR_SOLICITATION  = 135;
-    public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
-    public static final int ICMPV6_ND_OPTION_MIN_LENGTH = 8;
-    public static final int ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR = 8;
-    public static final int ICMPV6_ND_OPTION_SLLA  = 1;
-    public static final int ICMPV6_ND_OPTION_TLLA  = 2;
-    public static final int ICMPV6_ND_OPTION_PIO   = 3;
-    public static final int ICMPV6_ND_OPTION_MTU   = 5;
-    public static final int ICMPV6_ND_OPTION_RDNSS = 25;
-    public static final int ICMPV6_ND_OPTION_PREF64 = 38;
-
-
-    public static final int ICMPV6_RA_HEADER_LEN = 16;
-
-    /**
-     * UDP constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc768
-     */
-    public static final int UDP_HEADER_LEN = 8;
-
-
-    /**
-     * DHCP constants.
-     *
-     * See also:
-     *     - https://tools.ietf.org/html/rfc2131
-     */
-    public static final int INFINITE_LEASE = 0xffffffff;
-    public static final int DHCP4_CLIENT_PORT = 68;
-
-    /**
-     * IEEE802.11 standard constants.
-     *
-     * See also:
-     *     - https://ieeexplore.ieee.org/document/7786995
-     */
-    public static final int VENDOR_SPECIFIC_IE_ID = 0xdd;
-
-    private NetworkStackConstants() {
-        throw new UnsupportedOperationException("This class is not to be instantiated");
-    }
-}
diff --git a/src/com/android/server/util/PermissionUtil.java b/src/com/android/server/util/PermissionUtil.java
index 3dff715..fbd13d7 100644
--- a/src/com/android/server/util/PermissionUtil.java
+++ b/src/com/android/server/util/PermissionUtil.java
@@ -16,12 +16,18 @@
 
 package com.android.server.util;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Binder.getCallingPid;
 import static android.os.Binder.getCallingUid;
 
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Process;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 
+import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -30,6 +36,8 @@
 public final class PermissionUtil {
     private static final AtomicInteger sSystemPid = new AtomicInteger(-1);
 
+    private static volatile int sTestUid = Process.INVALID_UID;
+
     /**
      * Check that the caller is allowed to communicate with the network stack.
      * @throws SecurityException The caller is not allowed to communicate with the network stack.
@@ -41,8 +49,9 @@
             return;
         }
 
-        if (caller != Process.myUid() && // apps with NETWORK_STACK_UID
-                UserHandle.getAppId(caller) != Process.BLUETOOTH_UID) {
+        if (caller != Process.myUid() // apps with NETWORK_STACK_UID
+                && UserHandle.getAppId(caller) != Process.BLUETOOTH_UID
+                && !isTestUid(caller)) {
             throw new SecurityException("Invalid caller: " + caller);
         }
     }
@@ -67,6 +76,42 @@
         }
     }
 
+    private static boolean isTestUid(int uid) {
+        return uid == sTestUid;
+    }
+
+    /**
+     * Set a test uid that is allowed to call the NetworkStack. Pass in -1 to reset.
+     *
+     * <p>The UID must have a package with NETWORK_SETTINGS permissions when it is allowed.
+     */
+    public static void setTestUid(Context context, int uid) {
+        if (!isDebuggableBuild()) {
+            throw new SecurityException("Cannot set test UID on non-debuggable builds");
+        }
+        if (getCallingUid() != Process.ROOT_UID) {
+            throw new SecurityException("Only root can set the test UID");
+        }
+
+        if (uid == Process.INVALID_UID) {
+            sTestUid = uid;
+            return;
+        }
+
+        final PackageManager pm = context.getPackageManager();
+        final String[] packages = pm.getPackagesForUid(uid);
+        if (packages == null) {
+            throw new SecurityException("No package in uid " + uid);
+        }
+        final boolean hasPermission = Arrays.stream(packages).anyMatch(
+                p -> pm.checkPermission(NETWORK_SETTINGS, p) == PERMISSION_GRANTED);
+        if (!hasPermission) {
+            throw new SecurityException(
+                    "The uid must have a package with NETWORK_SETTINGS permissions");
+        }
+        sTestUid = uid;
+    }
+
     /**
      * Check that the caller is allowed to dump the network stack, e.g. dumpsys.
      * @throws SecurityException The caller is not allowed to dump the network stack.
@@ -79,6 +124,14 @@
         }
     }
 
+    /**
+     * @see android.os.Build.IS_DEBUGGABLE
+     */
+    public static boolean isDebuggableBuild() {
+        // TODO: consider adding Build.IS_DEBUGGABLE to @SystemApi
+        return SystemProperties.getInt("ro.debuggable", 0) == 1;
+    }
+
     private PermissionUtil() {
         throw new UnsupportedOperationException("This class is not to be instantiated");
     }
diff --git a/tests/hostdriven/Android.bp b/tests/hostdriven/Android.bp
index ec36424..6c3de45 100644
--- a/tests/hostdriven/Android.bp
+++ b/tests/hostdriven/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_test_host {
     name: "NetworkStackHostTests",
     srcs: ["host/src/**/*.kt"],
@@ -23,7 +27,7 @@
     ],
     static_libs: [
         "kotlin-test",
-        "module_test_util",
+        "cts-install-lib-host",
     ],
     test_suites: ["device-tests"],
     data: [":NetworkStack"],
diff --git a/tests/hostdriven/host/src/com/android/networkstack/hosttests/NetworkStackHostTests.kt b/tests/hostdriven/host/src/com/android/networkstack/hosttests/NetworkStackHostTests.kt
index bc1176f..a79de6a 100644
--- a/tests/hostdriven/host/src/com/android/networkstack/hosttests/NetworkStackHostTests.kt
+++ b/tests/hostdriven/host/src/com/android/networkstack/hosttests/NetworkStackHostTests.kt
@@ -16,7 +16,7 @@
 
 package com.android.networkstack.hosttests
 
-import com.android.tests.util.ModuleTestUtils
+import android.cts.install.lib.host.InstallUtilsHost
 import com.android.tradefed.device.ITestDevice
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
@@ -42,8 +42,7 @@
 @RunWith(DeviceJUnit4ClassRunner::class)
 class NetworkStackHostTests : BaseHostJUnit4Test() {
 
-    private val mUtils = ModuleTestUtils(this)
-    private val mModuleApk = mUtils.getTestFile(APP_APK)
+    private val mModuleApk = InstallUtilsHost(this).getTestFile(APP_APK)
     private val mPackageName = AaptParser.parse(mModuleApk)?.packageName
             ?: throw IllegalStateException("Could not parse test package name")
     private val mDevice by lazy { getDevice() }
@@ -74,12 +73,10 @@
         assumeFalse(error != null && error.contains("Unknown option --staged"))
         assertNull(error, "Error installing module package: $error")
         try {
-            mUtils.waitForStagedSessionReady()
             applyUpdateAndCheckNetworkStackRegistered()
             assertNotEquals(initialUpdateTime, getLastUpdateTime(), "Update time did not change")
         } finally {
             assertCommandSucceeds("pm rollback-app $mPackageName")
-            mUtils.waitForStagedSessionReady()
             applyUpdateAndCheckNetworkStackRegistered()
         }
     }
diff --git a/tests/hostlib/Android.bp b/tests/hostlib/Android.bp
index 30b2fb5..189a88c 100644
--- a/tests/hostlib/Android.bp
+++ b/tests/hostlib/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_library_host {
     name: "net-host-tests-utils",
     srcs: [
@@ -24,6 +28,6 @@
     ],
     static_libs: [
         "kotlin-test",
-        "module_test_util",
+        "cts-install-lib-host",
     ],
-}
\ No newline at end of file
+}
diff --git a/tests/hostlib/src/com/android/testutils/host/DeflakeHostTestBase.kt b/tests/hostlib/src/com/android/testutils/host/DeflakeHostTestBase.kt
index e4cc2ff..3119db7 100644
--- a/tests/hostlib/src/com/android/testutils/host/DeflakeHostTestBase.kt
+++ b/tests/hostlib/src/com/android/testutils/host/DeflakeHostTestBase.kt
@@ -16,7 +16,7 @@
 
 package com.android.testutils.host
 
-import com.android.tests.util.ModuleTestUtils
+import android.cts.install.lib.host.InstallUtilsHost
 import com.android.tradefed.config.Option
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
@@ -99,7 +99,7 @@
 
     @Test
     fun testDeflake() {
-        val apkFile = ModuleTestUtils(this).getTestFile(testApkFilename)
+        val apkFile = InstallUtilsHost(this).getTestFile(testApkFilename)
         val pkgName = AaptParser.parse(apkFile)?.packageName
                 ?: fail("Could not parse test package name")
 
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index af63f0e..3842b50 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_defaults {
     name: "NetworkStackIntegrationTestsJniDefaults",
     defaults: ["libnetworkstackutilsjni_deps"],
@@ -30,7 +34,10 @@
 
 java_defaults {
     name: "NetworkStackIntegrationTestsDefaults",
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.test.rules",
@@ -65,6 +72,7 @@
     platform_apis: true,
     test_suites: ["device-tests"],
     min_sdk_version: "29",
+    target_sdk_version: "30",
 }
 
 // Network stack next integration tests.
@@ -91,7 +99,10 @@
     platform_apis: true,
     min_sdk_version: "29",
     jarjar_rules: ":NetworkStackJarJarRules",
-    static_libs: ["NetworkStaticLibTestsLib"],
+    static_libs: [
+        "NetworkStaticLibTestsLib",
+        "NetdStaticLibTestsLib",
+    ],
 }
 
 // Special version of the network stack tests that includes all tests necessary for code coverage
@@ -101,10 +112,12 @@
     certificate: "networkstack",
     platform_apis: true,
     min_sdk_version: "29",
+    target_sdk_version: "30",
     test_suites: ["device-tests", "mts"],
     test_config: "AndroidTest_Coverage.xml",
     defaults: ["NetworkStackIntegrationTestsJniDefaults"],
     static_libs: [
+        "modules-utils-native-coverage-listener",
         "NetworkStackTestsLib",
         "NetworkStackIntegrationTestsLib",
         "NetworkStackStaticLibTestsLib",
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index 12f5d7d..bfd3735 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -16,7 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.server.networkstack.integrationtests"
           android:sharedUserId="android.uid.networkstack">
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
 
     <!-- Note: do not add any privileged or signature permissions that are granted
          to the network stack app. Otherwise, the test APK will install, but when the device is
diff --git a/tests/integration/AndroidManifest_coverage.xml b/tests/integration/AndroidManifest_coverage.xml
index 660e42d..fc91e59 100644
--- a/tests/integration/AndroidManifest_coverage.xml
+++ b/tests/integration/AndroidManifest_coverage.xml
@@ -16,7 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.server.networkstack.coverage"
           android:sharedUserId="android.uid.networkstack">
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
 
     <!-- Note: do not add any privileged or signature permissions that are granted
          to the network stack app. Otherwise, the test APK will install, but when the device is
diff --git a/tests/integration/AndroidTest_Coverage.xml b/tests/integration/AndroidTest_Coverage.xml
index e33fa87..3e7361b 100644
--- a/tests/integration/AndroidTest_Coverage.xml
+++ b/tests/integration/AndroidTest_Coverage.xml
@@ -23,5 +23,6 @@
         <option name="package" value="com.android.server.networkstack.coverage" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
+        <option name="device-listeners" value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
     </test>
 </configuration>
diff --git a/tests/integration/lint-baseline.xml b/tests/integration/lint-baseline.xml
new file mode 100644
index 0000000..eadec6f
--- /dev/null
+++ b/tests/integration/lint-baseline.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getDhcpServerAddress`"
+        errorLine1="        assertEquals(SERVER_ADDR, captor.getValue().getDhcpServerAddress());"
+        errorLine2="                                                    ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java"
+            line="1327"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="                argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));"
+        errorLine2="                                                          ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java"
+            line="1623"
+            column="59"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="                lp -> !Objects.equals(unchanged, lp.getNat64Prefix())));"
+        errorLine2="                                                    ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java"
+            line="1629"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="        if (lp.getNat64Prefix() != null) {"
+        errorLine2="               ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java"
+            line="1660"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.LinkProperties#getNat64Prefix`"
+        errorLine1="            assertEquals(prefix, lp.getNat64Prefix());"
+        errorLine2="                                    ~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java"
+            line="1661"
+            column="37"/>
+    </issue>
+
+</issues>
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
deleted file mode 100644
index 38eb84e..0000000
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
+++ /dev/null
@@ -1,2260 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.ip;
-
-import static android.net.dhcp.DhcpClient.EXPIRED_LEASE;
-import static android.net.dhcp.DhcpPacket.DHCP_BOOTREQUEST;
-import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
-import static android.net.dhcp.DhcpPacket.DHCP_MAGIC_COOKIE;
-import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
-import static android.net.dhcp.DhcpPacket.ENCAP_L2;
-import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
-import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
-import static android.net.dhcp.DhcpResultsParcelableUtil.fromStableParcelable;
-import static android.net.ipmemorystore.Status.SUCCESS;
-import static android.system.OsConstants.ETH_P_IPV6;
-import static android.system.OsConstants.IFA_F_TEMPORARY;
-import static android.system.OsConstants.IPPROTO_ICMPV6;
-import static android.system.OsConstants.IPPROTO_TCP;
-
-import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
-import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
-import static com.android.server.util.NetworkStackConstants.ARP_REPLY;
-import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
-import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.server.util.NetworkStackConstants.ETHER_HEADER_LEN;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV6;
-import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_CHECKSUM_OFFSET;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
-import static com.android.server.util.NetworkStackConstants.IPV6_HEADER_LEN;
-import static com.android.server.util.NetworkStackConstants.IPV6_LEN_OFFSET;
-import static com.android.server.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
-
-import static junit.framework.Assert.fail;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.contains;
-import static org.mockito.ArgumentMatchers.longThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.argThat;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.AlarmManager;
-import android.app.AlarmManager.OnAlarmListener;
-import android.app.Instrumentation;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Resources;
-import android.net.ConnectivityManager;
-import android.net.DhcpResults;
-import android.net.DhcpResultsParcelable;
-import android.net.INetd;
-import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
-import android.net.Layer2InformationParcelable;
-import android.net.Layer2PacketParcelable;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.MacAddress;
-import android.net.NetworkStackIpMemoryStore;
-import android.net.TestNetworkInterface;
-import android.net.TestNetworkManager;
-import android.net.Uri;
-import android.net.dhcp.DhcpClient;
-import android.net.dhcp.DhcpDeclinePacket;
-import android.net.dhcp.DhcpDiscoverPacket;
-import android.net.dhcp.DhcpPacket;
-import android.net.dhcp.DhcpPacket.ParseException;
-import android.net.dhcp.DhcpRequestPacket;
-import android.net.ipmemorystore.NetworkAttributes;
-import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
-import android.net.ipmemorystore.Status;
-import android.net.netlink.StructNdOptPref64;
-import android.net.shared.Layer2Information;
-import android.net.shared.ProvisioningConfiguration;
-import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
-import android.net.util.InterfaceParams;
-import android.net.util.IpUtils;
-import android.net.util.NetworkStackUtils;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.system.ErrnoException;
-import android.system.Os;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.util.StateMachine;
-import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
-import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.common.ShimUtils;
-import com.android.networkstack.arp.ArpPacket;
-import com.android.networkstack.metrics.IpProvisioningMetrics;
-import com.android.server.NetworkObserver;
-import com.android.server.NetworkObserverRegistry;
-import com.android.server.NetworkStackService.NetworkStackServiceManager;
-import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.HandlerUtilsKt;
-import com.android.testutils.TapPacketReader;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Objects;
-import java.util.Random;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-/**
- * Tests for IpClient.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class IpClientIntegrationTest {
-    private static final int DATA_BUFFER_LEN = 4096;
-    private static final int PACKET_TIMEOUT_MS = 5_000;
-    private static final int TEST_TIMEOUT_MS = 400;
-    private static final String TEST_L2KEY = "some l2key";
-    private static final String TEST_CLUSTER = "some cluster";
-    private static final int TEST_LEASE_DURATION_S = 3_600; // 1 hour
-
-    // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants.
-    private static final int IFA_F_STABLE_PRIVACY = 0x800;
-
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    @Mock private Context mContext;
-    @Mock private ConnectivityManager mCm;
-    @Mock private Resources mResources;
-    @Mock private IIpClientCallbacks mCb;
-    @Mock private AlarmManager mAlarm;
-    @Mock private ContentResolver mContentResolver;
-    @Mock private NetworkStackServiceManager mNetworkStackServiceManager;
-    @Mock private NetworkStackIpMemoryStore mIpMemoryStore;
-    @Mock private IpMemoryStoreService mIpMemoryStoreService;
-    @Mock private PowerManager.WakeLock mTimeoutWakeLock;
-
-    @Spy private INetd mNetd;
-
-    private String mIfaceName;
-    private NetworkObserverRegistry mNetworkObserverRegistry;
-    private HandlerThread mPacketReaderThread;
-    private Handler mHandler;
-    private TapPacketReader mPacketReader;
-    private FileDescriptor mTapFd;
-    private IpClient mIpc;
-    private Dependencies mDependencies;
-    private byte[] mClientMac;
-
-    // Ethernet header
-    private static final int ETH_HEADER_LEN = 14;
-
-    // IP header
-    private static final int IPV4_HEADER_LEN = 20;
-    private static final int IPV4_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 12;
-    private static final int IPV4_DST_ADDR_OFFSET = IPV4_SRC_ADDR_OFFSET + 4;
-
-    // UDP header
-    private static final int UDP_HEADER_LEN = 8;
-    private static final int UDP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN;
-    private static final int UDP_SRC_PORT_OFFSET = UDP_HEADER_OFFSET + 0;
-
-    // DHCP header
-    private static final int DHCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN
-            + UDP_HEADER_LEN;
-    private static final int DHCP_MESSAGE_OP_CODE_OFFSET = DHCP_HEADER_OFFSET + 0;
-    private static final int DHCP_TRANSACTION_ID_OFFSET = DHCP_HEADER_OFFSET + 4;
-    private static final int DHCP_OPTION_MAGIC_COOKIE_OFFSET = DHCP_HEADER_OFFSET + 236;
-
-    private static final Inet4Address SERVER_ADDR =
-            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.100");
-    private static final Inet4Address CLIENT_ADDR =
-            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.2");
-    private static final Inet4Address CLIENT_ADDR_NEW =
-            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.3");
-    private static final Inet4Address INADDR_ANY =
-            (Inet4Address) InetAddresses.parseNumericAddress("0.0.0.0");
-    private static final int PREFIX_LENGTH = 24;
-    private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH);
-    private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress(
-            SERVER_ADDR, PREFIX_LENGTH);
-    private static final String HOSTNAME = "testhostname";
-    private static final int TEST_DEFAULT_MTU = 1500;
-    private static final int TEST_MIN_MTU = 1280;
-    private static final byte[] SERVER_MAC = new byte[] { 0x00, 0x1A, 0x11, 0x22, 0x33, 0x44 };
-    private static final String TEST_HOST_NAME = "AOSP on Crosshatch";
-    private static final String TEST_HOST_NAME_TRANSLITERATION = "AOSP-on-Crosshatch";
-    private static final String TEST_CAPTIVE_PORTAL_URL = "https://example.com/capportapi";
-    private static final byte[] TEST_HOTSPOT_OUI = new byte[] {
-            (byte) 0x00, (byte) 0x17, (byte) 0xF2
-    };
-    private static final byte TEST_VENDOR_SPECIFIC_TYPE = 0x06;
-
-    private static final String TEST_DEFAULT_SSID = "test_ssid";
-    private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55";
-    private static final String TEST_DHCP_ROAM_SSID = "0001docomo";
-    private static final String TEST_DHCP_ROAM_BSSID = "00:4e:35:17:98:55";
-    private static final String TEST_DHCP_ROAM_L2KEY = "roaming_l2key";
-    private static final String TEST_DHCP_ROAM_CLUSTER = "roaming_cluster";
-    private static final byte[] TEST_AP_OUI = new byte[] { 0x00, 0x1A, 0x11 };
-
-    private class Dependencies extends IpClient.Dependencies {
-        private boolean mIsDhcpLeaseCacheEnabled;
-        private boolean mIsDhcpRapidCommitEnabled;
-        private boolean mIsDhcpIpConflictDetectEnabled;
-        // Can't use SparseIntArray, it doesn't have an easy way to know if a key is not present.
-        private HashMap<String, Integer> mIntConfigProperties = new HashMap<>();
-        private DhcpClient mDhcpClient;
-        private boolean mIsHostnameConfigurationEnabled;
-        private String mHostname;
-
-        public void setDhcpLeaseCacheEnabled(final boolean enable) {
-            mIsDhcpLeaseCacheEnabled = enable;
-        }
-
-        public void setDhcpRapidCommitEnabled(final boolean enable) {
-            mIsDhcpRapidCommitEnabled = enable;
-        }
-
-        public void setDhcpIpConflictDetectEnabled(final boolean enable) {
-            mIsDhcpIpConflictDetectEnabled = enable;
-        }
-
-        public void setHostnameConfiguration(final boolean enable, final String hostname) {
-            mIsHostnameConfigurationEnabled = enable;
-            mHostname = hostname;
-        }
-
-        @Override
-        public INetd getNetd(Context context) {
-            return mNetd;
-        }
-
-        @Override
-        public NetworkStackIpMemoryStore getIpMemoryStore(Context context,
-                NetworkStackServiceManager nssManager) {
-            return mIpMemoryStore;
-        }
-
-        @Override
-        public DhcpClient makeDhcpClient(Context context, StateMachine controller,
-                InterfaceParams ifParams, DhcpClient.Dependencies deps) {
-            mDhcpClient = DhcpClient.makeDhcpClient(context, controller, ifParams, deps);
-            return mDhcpClient;
-        }
-
-        @Override
-        public DhcpClient.Dependencies getDhcpClientDependencies(
-                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
-            return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
-                @Override
-                public boolean isFeatureEnabled(final Context context, final String name,
-                        final boolean defaultEnabled) {
-                    switch (name) {
-                        case NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION:
-                            return mIsDhcpRapidCommitEnabled;
-                        case NetworkStackUtils.DHCP_INIT_REBOOT_VERSION:
-                            return mIsDhcpLeaseCacheEnabled;
-                        case NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION:
-                            return mIsDhcpIpConflictDetectEnabled;
-                        default:
-                            fail("Invalid experiment flag: " + name);
-                            return false;
-                    }
-                }
-
-                @Override
-                public int getIntDeviceConfig(final String name, int minimumValue,
-                        int maximumValue, int defaultValue) {
-                    return getDeviceConfigPropertyInt(name, 0 /* default value */);
-                }
-
-                @Override
-                public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) {
-                    return mTimeoutWakeLock;
-                }
-
-                @Override
-                public boolean getSendHostnameOption(final Context context) {
-                    return mIsHostnameConfigurationEnabled;
-                }
-
-                @Override
-                public String getDeviceName(final Context context) {
-                    return mIsHostnameConfigurationEnabled ? mHostname : null;
-                }
-            };
-        }
-
-        @Override
-        public int getDeviceConfigPropertyInt(String name, int defaultValue) {
-            Integer value = mIntConfigProperties.get(name);
-            if (value == null) {
-                throw new IllegalStateException("Non-mocked device config property " + name);
-            }
-            return value;
-        }
-
-        public void setDeviceConfigProperty(String name, int value) {
-            mIntConfigProperties.put(name, value);
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mDependencies = new Dependencies();
-        when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
-        when(mContext.getSystemService(eq(ConnectivityManager.class))).thenReturn(mCm);
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mContext.getContentResolver()).thenReturn(mContentResolver);
-        when(mNetworkStackServiceManager.getIpMemoryStoreService())
-                .thenReturn(mIpMemoryStoreService);
-
-        mDependencies.setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67);
-        mDependencies.setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10);
-        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10);
-        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MIN_MS, 10);
-        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MAX_MS, 20);
-        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_ANNOUNCE_DELAY_MS, 10);
-        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_ANNOUNCE_INTERVAL_MS, 10);
-
-        setUpTapInterface();
-        setUpIpClient();
-    }
-
-    private void awaitIpClientShutdown() throws Exception {
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onQuit();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (mPacketReader != null) {
-            mHandler.post(() -> mPacketReader.stop()); // Also closes the socket
-            mTapFd = null;
-        }
-        if (mPacketReaderThread != null) {
-            mPacketReaderThread.quitSafely();
-        }
-        mIpc.shutdown();
-        awaitIpClientShutdown();
-    }
-
-    private void setUpTapInterface() {
-        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
-        // Adopt the shell permission identity to create a test TAP interface.
-        inst.getUiAutomation().adoptShellPermissionIdentity();
-
-        final TestNetworkInterface iface;
-        try {
-            final TestNetworkManager tnm = (TestNetworkManager)
-                    inst.getContext().getSystemService(Context.TEST_NETWORK_SERVICE);
-            iface = tnm.createTapInterface();
-        } finally {
-            // Drop the identity in order to regain the network stack permissions, which the shell
-            // does not have.
-            inst.getUiAutomation().dropShellPermissionIdentity();
-        }
-        mIfaceName = iface.getInterfaceName();
-        mClientMac = InterfaceParams.getByName(mIfaceName).macAddr.toByteArray();
-        mPacketReaderThread = new HandlerThread(IpClientIntegrationTest.class.getSimpleName());
-        mPacketReaderThread.start();
-        mHandler = mPacketReaderThread.getThreadHandler();
-
-        // Detach the FileDescriptor from the ParcelFileDescriptor.
-        // Otherwise, the garbage collector might call the ParcelFileDescriptor's finalizer, which
-        // closes the FileDescriptor and destroys our tap interface. An alternative would be to
-        // make the ParcelFileDescriptor or the TestNetworkInterface a class member so they never
-        // go out of scope.
-        mTapFd = new FileDescriptor();
-        mTapFd.setInt$(iface.getFileDescriptor().detachFd());
-        mPacketReader = new TapPacketReader(mHandler, mTapFd, DATA_BUFFER_LEN);
-        mHandler.post(() -> mPacketReader.start());
-    }
-
-    private void setUpIpClient() throws Exception {
-        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
-        final IBinder netdIBinder =
-                (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
-        mNetd = spy(INetd.Stub.asInterface(netdIBinder));
-        when(mContext.getSystemService(eq(Context.NETD_SERVICE))).thenReturn(netdIBinder);
-        assertNotNull(mNetd);
-
-        mNetworkObserverRegistry = new NetworkObserverRegistry();
-        mNetworkObserverRegistry.register(mNetd);
-        mIpc = new IpClient(mContext, mIfaceName, mCb, mNetworkObserverRegistry,
-                mNetworkStackServiceManager, mDependencies);
-        // Wait for IpClient to enter its initial state. Otherwise, additional setup steps or tests
-        // that mock IpClient's dependencies might interact with those mocks while IpClient is
-        // starting. This would cause UnfinishedStubbingExceptions as mocks cannot be interacted
-        // with while they are being stubbed.
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-
-        // Tell the IpMemoryStore immediately to answer any question about network attributes with a
-        // null response. Otherwise, the DHCP client will wait for two seconds before starting,
-        // while its query to the IpMemoryStore times out.
-        // This does not affect any test that makes the mock memory store return results, because
-        // unlike when(), it is documented that doAnswer() can be called more than once, to change
-        // the behaviour of a mock in the middle of a test.
-        doAnswer(invocation -> {
-            final String l2Key = invocation.getArgument(0);
-            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
-                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
-            return null;
-        }).when(mIpMemoryStore).retrieveNetworkAttributes(any(), any());
-
-        disableIpv6ProvisioningDelays();
-    }
-
-    private <T> T verifyWithTimeout(InOrder inOrder, T t) {
-        if (inOrder != null) {
-            return inOrder.verify(t, timeout(TEST_TIMEOUT_MS));
-        } else {
-            return verify(t, timeout(TEST_TIMEOUT_MS));
-        }
-    }
-
-    private void expectAlarmCancelled(InOrder inOrder, OnAlarmListener listener) {
-        inOrder.verify(mAlarm, timeout(TEST_TIMEOUT_MS)).cancel(eq(listener));
-    }
-
-    private OnAlarmListener expectAlarmSet(InOrder inOrder, String tagMatch, int afterSeconds) {
-        // Allow +/- 3 seconds to prevent flaky tests.
-        final long when = SystemClock.elapsedRealtime() + afterSeconds * 1000;
-        final long min = when - 3 * 1000;
-        final long max = when + 3 * 1000;
-        ArgumentCaptor<OnAlarmListener> captor = ArgumentCaptor.forClass(OnAlarmListener.class);
-        verifyWithTimeout(inOrder, mAlarm).setExact(
-                anyInt(), longThat(x -> x >= min && x <= max),
-                contains(tagMatch), captor.capture(), eq(mIpc.getHandler()));
-        return captor.getValue();
-    }
-
-    private boolean packetContainsExpectedField(final byte[] packet, final int offset,
-            final byte[] expected) {
-        if (packet.length < offset + expected.length) return false;
-        for (int i = 0; i < expected.length; ++i) {
-            if (packet[offset + i] != expected[i]) return false;
-        }
-        return true;
-    }
-
-    private boolean isDhcpPacket(final byte[] packet) {
-        final ByteBuffer buffer = ByteBuffer.wrap(packet);
-
-        // check the packet length
-        if (packet.length < DHCP_HEADER_OFFSET) return false;
-
-        // check the source port and dest port in UDP header
-        buffer.position(UDP_SRC_PORT_OFFSET);
-        final short udpSrcPort = buffer.getShort();
-        final short udpDstPort = buffer.getShort();
-        if (udpSrcPort != DHCP_CLIENT || udpDstPort != DHCP_SERVER) return false;
-
-        // check DHCP message type
-        buffer.position(DHCP_MESSAGE_OP_CODE_OFFSET);
-        final byte dhcpOpCode = buffer.get();
-        if (dhcpOpCode != DHCP_BOOTREQUEST) return false;
-
-        // check DHCP magic cookie
-        buffer.position(DHCP_OPTION_MAGIC_COOKIE_OFFSET);
-        final int dhcpMagicCookie = buffer.getInt();
-        if (dhcpMagicCookie != DHCP_MAGIC_COOKIE) return false;
-
-        return true;
-    }
-
-    private ArpPacket parseArpPacketOrNull(final byte[] packet) {
-        try {
-            return ArpPacket.parseArpPacket(packet, packet.length);
-        } catch (ArpPacket.ParseException e) {
-            return null;
-        }
-    }
-
-    private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
-            final Integer leaseTimeSec, final short mtu, final String captivePortalUrl) {
-        return DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
-                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
-                CLIENT_ADDR /* yourIp */, packet.getClientMac(), leaseTimeSec,
-                NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
-                Collections.singletonList(SERVER_ADDR) /* gateways */,
-                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
-                SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, HOSTNAME,
-                false /* metered */, mtu, captivePortalUrl);
-    }
-
-    private static ByteBuffer buildDhcpAckPacket(final DhcpPacket packet,
-            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
-            final boolean rapidCommit, final String captivePortalApiUrl) {
-        return DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
-                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
-                clientAddress /* yourIp */, CLIENT_ADDR /* requestIp */, packet.getClientMac(),
-                leaseTimeSec, NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
-                Collections.singletonList(SERVER_ADDR) /* gateways */,
-                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
-                SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, HOSTNAME,
-                false /* metered */, mtu, rapidCommit, captivePortalApiUrl);
-    }
-
-    private static ByteBuffer buildDhcpNakPacket(final DhcpPacket packet) {
-        return DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
-            SERVER_ADDR /* serverIp */, INADDR_ANY /* relayIp */, packet.getClientMac(),
-            false /* broadcast */, "duplicated request IP address");
-    }
-
-    private void sendArpReply(final byte[] clientMac) throws IOException {
-        final ByteBuffer packet = ArpPacket.buildArpPacket(clientMac /* dst */,
-                SERVER_MAC /* src */, INADDR_ANY.getAddress() /* target IP */,
-                clientMac /* target HW address */, CLIENT_ADDR.getAddress() /* sender IP */,
-                (short) ARP_REPLY);
-        mPacketReader.sendResponse(packet);
-    }
-
-    private void sendArpProbe() throws IOException {
-        final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST /* dst */,
-                SERVER_MAC /* src */, CLIENT_ADDR.getAddress() /* target IP */,
-                new byte[ETHER_ADDR_LEN] /* target HW address */,
-                INADDR_ANY.getAddress() /* sender IP */, (short) ARP_REQUEST);
-        mPacketReader.sendResponse(packet);
-    }
-
-    private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
-            final boolean shouldReplyRapidCommitAck, final boolean isPreconnectionEnabled,
-            final boolean isDhcpIpConflictDetectEnabled,
-            final boolean isHostnameConfigurationEnabled, final String hostname,
-            final String displayName, final ScanResultInfo scanResultInfo)
-            throws RemoteException {
-        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
-                        MacAddress.fromString(TEST_DEFAULT_BSSID)))
-                .withoutIPv6();
-        if (isPreconnectionEnabled) prov.withPreconnection();
-        if (displayName != null) prov.withDisplayName(displayName);
-        if (scanResultInfo != null) prov.withScanResultInfo(scanResultInfo);
-
-        mDependencies.setDhcpLeaseCacheEnabled(isDhcpLeaseCacheEnabled);
-        mDependencies.setDhcpRapidCommitEnabled(shouldReplyRapidCommitAck);
-        mDependencies.setDhcpIpConflictDetectEnabled(isDhcpIpConflictDetectEnabled);
-        mDependencies.setHostnameConfiguration(isHostnameConfigurationEnabled, hostname);
-        mIpc.startProvisioning(prov.build());
-        if (!isPreconnectionEnabled) {
-            verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
-        }
-        verify(mCb, never()).onProvisioningFailure(any());
-    }
-
-    private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
-            final boolean isDhcpRapidCommitEnabled, final boolean isPreconnectionEnabled,
-            final boolean isDhcpIpConflictDetectEnabled)
-            throws RemoteException {
-        startIpClientProvisioning(isDhcpLeaseCacheEnabled, isDhcpRapidCommitEnabled,
-                isPreconnectionEnabled, isDhcpIpConflictDetectEnabled,
-                false /* isHostnameConfigurationEnabled */, null /* hostname */,
-                null /* displayName */, null /* ScanResultInfo */);
-    }
-
-    private void assertIpMemoryStoreNetworkAttributes(final Integer leaseTimeSec,
-            final long startTime, final int mtu) {
-        final ArgumentCaptor<NetworkAttributes> networkAttributes =
-                ArgumentCaptor.forClass(NetworkAttributes.class);
-
-        verify(mIpMemoryStore, timeout(TEST_TIMEOUT_MS))
-            .storeNetworkAttributes(eq(TEST_L2KEY), networkAttributes.capture(), any());
-        final NetworkAttributes naValueCaptured = networkAttributes.getValue();
-        assertEquals(CLIENT_ADDR, naValueCaptured.assignedV4Address);
-        if (leaseTimeSec == null || leaseTimeSec.intValue() == DhcpPacket.INFINITE_LEASE) {
-            assertEquals(Long.MAX_VALUE, naValueCaptured.assignedV4AddressExpiry.longValue());
-        } else {
-            // check the lease expiry's scope
-            final long upperBound = startTime + 7_200_000; // start timestamp + 2h
-            final long lowerBound = startTime + 3_600_000; // start timestamp + 1h
-            final long expiry = naValueCaptured.assignedV4AddressExpiry;
-            assertTrue(upperBound > expiry);
-            assertTrue(lowerBound < expiry);
-        }
-        assertEquals(Collections.singletonList(SERVER_ADDR), naValueCaptured.dnsAddresses);
-        assertEquals(new Integer(mtu), naValueCaptured.mtu);
-    }
-
-    private void assertIpMemoryNeverStoreNetworkAttributes() {
-        verify(mIpMemoryStore, never()).storeNetworkAttributes(any(), any(), any());
-    }
-
-    private void assertHostname(final boolean isHostnameConfigurationEnabled,
-            final String hostname, final String hostnameAfterTransliteration,
-            final List<DhcpPacket> packetList) throws Exception {
-        for (DhcpPacket packet : packetList) {
-            if (!isHostnameConfigurationEnabled || hostname == null) {
-                assertNoHostname(packet.getHostname());
-            } else {
-                assertEquals(packet.getHostname(), hostnameAfterTransliteration);
-            }
-        }
-    }
-
-    private void assertNoHostname(String hostname) {
-        if (ShimUtils.isAtLeastR()) {
-            assertNull(hostname);
-        } else {
-            // Until Q, if no hostname is set, the device falls back to the hostname set via
-            // system property, to avoid breaking Q devices already launched with that setup.
-            assertEquals(SystemProperties.get("net.hostname"), hostname);
-        }
-    }
-
-    // Helper method to complete DHCP 2-way or 4-way handshake
-    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
-            final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
-            final boolean shouldReplyRapidCommitAck, final int mtu,
-            final boolean isDhcpIpConflictDetectEnabled,
-            final boolean isHostnameConfigurationEnabled, final String hostname,
-            final String captivePortalApiUrl, final String displayName,
-            final ScanResultInfo scanResultInfo) throws Exception {
-        startIpClientProvisioning(isDhcpLeaseCacheEnabled, shouldReplyRapidCommitAck,
-                false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled,
-                isHostnameConfigurationEnabled, hostname, displayName, scanResultInfo);
-        return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck, mtu,
-                captivePortalApiUrl);
-    }
-
-    private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
-            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
-            final String captivePortalApiUrl) throws Exception {
-        final List<DhcpPacket> packetList = new ArrayList<>();
-        DhcpPacket packet;
-        while ((packet = getNextDhcpPacket()) != null) {
-            packetList.add(packet);
-            if (packet instanceof DhcpDiscoverPacket) {
-                if (shouldReplyRapidCommitAck) {
-                    mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec,
-                              (short) mtu, true /* rapidCommit */, captivePortalApiUrl));
-                } else {
-                    mPacketReader.sendResponse(buildDhcpOfferPacket(packet, leaseTimeSec,
-                            (short) mtu, captivePortalApiUrl));
-                }
-            } else if (packet instanceof DhcpRequestPacket) {
-                final ByteBuffer byteBuffer = isSuccessLease
-                        ? buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec, (short) mtu,
-                                false /* rapidCommit */, captivePortalApiUrl)
-                        : buildDhcpNakPacket(packet);
-                mPacketReader.sendResponse(byteBuffer);
-            } else {
-                fail("invalid DHCP packet");
-            }
-
-            // wait for reply to DHCPOFFER packet if disabling rapid commit option
-            if (shouldReplyRapidCommitAck || !(packet instanceof DhcpDiscoverPacket)) {
-                return packetList;
-            }
-        }
-        fail("No DHCPREQUEST received on interface");
-        return packetList;
-    }
-
-    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
-            final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
-            final boolean isDhcpRapidCommitEnabled, final int mtu,
-            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
-        return performDhcpHandshake(isSuccessLease, leaseTimeSec, isDhcpLeaseCacheEnabled,
-                isDhcpRapidCommitEnabled, mtu, isDhcpIpConflictDetectEnabled,
-                false /* isHostnameConfigurationEnabled */, null /* hostname */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
-    }
-
-    private List<DhcpPacket> performDhcpHandshake() throws Exception {
-        return performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                false /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-    }
-
-    private DhcpPacket getNextDhcpPacket() throws ParseException {
-        byte[] packet;
-        while ((packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS)) != null) {
-            if (!isDhcpPacket(packet)) continue;
-            return DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L2);
-        }
-        fail("No expected DHCP packet received on interface within timeout");
-        return null;
-    }
-
-    private DhcpPacket getReplyFromDhcpLease(final NetworkAttributes na, boolean timeout)
-            throws Exception {
-        doAnswer(invocation -> {
-            if (timeout) return null;
-            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
-                    .onNetworkAttributesRetrieved(new Status(SUCCESS), TEST_L2KEY, na);
-            return null;
-        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
-        startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */,
-                false /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        return getNextDhcpPacket();
-    }
-
-    private void removeTapInterface(final FileDescriptor fd) {
-        try {
-            Os.close(fd);
-        } catch (ErrnoException e) {
-            fail("Fail to close file descriptor: " + e);
-        }
-    }
-
-    private void verifyAfterIpClientShutdown() throws RemoteException {
-        final LinkProperties emptyLp = new LinkProperties();
-        emptyLp.setInterfaceName(mIfaceName);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(emptyLp);
-    }
-
-    // Verify IPv4-only provisioning success. No need to verify IPv4 provisioning when below cases
-    // happen:
-    // 1. if there's a failure lease, onProvisioningSuccess() won't be called;
-    // 2. if duplicated IPv4 address detection is enabled, verify TIMEOUT will affect ARP packets
-    //    capture running in other test cases.
-    // 3. if IPv6 is enabled, e.g. withoutIPv6() isn't called when starting provisioning.
-    private void verifyIPv4OnlyProvisioningSuccess(final Collection<InetAddress> addresses)
-            throws Exception {
-        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
-        LinkProperties lp = captor.getValue();
-        assertNotNull(lp);
-        assertNotEquals(0, lp.getDnsServers().size());
-        assertEquals(addresses.size(), lp.getAddresses().size());
-        assertTrue(lp.getAddresses().containsAll(addresses));
-    }
-
-    private void doRestoreInitialMtuTest(final boolean shouldChangeMtu,
-            final boolean shouldRemoveTapInterface) throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        int mtu = TEST_DEFAULT_MTU;
-
-        if (shouldChangeMtu) mtu = TEST_MIN_MTU;
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                mtu, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, mtu);
-
-        if (shouldChangeMtu) {
-            // Pretend that ConnectivityService set the MTU.
-            mNetd.interfaceSetMtu(mIfaceName, mtu);
-            assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), mtu);
-        }
-
-        // Sometimes, IpClient receives an update with an empty LinkProperties during startup,
-        // when the link-local address is deleted after interface bringup. Reset expectations
-        // here to ensure that verifyAfterIpClientShutdown does not fail because it sees two
-        // empty LinkProperties changes instead of one.
-        reset(mCb);
-
-        if (shouldRemoveTapInterface) removeTapInterface(mTapFd);
-        try {
-            mIpc.shutdown();
-            awaitIpClientShutdown();
-            if (shouldRemoveTapInterface) {
-                verify(mNetd, never()).interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
-            } else {
-                // Verify that MTU indeed has been restored or not.
-                verify(mNetd, times(shouldChangeMtu ? 1 : 0))
-                        .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
-            }
-            verifyAfterIpClientShutdown();
-        } catch (Exception e) {
-            fail("Exception should not have been thrown after shutdown: " + e);
-        }
-    }
-
-    private DhcpPacket assertDiscoverPacketOnPreconnectionStart() throws Exception {
-        final ArgumentCaptor<List<Layer2PacketParcelable>> l2PacketList =
-                ArgumentCaptor.forClass(List.class);
-
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onPreconnectionStart(l2PacketList.capture());
-        final byte[] payload = l2PacketList.getValue().get(0).payload;
-        DhcpPacket packet = DhcpPacket.decodeFullPacket(payload, payload.length, ENCAP_L2);
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-        assertArrayEquals(INADDR_BROADCAST.getAddress(),
-                Arrays.copyOfRange(payload, IPV4_DST_ADDR_OFFSET, IPV4_DST_ADDR_OFFSET + 4));
-        return packet;
-    }
-
-    private void doIpClientProvisioningWithPreconnectionTest(
-            final boolean shouldReplyRapidCommitAck, final boolean shouldAbortPreconnection,
-            final boolean shouldFirePreconnectionTimeout,
-            final boolean timeoutBeforePreconnectionComplete) throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        final ArgumentCaptor<InterfaceConfigurationParcel> ifConfig =
-                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
-
-        startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */,
-                shouldReplyRapidCommitAck, true /* isDhcpPreConnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        DhcpPacket packet = assertDiscoverPacketOnPreconnectionStart();
-        final int preconnDiscoverTransId = packet.getTransactionId();
-
-        if (shouldAbortPreconnection) {
-            if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
-                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
-            }
-
-            mIpc.notifyPreconnectionComplete(false /* abort */);
-            HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-
-            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
-                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
-            }
-
-            // Either way should get DhcpClient go back to INIT state, and broadcast
-            // DISCOVER with new transaction ID.
-            packet = getNextDhcpPacket();
-            assertTrue(packet instanceof DhcpDiscoverPacket);
-            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
-        } else if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
-            // If timeout fires before success preconnection, DhcpClient will go back to INIT state,
-            // and broadcast DISCOVER with new transaction ID.
-            mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
-            packet = getNextDhcpPacket();
-            assertTrue(packet instanceof DhcpDiscoverPacket);
-            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
-            // any old response would be ignored due to mismatched transaction ID.
-        }
-
-        final short mtu = (short) TEST_DEFAULT_MTU;
-        if (!shouldReplyRapidCommitAck) {
-            mPacketReader.sendResponse(buildDhcpOfferPacket(packet, TEST_LEASE_DURATION_S, mtu,
-                    null /* captivePortalUrl */));
-            packet = getNextDhcpPacket();
-            assertTrue(packet instanceof DhcpRequestPacket);
-        }
-        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, TEST_LEASE_DURATION_S,
-                mtu, shouldReplyRapidCommitAck, null /* captivePortalUrl */));
-
-        if (!shouldAbortPreconnection) {
-            mIpc.notifyPreconnectionComplete(true /* success */);
-            HandlerUtilsKt.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
-
-            // If timeout fires after successful preconnection, right now DhcpClient will have
-            // already entered BOUND state, the delayed CMD_TIMEOUT command would be ignored. So
-            // this case should be very rare, because the timeout alarm is cancelled when state
-            // machine exits from Preconnecting state.
-            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
-                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
-            }
-        }
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
-
-        final LinkAddress ipAddress = new LinkAddress(CLIENT_ADDR, PREFIX_LENGTH);
-        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetCfg(ifConfig.capture());
-        assertEquals(ifConfig.getValue().ifName, mIfaceName);
-        assertEquals(ifConfig.getValue().ipv4Addr, ipAddress.getAddress().getHostAddress());
-        assertEquals(ifConfig.getValue().prefixLength, PREFIX_LENGTH);
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    private ArpPacket getNextArpPacket(final int timeout) throws Exception {
-        byte[] packet;
-        while ((packet = mPacketReader.popPacket(timeout)) != null) {
-            final ArpPacket arpPacket = parseArpPacketOrNull(packet);
-            if (arpPacket != null) return arpPacket;
-        }
-        return null;
-    }
-
-    private ArpPacket getNextArpPacket() throws Exception {
-        final ArpPacket packet = getNextArpPacket(PACKET_TIMEOUT_MS);
-        assertNotNull("No expected ARP packet received on interface within timeout", packet);
-        return packet;
-    }
-
-    private void assertArpPacket(final ArpPacket packet) {
-        assertEquals(packet.opCode, ARP_REQUEST);
-        assertEquals(packet.targetIp, CLIENT_ADDR);
-        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
-    }
-
-    private void assertArpProbe(final ArpPacket packet) {
-        assertArpPacket(packet);
-        assertEquals(packet.senderIp, INADDR_ANY);
-    }
-
-    private void assertArpAnnounce(final ArpPacket packet) {
-        assertArpPacket(packet);
-        assertEquals(packet.senderIp, CLIENT_ADDR);
-    }
-
-    private void doIpAddressConflictDetectionTest(final boolean causeIpAddressConflict,
-            final boolean shouldReplyRapidCommitAck, final boolean isDhcpIpConflictDetectEnabled,
-            final boolean shouldResponseArpReply) throws Exception {
-        final long currentTime = System.currentTimeMillis();
-
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, shouldReplyRapidCommitAck,
-                TEST_DEFAULT_MTU, isDhcpIpConflictDetectEnabled);
-
-        // If we receive an ARP packet here, it's guaranteed to be from IP conflict detection,
-        // because at this time the test interface does not have an IP address and therefore
-        // won't send ARP for anything.
-        if (causeIpAddressConflict) {
-            final ArpPacket arpProbe = getNextArpPacket();
-            assertArpProbe(arpProbe);
-
-            if (shouldResponseArpReply) {
-                sendArpReply(mClientMac);
-            } else {
-                sendArpProbe();
-            }
-            final DhcpPacket packet = getNextDhcpPacket();
-            assertTrue(packet instanceof DhcpDeclinePacket);
-            assertEquals(packet.mServerIdentifier, SERVER_ADDR);
-            assertEquals(packet.mRequestedIp, CLIENT_ADDR);
-
-            verify(mCb, never()).onProvisioningFailure(any());
-            assertIpMemoryNeverStoreNetworkAttributes();
-        } else if (isDhcpIpConflictDetectEnabled) {
-            int arpPacketCount = 0;
-            final List<ArpPacket> packetList = new ArrayList<ArpPacket>();
-            // Total sent ARP packets should be 5 (3 ARP Probes + 2 ARP Announcements)
-            ArpPacket packet;
-            while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
-                packetList.add(packet);
-            }
-            assertEquals(5, packetList.size());
-            assertArpProbe(packetList.get(0));
-            assertArpAnnounce(packetList.get(3));
-        } else {
-            verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
-                    TEST_DEFAULT_MTU);
-        }
-    }
-
-    @Test
-    public void testInterfaceParams() throws Exception {
-        InterfaceParams params = InterfaceParams.getByName(mIfaceName);
-        assertNotNull(params);
-        assertEquals(mIfaceName, params.name);
-        assertTrue(params.index > 0);
-        assertNotNull(params.macAddr);
-        assertTrue(params.hasMacAddress);
-
-        // Sanity check.
-        params = InterfaceParams.getByName("lo");
-        assertNotNull(params);
-        assertEquals("lo", params.name);
-        assertTrue(params.index > 0);
-        assertNotNull(params.macAddr);
-        assertFalse(params.hasMacAddress);
-    }
-
-    @Test
-    public void testDhcpInit() throws Exception {
-        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
-                false /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        final DhcpPacket packet = getNextDhcpPacket();
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test
-    public void testHandleSuccessDhcpLease() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testHandleFailureDhcpLease() throws Exception {
-        performDhcpHandshake(false /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-
-        verify(mCb, never()).onProvisioningSuccess(any());
-        assertIpMemoryNeverStoreNetworkAttributes();
-    }
-
-    @Test
-    public void testHandleInfiniteLease() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        performDhcpHandshake(true /* isSuccessLease */, INFINITE_LEASE,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(INFINITE_LEASE, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testHandleNoLease() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        performDhcpHandshake(true /* isSuccessLease */, null /* no lease time */,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(null, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test @IgnoreAfter(Build.VERSION_CODES.Q) // INIT-REBOOT is enabled on R.
-    public void testHandleDisableInitRebootState() throws Exception {
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                false /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryNeverStoreNetworkAttributes();
-    }
-
-    @Test
-    public void testHandleRapidCommitOption() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, true /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testDhcpClientStartWithCachedInfiniteLease() throws Exception {
-        final DhcpPacket packet = getReplyFromDhcpLease(
-                new NetworkAttributes.Builder()
-                    .setAssignedV4Address(CLIENT_ADDR)
-                    .setAssignedV4AddressExpiry(Long.MAX_VALUE) // lease is always valid
-                    .setMtu(new Integer(TEST_DEFAULT_MTU))
-                    .setCluster(TEST_CLUSTER)
-                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
-                    .build(), false /* timeout */);
-        assertTrue(packet instanceof DhcpRequestPacket);
-    }
-
-    @Test
-    public void testDhcpClientStartWithCachedExpiredLease() throws Exception {
-        final DhcpPacket packet = getReplyFromDhcpLease(
-                 new NetworkAttributes.Builder()
-                    .setAssignedV4Address(CLIENT_ADDR)
-                    .setAssignedV4AddressExpiry(EXPIRED_LEASE)
-                    .setMtu(new Integer(TEST_DEFAULT_MTU))
-                    .setCluster(TEST_CLUSTER)
-                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
-                    .build(), false /* timeout */);
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test
-    public void testDhcpClientStartWithNullRetrieveNetworkAttributes() throws Exception {
-        final DhcpPacket packet = getReplyFromDhcpLease(null /* na */, false /* timeout */);
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test
-    public void testDhcpClientStartWithTimeoutRetrieveNetworkAttributes() throws Exception {
-        final DhcpPacket packet = getReplyFromDhcpLease(
-                new NetworkAttributes.Builder()
-                    .setAssignedV4Address(CLIENT_ADDR)
-                    .setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000)
-                    .setMtu(new Integer(TEST_DEFAULT_MTU))
-                    .setCluster(TEST_CLUSTER)
-                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
-                    .build(), true /* timeout */);
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test
-    public void testDhcpClientStartWithCachedLeaseWithoutIPAddress() throws Exception {
-        final DhcpPacket packet = getReplyFromDhcpLease(
-                new NetworkAttributes.Builder()
-                    .setMtu(new Integer(TEST_DEFAULT_MTU))
-                    .setCluster(TEST_CLUSTER)
-                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
-                    .build(), false /* timeout */);
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test
-    public void testDhcpClientRapidCommitEnabled() throws Exception {
-        startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */,
-                true /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        final DhcpPacket packet = getNextDhcpPacket();
-        assertTrue(packet instanceof DhcpDiscoverPacket);
-    }
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    public void testDhcpServerInLinkProperties() throws Exception {
-        assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q);
-
-        performDhcpHandshake();
-        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
-        assertEquals(SERVER_ADDR, captor.getValue().getDhcpServerAddress());
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu() throws Exception {
-        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu_WithoutMtuChange() throws Exception {
-        doRestoreInitialMtuTest(false /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu_WithException() throws Exception {
-        doThrow(new RemoteException("NetdNativeService::interfaceSetMtu")).when(mNetd)
-                .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
-
-        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
-        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStopping() throws Exception {
-        doRestoreInitialMtuTest(true /* shouldChangeMtu */, true /* shouldRemoveTapInterface */);
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStartingProvisioning()
-            throws Exception {
-        removeTapInterface(mTapFd);
-        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .withoutIPv6()
-                .build();
-
-        mIpc.startProvisioning(config);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
-        verify(mCb, never()).setNeighborDiscoveryOffload(true);
-    }
-
-    @Test
-    public void testRestoreInitialInterfaceMtu_stopIpClientAndRestart() throws Exception {
-        long currentTime = System.currentTimeMillis();
-
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_MIN_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_MIN_MTU);
-
-        // Pretend that ConnectivityService set the MTU.
-        mNetd.interfaceSetMtu(mIfaceName, TEST_MIN_MTU);
-        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
-
-        reset(mCb);
-        reset(mIpMemoryStore);
-
-        // Stop IpClient and then restart provisioning immediately.
-        mIpc.stop();
-        currentTime = System.currentTimeMillis();
-        // Intend to set mtu option to 0, then verify that won't influence interface mtu restore.
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                0 /* mtu */, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, 0 /* mtu */);
-        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_DEFAULT_MTU);
-    }
-
-    private boolean isRouterSolicitation(final byte[] packetBytes) {
-        ByteBuffer packet = ByteBuffer.wrap(packetBytes);
-        return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
-                && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
-                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN)
-                        == (byte) ICMPV6_ROUTER_SOLICITATION;
-    }
-
-    private void waitForRouterSolicitation() throws ParseException {
-        byte[] packet;
-        while ((packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS)) != null) {
-            if (isRouterSolicitation(packet)) return;
-        }
-        fail("No router solicitation received on interface within timeout");
-    }
-
-    private void sendRouterAdvertisement(boolean waitForRs, short lifetime) throws Exception {
-        final String dnsServer = "2001:4860:4860::64";
-        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
-        ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
-        ByteBuffer ra = buildRaPacket(lifetime, pio, rdnss);
-
-        if (waitForRs) {
-            waitForRouterSolicitation();
-        }
-
-        mPacketReader.sendResponse(ra);
-    }
-
-    private void sendBasicRouterAdvertisement(boolean waitForRs) throws Exception {
-        sendRouterAdvertisement(waitForRs, (short) 1800);
-    }
-
-    private void sendRouterAdvertisementWithZeroLifetime() throws Exception {
-        sendRouterAdvertisement(false /* waitForRs */, (short) 0);
-    }
-
-    // TODO: move this and the following method to a common location and use them in ApfTest.
-    private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
-            throws Exception {
-        final int optLen = 4;
-        IpPrefix prefix = new IpPrefix(prefixString);
-        ByteBuffer option = ByteBuffer.allocate(optLen * ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR);
-        option.put((byte) ICMPV6_ND_OPTION_PIO);      // Type
-        option.put((byte) optLen);                    // Length in 8-byte units
-        option.put((byte) prefix.getPrefixLength());  // Prefix length
-        option.put((byte) 0b11000000);                // L = 1, A = 1
-        option.putInt(valid);
-        option.putInt(preferred);
-        option.putInt(0);                             // Reserved
-        option.put(prefix.getRawAddress());
-        option.flip();
-        return option;
-    }
-
-    private static ByteBuffer buildRdnssOption(int lifetime, String... servers) throws Exception {
-        final int optLen = 1 + 2 * servers.length;
-        ByteBuffer option = ByteBuffer.allocate(optLen * ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR);
-        option.put((byte) ICMPV6_ND_OPTION_RDNSS);  // Type
-        option.put((byte) optLen);                  // Length in 8-byte units
-        option.putShort((short) 0);                 // Reserved
-        option.putInt(lifetime);                    // Lifetime
-        for (String server : servers) {
-            option.put(InetAddress.getByName(server).getAddress());
-        }
-        option.flip();
-        return option;
-    }
-
-    // HACK: these functions are here because IpUtils#transportChecksum is private. Even if we made
-    // that public, it won't be available on Q devices, and this test needs to run on Q devices.
-    // TODO: move the IpUtils code to frameworks/lib/net and link it statically.
-    private static int checksumFold(int sum) {
-        while (sum > 0xffff) {
-            sum = (sum >> 16) + (sum & 0xffff);
-        }
-        return sum;
-    }
-
-    private static short checksumAdjust(short checksum, short oldWord, short newWord) {
-        checksum = (short) ~checksum;
-        int tempSum = checksumFold(uint16(checksum) + uint16(newWord) + 0xffff - uint16(oldWord));
-        return (short) ~tempSum;
-    }
-
-    public static int uint16(short s) {
-        return s & 0xffff;
-    }
-
-    private static short icmpv6Checksum(ByteBuffer buf, int ipOffset, int transportOffset,
-            int transportLen) {
-        // The ICMPv6 checksum is the same as the TCP checksum, except the pseudo-header uses
-        // 58 (ICMPv6) instead of 6 (TCP). Calculate the TCP checksum, and then do an incremental
-        // checksum adjustment  for the change in the next header byte.
-        short checksum = IpUtils.tcpChecksum(buf, ipOffset, transportOffset, transportLen);
-        return checksumAdjust(checksum, (short) IPPROTO_TCP, (short) IPPROTO_ICMPV6);
-    }
-
-    private static ByteBuffer buildRaPacket(short lifetime, ByteBuffer... options)
-            throws Exception {
-        final MacAddress srcMac = MacAddress.fromString("33:33:00:00:00:01");
-        final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06");
-        final byte[] routerLinkLocal = InetAddresses.parseNumericAddress("fe80::1").getAddress();
-        final byte[] allNodes = InetAddresses.parseNumericAddress("ff02::1").getAddress();
-
-        final ByteBuffer packet = ByteBuffer.allocate(TEST_DEFAULT_MTU);
-        int icmpLen = ICMPV6_RA_HEADER_LEN;
-
-        // Ethernet header.
-        packet.put(srcMac.toByteArray());
-        packet.put(dstMac.toByteArray());
-        packet.putShort((short) ETHER_TYPE_IPV6);
-
-        // IPv6 header.
-        packet.putInt(0x600abcde);                       // Version, traffic class, flowlabel
-        packet.putShort((short) 0);                      // Length, TBD
-        packet.put((byte) IPPROTO_ICMPV6);               // Next header
-        packet.put((byte) 0xff);                         // Hop limit
-        packet.put(routerLinkLocal);                     // Source address
-        packet.put(allNodes);                            // Destination address
-
-        // Router advertisement.
-        packet.put((byte) ICMPV6_ROUTER_ADVERTISEMENT);  // ICMP type
-        packet.put((byte) 0);                            // ICMP code
-        packet.putShort((short) 0);                      // Checksum, TBD
-        packet.put((byte) 0);                            // Hop limit, unspecified
-        packet.put((byte) 0);                            // M=0, O=0
-        packet.putShort(lifetime);                       // Router lifetime
-        packet.putInt(0);                                // Reachable time, unspecified
-        packet.putInt(100);                              // Retrans time 100ms.
-
-        for (ByteBuffer option : options) {
-            packet.put(option);
-            option.clear();  // So we can reuse it in a future packet.
-            icmpLen += option.capacity();
-        }
-
-        // Populate length and checksum fields.
-        final int transportOffset = ETHER_HEADER_LEN + IPV6_HEADER_LEN;
-        final short checksum = icmpv6Checksum(packet, ETHER_HEADER_LEN, transportOffset, icmpLen);
-        packet.putShort(ETHER_HEADER_LEN + IPV6_LEN_OFFSET, (short) icmpLen);
-        packet.putShort(transportOffset + ICMPV6_CHECKSUM_OFFSET, checksum);
-
-        packet.flip();
-        return packet;
-    }
-
-    private static ByteBuffer buildRaPacket(ByteBuffer... options) throws Exception {
-        return buildRaPacket((short) 1800, options);
-    }
-
-    private void disableIpv6ProvisioningDelays() throws Exception {
-        // Speed up the test by disabling DAD and removing router_solicitation_delay.
-        // We don't need to restore the default value because the interface is removed in tearDown.
-        // TODO: speed up further by not waiting for RS but keying off first IPv6 packet.
-        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "router_solicitation_delay", "0");
-        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "0");
-    }
-
-    private void assertHasAddressThat(String msg, LinkProperties lp,
-            Predicate<LinkAddress> condition) {
-        for (LinkAddress addr : lp.getLinkAddresses()) {
-            if (condition.test(addr)) {
-                return;
-            }
-        }
-        fail(msg + " not found in: " + lp);
-    }
-
-    private boolean hasFlag(LinkAddress addr, int flag) {
-        return (addr.getFlags() & flag) == flag;
-    }
-
-    private boolean isPrivacyAddress(LinkAddress addr) {
-        return addr.isGlobalPreferred() && hasFlag(addr, IFA_F_TEMPORARY);
-    }
-
-    private boolean isStablePrivacyAddress(LinkAddress addr) {
-        // TODO: move away from getting address updates from netd and make this work on Q as well.
-        final int flag = ShimUtils.isAtLeastR() ? IFA_F_STABLE_PRIVACY : 0;
-        return addr.isGlobalPreferred() && hasFlag(addr, flag);
-    }
-
-    private LinkProperties doIpv6OnlyProvisioning(InOrder inOrder, ByteBuffer ra) throws Exception {
-        waitForRouterSolicitation();
-        mPacketReader.sendResponse(ra);
-
-        // The lambda below needs to write a LinkProperties to a local variable, but lambdas cannot
-        // write to non-final local variables. So declare a final variable to write to.
-        final AtomicReference<LinkProperties> lpRef = new AtomicReference<>();
-
-        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
-        lpRef.set(captor.getValue());
-
-        // Sometimes provisioning completes as soon as the link-local and the stable address appear,
-        // before the privacy address appears. If so, wait here for the LinkProperties update that
-        // contains all three address. Otherwise, future calls to verify() might get confused.
-        if (captor.getValue().getLinkAddresses().size() == 2) {
-            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(lp -> {
-                lpRef.set(lp);
-                return lp.getLinkAddresses().size() == 3;
-            }));
-        }
-
-        LinkProperties lp = lpRef.get();
-        assertEquals("Should have 3 IPv6 addresses after provisioning: " + lp,
-                3, lp.getLinkAddresses().size());
-        assertHasAddressThat("link-local address", lp, x -> x.getAddress().isLinkLocalAddress());
-        assertHasAddressThat("privacy address", lp, this::isPrivacyAddress);
-        assertHasAddressThat("stable privacy address", lp, this::isStablePrivacyAddress);
-
-        return lp;
-    }
-
-    @Test
-    public void testRaRdnss() throws Exception {
-        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .withoutIPv4()
-                .build();
-        mIpc.startProvisioning(config);
-
-        InOrder inOrder = inOrder(mCb);
-        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-
-        final String dnsServer = "2001:4860:4860::64";
-        final String lowlifeDnsServer = "2001:4860:4860::6464";
-
-        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
-        ByteBuffer rdnss1 = buildRdnssOption(60, lowlifeDnsServer);
-        ByteBuffer rdnss2 = buildRdnssOption(600, dnsServer);
-        ByteBuffer ra = buildRaPacket(pio, rdnss1, rdnss2);
-
-        LinkProperties lp = doIpv6OnlyProvisioning(inOrder, ra);
-
-        // Expect that DNS servers with lifetimes below CONFIG_MIN_RDNSS_LIFETIME are not accepted.
-        assertNotNull(lp);
-        assertEquals(1, lp.getDnsServers().size());
-        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
-
-        // If the RDNSS lifetime is above the minimum, the DNS server is accepted.
-        rdnss1 = buildRdnssOption(68, lowlifeDnsServer);
-        ra = buildRaPacket(pio, rdnss1, rdnss2);
-        mPacketReader.sendResponse(ra);
-        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(captor.capture());
-        lp = captor.getValue();
-        assertNotNull(lp);
-        assertEquals(2, lp.getDnsServers().size());
-        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
-        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(lowlifeDnsServer)));
-
-        // Expect that setting RDNSS lifetime of 0 causes loss of provisioning.
-        rdnss1 = buildRdnssOption(0, dnsServer);
-        rdnss2 = buildRdnssOption(0, lowlifeDnsServer);
-        ra = buildRaPacket(pio, rdnss1, rdnss2);
-        mPacketReader.sendResponse(ra);
-
-        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
-        lp = captor.getValue();
-        assertNotNull(lp);
-        assertEquals(0, lp.getDnsServers().size());
-        reset(mCb);
-    }
-
-    private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception {
-        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
-                argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));
-
-    }
-
-    private void expectNoNat64PrefixUpdate(InOrder inOrder, IpPrefix unchanged) throws Exception {
-        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(0)).onLinkPropertiesChange(argThat(
-                lp -> !Objects.equals(unchanged, lp.getNat64Prefix())));
-
-    }
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    public void testPref64Option() throws Exception {
-        assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q);
-
-        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .withoutIPv4()
-                .build();
-        mIpc.startProvisioning(config);
-
-        final String dnsServer = "2001:4860:4860::64";
-        final IpPrefix prefix = new IpPrefix("64:ff9b::/96");
-        final IpPrefix otherPrefix = new IpPrefix("2001:db8:64::/96");
-
-        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
-        ByteBuffer rdnss = buildRdnssOption(600, dnsServer);
-        ByteBuffer pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
-        ByteBuffer ra = buildRaPacket(pio, rdnss, pref64);
-
-        // The NAT64 prefix might be detected before or after provisioning success.
-        // Don't test order between these two events.
-        LinkProperties lp = doIpv6OnlyProvisioning(null /*inOrder*/, ra);
-        expectAlarmSet(null /*inOrder*/, "PREF64", 600);
-
-        // From now on expect events in order.
-        InOrder inOrder = inOrder(mCb, mAlarm);
-        if (lp.getNat64Prefix() != null) {
-            assertEquals(prefix, lp.getNat64Prefix());
-        } else {
-            expectNat64PrefixUpdate(inOrder, prefix);
-        }
-
-        // Increase the lifetime and expect the prefix not to change.
-        pref64 = new StructNdOptPref64(prefix, 1800).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        OnAlarmListener pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1800);
-        expectNoNat64PrefixUpdate(inOrder, prefix);
-        reset(mCb, mAlarm);
-
-        // Reduce the lifetime and expect to reschedule expiry.
-        pref64 = new StructNdOptPref64(prefix, 1500).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1496);
-        expectNoNat64PrefixUpdate(inOrder, prefix);
-        reset(mCb, mAlarm);
-
-        // Withdraw the prefix and expect it to be set to null.
-        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        expectAlarmCancelled(inOrder, pref64Alarm);
-        expectNat64PrefixUpdate(inOrder, null);
-        reset(mCb, mAlarm);
-
-        // Re-announce the prefix.
-        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        expectAlarmSet(inOrder, "PREF64", 600);
-        expectNat64PrefixUpdate(inOrder, prefix);
-        reset(mCb, mAlarm);
-
-        // Announce two prefixes. Don't expect any update because if there is already a NAT64
-        // prefix, any new prefix is ignored.
-        ByteBuffer otherPref64 = new StructNdOptPref64(otherPrefix, 1200).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
-        mPacketReader.sendResponse(ra);
-        expectAlarmSet(inOrder, "PREF64", 600);
-        expectNoNat64PrefixUpdate(inOrder, prefix);
-        reset(mCb, mAlarm);
-
-        // Withdraw the old prefix and continue to announce the new one. Expect a prefix change.
-        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
-        mPacketReader.sendResponse(ra);
-        expectAlarmCancelled(inOrder, pref64Alarm);
-        // Need a different OnAlarmListener local variable because posting it to the handler in the
-        // lambda below requires it to be final.
-        final OnAlarmListener lastAlarm = expectAlarmSet(inOrder, "PREF64", 1200);
-        expectNat64PrefixUpdate(inOrder, otherPrefix);
-        reset(mCb, mAlarm);
-
-        // Simulate prefix expiry.
-        mIpc.getHandler().post(() -> lastAlarm.onAlarm());
-        expectAlarmCancelled(inOrder, pref64Alarm);
-        expectNat64PrefixUpdate(inOrder, null);
-
-        // Announce a non-/96 prefix and expect it to be ignored.
-        IpPrefix invalidPrefix = new IpPrefix("64:ff9b::/64");
-        pref64 = new StructNdOptPref64(invalidPrefix, 1200).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        expectNoNat64PrefixUpdate(inOrder, invalidPrefix);
-
-        // Re-announce the prefix.
-        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
-        ra = buildRaPacket(pio, rdnss, pref64);
-        mPacketReader.sendResponse(ra);
-        final OnAlarmListener clearAlarm = expectAlarmSet(inOrder, "PREF64", 600);
-        expectNat64PrefixUpdate(inOrder, prefix);
-        reset(mCb, mAlarm);
-
-        // Check that the alarm is cancelled when IpClient is stopped.
-        mIpc.stop();
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-        expectAlarmCancelled(inOrder, clearAlarm);
-        expectNat64PrefixUpdate(inOrder, null);
-
-        // Check that even if the alarm was already in the message queue while it was cancelled, it
-        // is safely ignored.
-        mIpc.getHandler().post(() -> clearAlarm.onAlarm());
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-    }
-
-    private void addIpAddressAndWaitForIt(final String iface) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        final String addr1 = "192.0.2.99";
-        final String addr2 = "192.0.2.3";
-        final int prefixLength = 26;
-
-        // Add two IPv4 addresses to the specified interface, and proceed when the NetworkObserver
-        // has seen the second one. This ensures that every other NetworkObserver registered with
-        // mNetworkObserverRegistry - in particular, IpClient's - has seen the addition of the first
-        // address.
-        final LinkAddress trigger = new LinkAddress(addr2 + "/" + prefixLength);
-        NetworkObserver observer = new NetworkObserver() {
-            @Override
-            public void onInterfaceAddressUpdated(LinkAddress address, String ifName) {
-                if (ifName.equals(iface) && address.isSameAddressAs(trigger)) {
-                    latch.countDown();
-                }
-            }
-        };
-
-        mNetworkObserverRegistry.registerObserverForNonblockingCallback(observer);
-        try {
-            mNetd.interfaceAddAddress(iface, addr1, prefixLength);
-            mNetd.interfaceAddAddress(iface, addr2, prefixLength);
-            assertTrue("Trigger IP address " + addr2 + " not seen after " + TEST_TIMEOUT_MS + "ms",
-                    latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        } finally {
-            mNetworkObserverRegistry.unregisterObserver(observer);
-        }
-
-        // Wait for IpClient to process the addition of the address.
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-    }
-
-    private void doIPv4OnlyProvisioningAndExitWithLeftAddress() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-
-        // Stop IpClient and expect a final LinkProperties callback with an empty LP.
-        mIpc.stop();
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
-                x -> x.getAddresses().size() == 0
-                        && x.getRoutes().size() == 0
-                        && x.getDnsServers().size() == 0));
-        reset(mCb);
-
-        // Pretend that something else (e.g., Tethering) used the interface and left an IP address
-        // configured on it. When IpClient starts, it must clear this address before proceeding.
-        // The address must be noticed before startProvisioning is called, or IpClient will
-        // immediately declare provisioning success due to the presence of an IPv4 address.
-        // The address must be IPv4 because IpClient clears IPv6 addresses on startup.
-        //
-        // TODO: once IpClient gets IP addresses directly from netlink instead of from netd, it
-        // may be sufficient to call waitForIdle to see if IpClient has seen the address.
-        addIpAddressAndWaitForIt(mIfaceName);
-    }
-
-    @Test
-    public void testIpClientClearingIpAddressState() throws Exception {
-        doIPv4OnlyProvisioningAndExitWithLeftAddress();
-
-        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .build();
-        mIpc.startProvisioning(config);
-
-        sendBasicRouterAdvertisement(true /*waitForRs*/);
-
-        // Check that the IPv4 addresses configured earlier are not in LinkProperties...
-        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
-        assertFalse(captor.getValue().hasIpv4Address());
-
-        // ... or configured on the interface.
-        InterfaceConfigurationParcel cfg = mNetd.interfaceGetCfg(mIfaceName);
-        assertEquals("0.0.0.0", cfg.ipv4Addr);
-    }
-
-    @Test
-    public void testIpClientClearingIpAddressState_enablePreconnection() throws Exception {
-        doIPv4OnlyProvisioningAndExitWithLeftAddress();
-
-        // Enter ClearingIpAddressesState to clear the remaining IPv4 addresses and transition to
-        // PreconnectionState instead of RunningState.
-        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
-                false /* shouldReplyRapidCommitAck */, true /* isDhcpPreConnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        assertDiscoverPacketOnPreconnectionStart();
-
-        // Force to enter RunningState.
-        mIpc.notifyPreconnectionComplete(false /* abort */);
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_success() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_SuccessWithoutRapidCommit() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_Abort() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_AbortWithoutRapiCommit() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutBeforeAbort() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                true /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutBeforeAbortWithoutRapidCommit()
-            throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                true /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutafterAbort() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutAfterAbortWithoutRapidCommit() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutBeforeSuccess() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                true /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutBeforeSuccessWithoutRapidCommit()
-            throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                true /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutAfterSuccess() throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_TimeoutAfterSuccessWithoutRapidCommit()
-            throws Exception {
-        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
-                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
-                false /* timeoutBeforePreconnectionComplete */);
-    }
-
-    @Test
-    public void testDhcpClientPreconnection_WithoutLayer2InfoWhenStartingProv() throws Exception {
-        // For FILS connection, current bssid (also l2key and cluster) is still null when
-        // starting provisioning since the L2 link hasn't been established yet. Ensure that
-        // IpClient won't crash even if initializing an Layer2Info class with null members.
-        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .withoutIPv6()
-                .withPreconnection()
-                .withLayer2Information(new Layer2Information(null /* l2key */, null /* cluster */,
-                        null /* bssid */));
-
-        mIpc.startProvisioning(prov.build());
-        assertDiscoverPacketOnPreconnectionStart();
-        verify(mCb).setNeighborDiscoveryOffload(true);
-
-        // Force IpClient transition to RunningState from PreconnectionState.
-        mIpc.notifyPreconnectionComplete(false /* success */);
-        HandlerUtilsKt.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
-    }
-
-    @Test
-    public void testDhcpDecline_conflictByArpReply() throws Exception {
-        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
-                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                true /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_conflictByArpProbe() throws Exception {
-        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
-                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_EnableFlagWithoutIpConflict() throws Exception {
-        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
-                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_WithoutIpConflict() throws Exception {
-        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
-                false /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_WithRapidCommitWithoutIpConflict() throws Exception {
-        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
-                true /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_WithRapidCommitConflictByArpReply() throws Exception {
-        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
-                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                true /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_WithRapidCommitConflictByArpProbe() throws Exception {
-        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
-                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testDhcpDecline_EnableFlagWithRapidCommitWithoutIpConflict() throws Exception {
-        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
-                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
-                false /* shouldResponseArpReply */);
-    }
-
-    @Test
-    public void testHostname_enableConfig() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
-                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
-                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                true /* isHostnameConfigurationEnabled */, TEST_HOST_NAME /* hostname */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
-        assertEquals(2, sentPackets.size());
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertHostname(true, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testHostname_disableConfig() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
-                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
-                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                false /* isHostnameConfigurationEnabled */, TEST_HOST_NAME,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
-        assertEquals(2, sentPackets.size());
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertHostname(false, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testHostname_enableConfigWithNullHostname() throws Exception {
-        final long currentTime = System.currentTimeMillis();
-        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
-                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
-                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                true /* isHostnameConfigurationEnabled */, null /* hostname */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
-        assertEquals(2, sentPackets.size());
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertHostname(true, null /* hostname */, null /* hostnameAfterTransliteration */,
-                sentPackets);
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    private void runDhcpClientCaptivePortalApiTest(boolean featureEnabled,
-            boolean serverSendsOption) throws Exception {
-        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
-                false /* shouldReplyRapidCommitAck */, false /* isPreConnectionEnabled */,
-                false /* isDhcpIpConflictDetectEnabled */);
-        final DhcpPacket discover = getNextDhcpPacket();
-        assertTrue(discover instanceof DhcpDiscoverPacket);
-        assertEquals(featureEnabled, discover.hasRequestedParam(DhcpPacket.DHCP_CAPTIVE_PORTAL));
-
-        // Send Offer and handle Request -> Ack
-        final String serverSentUrl = serverSendsOption ? TEST_CAPTIVE_PORTAL_URL : null;
-        mPacketReader.sendResponse(buildDhcpOfferPacket(discover, TEST_LEASE_DURATION_S,
-                (short) TEST_DEFAULT_MTU, serverSentUrl));
-        final int testMtu = 1345;
-        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                false /* shouldReplyRapidCommitAck */, testMtu, serverSentUrl);
-
-        final Uri expectedUrl = featureEnabled && serverSendsOption
-                ? Uri.parse(TEST_CAPTIVE_PORTAL_URL) : null;
-        // Wait for LinkProperties containing DHCP-obtained info, such as MTU
-        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
-                argThat(lp -> lp.getMtu() == testMtu));
-
-        // Ensure that the URL was set as expected in the callbacks.
-        // Can't verify the URL up to Q as there is no such attribute in LinkProperties.
-        if (!ShimUtils.isAtLeastR()) return;
-        verify(mCb).onLinkPropertiesChange(captor.capture());
-        assertTrue(captor.getAllValues().stream().anyMatch(
-                lp -> Objects.equals(expectedUrl, lp.getCaptivePortalApiUrl())));
-    }
-
-    @Test
-    public void testDhcpClientCaptivePortalApiEnabled() throws Exception {
-        // Only run the test on platforms / builds where the API is enabled
-        assumeTrue(CaptivePortalDataShimImpl.isSupported());
-        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, true /* serverSendsOption */);
-    }
-
-    @Test
-    public void testDhcpClientCaptivePortalApiEnabled_NoUrl() throws Exception {
-        // Only run the test on platforms / builds where the API is enabled
-        assumeTrue(CaptivePortalDataShimImpl.isSupported());
-        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, false /* serverSendsOption */);
-    }
-
-    @Test
-    public void testDhcpClientCaptivePortalApiDisabled() throws Exception {
-        // Only run the test on platforms / builds where the API is disabled
-        assumeFalse(CaptivePortalDataShimImpl.isSupported());
-        runDhcpClientCaptivePortalApiTest(false /* featureEnabled */, true /* serverSendsOption */);
-    }
-
-    private ScanResultInfo makeScanResultInfo(final int id, final String ssid,
-            final String bssid, final byte[] oui, final byte type, final byte[] data) {
-        final ByteBuffer payload = ByteBuffer.allocate(4 + data.length);
-        payload.put(oui);
-        payload.put(type);
-        payload.put(data);
-        payload.flip();
-        final ScanResultInfo.InformationElement ie =
-                new ScanResultInfo.InformationElement(id /* IE id */, payload);
-        return new ScanResultInfo(ssid, bssid, Collections.singletonList(ie));
-    }
-
-    private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        return makeScanResultInfo(0xdd, ssid, bssid, TEST_AP_OUI, (byte) 0x06, data);
-    }
-
-    private void doUpstreamHotspotDetectionTest(final int id, final String displayName,
-            final String ssid, final byte[] oui, final byte type, final byte[] data,
-            final boolean expectMetered) throws Exception {
-        final ScanResultInfo info = makeScanResultInfo(id, ssid, TEST_DEFAULT_BSSID, oui, type,
-                data);
-        final long currentTime = System.currentTimeMillis();
-        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
-                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
-                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                false /* isHostnameConfigurationEnabled */, null /* hostname */,
-                null /* captivePortalApiUrl */, displayName, info /* scanResultInfo */);
-        assertEquals(2, sentPackets.size());
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-
-        ArgumentCaptor<DhcpResultsParcelable> captor =
-                ArgumentCaptor.forClass(DhcpResultsParcelable.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
-        DhcpResults lease = fromStableParcelable(captor.getValue());
-        assertNotNull(lease);
-        assertEquals(lease.getIpAddress().getAddress(), CLIENT_ADDR);
-        assertEquals(lease.getGateway(), SERVER_ADDR);
-        assertEquals(1, lease.getDnsServers().size());
-        assertTrue(lease.getDnsServers().contains(SERVER_ADDR));
-        assertEquals(lease.getServerAddress(), SERVER_ADDR);
-        assertEquals(lease.getMtu(), TEST_DEFAULT_MTU);
-
-        if (expectMetered) {
-            assertEquals(lease.vendorInfo, DhcpPacket.VENDOR_INFO_ANDROID_METERED);
-        } else {
-            assertNull(lease.vendorInfo);
-        }
-
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection() throws Exception {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
-                true /* expectMetered */);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection_incorrectIeId() throws Exception {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdc, "\"ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
-                false /* expectMetered */);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection_incorrectOUI() throws Exception {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x1A, (byte) 0x11 }, (byte) 0x06, data,
-                false /* expectMetered */);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection_incorrectSsid() throws Exception {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"another ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
-                false /* expectMetered */);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection_incorrectType() throws Exception {
-        byte[] data = new byte[10];
-        new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x0a, data,
-                false /* expectMetered */);
-    }
-
-    @Test
-    public void testUpstreamHotspotDetection_zeroLengthData() throws Exception {
-        byte[] data = new byte[0];
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
-                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
-                true /* expectMetered */);
-    }
-
-    private void doDhcpRoamingTest(final boolean hasMismatchedIpAddress, final String displayName,
-            final String ssid, final String bssid, final boolean expectRoaming) throws Exception {
-        long currentTime = System.currentTimeMillis();
-        final ScanResultInfo scanResultInfo = makeScanResultInfo(ssid, bssid);
-
-        doAnswer(invocation -> {
-            // we don't rely on the Init-Reboot state to renew previous cached IP lease.
-            // Just return null and force state machine enter INIT state.
-            final String l2Key = invocation.getArgument(0);
-            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
-                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
-            return null;
-        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
-
-        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */,
-                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */,
-                true /* isHostnameConfigurationEnabled */, null /* hostname */,
-                null /* captivePortalApiUrl */, displayName, scanResultInfo);
-        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
-        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
-
-        // simulate the roaming by updating bssid.
-        final Layer2InformationParcelable roamingInfo = new Layer2InformationParcelable();
-        roamingInfo.bssid = MacAddress.fromString(TEST_DHCP_ROAM_BSSID);
-        roamingInfo.l2Key = TEST_DHCP_ROAM_L2KEY;
-        roamingInfo.cluster = TEST_DHCP_ROAM_CLUSTER;
-        mIpc.updateLayer2Information(roamingInfo);
-
-        currentTime = System.currentTimeMillis();
-        reset(mIpMemoryStore);
-        reset(mCb);
-        if (!expectRoaming) {
-            assertIpMemoryNeverStoreNetworkAttributes();
-            return;
-        }
-        // check DHCPREQUEST broadcast sent to renew IP address.
-        DhcpPacket packet;
-        packet = getNextDhcpPacket();
-        assertTrue(packet instanceof DhcpRequestPacket);
-        assertEquals(packet.mClientIp, CLIENT_ADDR);    // client IP
-        assertNull(packet.mRequestedIp);                // requested IP option
-        assertNull(packet.mServerIdentifier);           // server ID
-
-        mPacketReader.sendResponse(buildDhcpAckPacket(packet,
-                hasMismatchedIpAddress ? CLIENT_ADDR_NEW : CLIENT_ADDR, TEST_LEASE_DURATION_S,
-                (short) TEST_DEFAULT_MTU, false /* rapidcommit */, null /* captivePortalUrl */));
-        HandlerUtilsKt.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
-        if (hasMismatchedIpAddress) {
-            // notifyFailure
-            ArgumentCaptor<DhcpResultsParcelable> captor =
-                    ArgumentCaptor.forClass(DhcpResultsParcelable.class);
-            verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
-            DhcpResults lease = fromStableParcelable(captor.getValue());
-            assertNull(lease);
-
-            // roll back to INIT state.
-            packet = getNextDhcpPacket();
-            assertTrue(packet instanceof DhcpDiscoverPacket);
-        } else {
-            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
-                    TEST_DEFAULT_MTU);
-        }
-    }
-
-    @Test
-    public void testDhcpRoaming() throws Exception {
-        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, true /* expectRoaming */);
-    }
-
-    @Test
-    public void testDhcpRoaming_invalidBssid() throws Exception {
-        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DHCP_ROAM_BSSID, false /* expectRoaming */);
-    }
-
-    @Test
-    public void testDhcpRoaming_invalidSsid() throws Exception {
-        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID, false /* expectRoaming */);
-    }
-
-    @Test
-    public void testDhcpRoaming_invalidDisplayName() throws Exception {
-        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"test-ssid\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, false /* expectRoaming */);
-    }
-
-    @Test
-    public void testDhcpRoaming_mismatchedLeasedIpAddress() throws Exception {
-        doDhcpRoamingTest(true /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, true /* expectRoaming */);
-    }
-
-    private void doDualStackProvisioning() throws Exception {
-        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
-
-        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
-                .withoutIpReachabilityMonitor()
-                .build();
-        // Accelerate DHCP handshake to shorten test duration, not strictly necessary.
-        mDependencies.setDhcpRapidCommitEnabled(true);
-        mIpc.startProvisioning(config);
-
-        final InOrder inOrder = inOrder(mCb);
-        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
-        final String dnsServer = "2001:4860:4860::64";
-        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
-        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
-        final ByteBuffer ra = buildRaPacket(pio, rdnss);
-
-        doIpv6OnlyProvisioning(inOrder, ra);
-
-        // Start IPv4 provisioning and wait until entire provisioning completes.
-        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
-                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
-        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(x -> {
-            if (!x.isIpv4Provisioned() || !x.isIpv6Provisioned()) return false;
-            lpFuture.complete(x);
-            return true;
-        }));
-
-        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertNotNull(lp);
-        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
-        assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
-
-        reset(mCb);
-    }
-
-    @Test
-    public void testIgnoreIpv6ProvisioningLoss() throws Exception {
-        doDualStackProvisioning();
-
-        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
-
-        // Send RA with 0-lifetime and wait until all IPv6-related default route and DNS servers
-        // have been removed, then verify if there is IPv4-only info left in the LinkProperties.
-        sendRouterAdvertisementWithZeroLifetime();
-        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
-                argThat(x -> {
-                    final boolean isOnlyIPv4Provisioned = (x.getLinkAddresses().size() == 1
-                            && x.getDnsServers().size() == 1
-                            && x.getAddresses().get(0) instanceof Inet4Address
-                            && x.getDnsServers().get(0) instanceof Inet4Address);
-
-                    if (!isOnlyIPv4Provisioned) return false;
-                    lpFuture.complete(x);
-                    return true;
-                }));
-        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertNotNull(lp);
-        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
-        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
-    }
-
-    @Test
-    public void testDualStackProvisioning() throws Exception {
-        doDualStackProvisioning();
-
-        verify(mCb, never()).onProvisioningFailure(any());
-    }
-}
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
new file mode 100644
index 0000000..b217ebb
--- /dev/null
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 android.net.ip
+
+import android.net.ipmemorystore.NetworkAttributes
+import android.util.ArrayMap
+import java.net.Inet6Address
+import kotlin.test.assertEquals
+import org.mockito.Mockito.any
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+/**
+ * Tests for IpClient, run with signature permissions.
+ */
+class IpClientIntegrationTest : IpClientIntegrationTestCommon() {
+    private val mEnabledFeatures = ArrayMap<String, Boolean>()
+
+    override fun makeIIpClient(ifaceName: String, cb: IIpClientCallbacks): IIpClient {
+        return mIpc.makeConnector()
+    }
+
+    override fun useNetworkStackSignature() = true
+
+    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
+        return mEnabledFeatures.get(name) ?: defaultEnabled
+    }
+
+    override fun setFeatureEnabled(name: String, enabled: Boolean) {
+        mEnabledFeatures.put(name, enabled)
+    }
+
+    override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes {
+        val networkAttributesCaptor = ArgumentCaptor.forClass(NetworkAttributes::class.java)
+
+        verify(mIpMemoryStore, timeout(timeout))
+                .storeNetworkAttributes(eq(l2Key), networkAttributesCaptor.capture(), any())
+        return networkAttributesCaptor.value
+    }
+
+    override fun assertIpMemoryNeverStoreNetworkAttributes(l2Key: String, timeout: Long) {
+        verify(mIpMemoryStore, never()).storeNetworkAttributes(eq(l2Key), any(), any())
+    }
+
+    override fun assertNotifyNeighborLost(targetIp: Inet6Address) {
+        val target = ArgumentCaptor.forClass(Inet6Address::class.java)
+
+        verify(mCallback, timeout(TEST_TIMEOUT_MS)).notifyLost(target.capture(), any())
+        assertEquals(targetIp, target.getValue())
+    }
+
+    override fun assertNeverNotifyNeighborLost() {
+        verify(mCallback, never()).notifyLost(any(), any())
+    }
+}
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
new file mode 100644
index 0000000..2b00fb1
--- /dev/null
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
@@ -0,0 +1,3252 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.dhcp.DhcpClient.EXPIRED_LEASE;
+import static android.net.dhcp.DhcpPacket.DHCP_BOOTREQUEST;
+import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
+import static android.net.dhcp.DhcpPacket.DHCP_IPV6_ONLY_PREFERRED;
+import static android.net.dhcp.DhcpPacket.DHCP_MAGIC_COOKIE;
+import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
+import static android.net.dhcp.DhcpPacket.ENCAP_L2;
+import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
+import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
+import static android.net.dhcp.DhcpPacket.MIN_V6ONLY_WAIT_MS;
+import static android.net.dhcp.DhcpResultsParcelableUtil.fromStableParcelable;
+import static android.net.ip.IpReachabilityMonitor.MIN_NUD_SOLICIT_NUM;
+import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM;
+import static android.net.ipmemorystore.Status.SUCCESS;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IFA_F_TEMPORARY;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
+import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+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_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.longThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.AlarmManager.OnAlarmListener;
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.DhcpResults;
+import android.net.DhcpResultsParcelable;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.Layer2InformationParcelable;
+import android.net.Layer2PacketParcelable;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.NetworkStackIpMemoryStore;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.Uri;
+import android.net.dhcp.DhcpClient;
+import android.net.dhcp.DhcpDeclinePacket;
+import android.net.dhcp.DhcpDiscoverPacket;
+import android.net.dhcp.DhcpPacket;
+import android.net.dhcp.DhcpPacket.ParseException;
+import android.net.dhcp.DhcpRequestPacket;
+import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
+import android.net.ipmemorystore.NetworkAttributes;
+import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
+import android.net.ipmemorystore.Status;
+import android.net.netlink.StructNdOptPref64;
+import android.net.networkstack.TestNetworkStackServiceClient;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
+import android.net.shared.Layer2Information;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
+import android.net.util.InterfaceParams;
+import android.net.util.NetworkStackUtils;
+import android.net.util.SharedLog;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.stats.connectivity.NetworkQuirkEvent;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RdnssOption;
+import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+import com.android.networkstack.arp.ArpPacket;
+import com.android.networkstack.metrics.IpProvisioningMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
+import com.android.networkstack.packets.NeighborAdvertisement;
+import com.android.networkstack.packets.NeighborSolicitation;
+import com.android.server.NetworkObserver;
+import com.android.server.NetworkObserverRegistry;
+import com.android.server.NetworkStackService.NetworkStackServiceManager;
+import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+
+import kotlin.Lazy;
+import kotlin.LazyKt;
+
+/**
+ * Base class for IpClient tests.
+ *
+ * Tests in this class can either be run with signature permissions, or with root access.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public abstract class IpClientIntegrationTestCommon {
+    private static final int DATA_BUFFER_LEN = 4096;
+    private static final int PACKET_TIMEOUT_MS = 5_000;
+    private static final String TEST_CLUSTER = "some cluster";
+    private static final int TEST_LEASE_DURATION_S = 3_600; // 1 hour
+    private static final int TEST_IPV6_ONLY_WAIT_S = 1_800; // 30 min
+    private static final int TEST_LOWER_IPV6_ONLY_WAIT_S = (int) (MIN_V6ONLY_WAIT_MS / 1000 - 1);
+    private static final int TEST_ZERO_IPV6_ONLY_WAIT_S = 0;
+    private static final long TEST_MAX_IPV6_ONLY_WAIT_S = 0xffffffffL;
+    protected static final String TEST_L2KEY = "some l2key";
+
+    // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants.
+    private static final int IFA_F_STABLE_PRIVACY = 0x800;
+
+    protected static final long TEST_TIMEOUT_MS = 2_000L;
+
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+    @Rule
+    public final TestName mTestNameRule = new TestName();
+
+    /**
+     * Indicates that a test requires signature permissions to run.
+     *
+     * Such tests can only be run on devices that use known signing keys, so this annotation must be
+     * avoided as much as possible. Consider whether the test can be written to use shell and root
+     * shell permissions, and run against the NetworkStack AIDL interface (IIpClient) instead.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD})
+    private @interface SignatureRequiredTest {
+        String reason();
+    }
+
+    /**** BEGIN signature required test members ****/
+    // Do not use unless the test *really* cannot be written to exercise IIpClient without mocks.
+    // Tests using the below members must be annotated with @SignatureRequiredTest (otherwise the
+    // members will be null), and can only be run on devices that use known signing keys.
+    // The members could technically be moved to the IpClientIntegrationTest subclass together with
+    // the tests requiring signature permissions, but this would make it harder to follow tests in
+    // multiple classes, and harder to migrate tests between signature required and not required.
+
+    @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
+    @Mock private Resources mResources;
+    @Mock private AlarmManager mAlarm;
+    @Mock private ContentResolver mContentResolver;
+    @Mock private NetworkStackServiceManager mNetworkStackServiceManager;
+    @Mock private IpMemoryStoreService mIpMemoryStoreService;
+    @Mock private PowerManager.WakeLock mTimeoutWakeLock;
+    @Mock protected NetworkStackIpMemoryStore mIpMemoryStore;
+    @Mock private NetworkQuirkMetrics.Dependencies mNetworkQuirkMetricsDeps;
+    @Mock protected IpReachabilityMonitor.Callback mCallback;
+
+    @Spy private INetd mNetd;
+    private NetworkObserverRegistry mNetworkObserverRegistry;
+
+    protected IpClient mIpc;
+    protected Dependencies mDependencies;
+
+    /***** END signature required test members *****/
+
+    protected IIpClientCallbacks mCb;
+    private IIpClient mIIpClient;
+    private String mIfaceName;
+    private HandlerThread mPacketReaderThread;
+    private Handler mHandler;
+    private TapPacketReader mPacketReader;
+    private FileDescriptor mTapFd;
+    private byte[] mClientMac;
+
+    private boolean mIsSignatureRequiredTest;
+
+    // ReadHeads for various packet streams. Cannot be initialized in @Before because ReadHead is
+    // single-thread-only, and AndroidJUnitRunner runs @Before and @Test on different threads.
+    // While it looks like these are created only once per test, they are actually created once per
+    // test method because JUnit recreates a fresh test class instance before every test method.
+    private Lazy<ArrayTrackRecord<byte[]>.ReadHead> mDhcpPacketReadHead =
+            LazyKt.lazy(() -> mPacketReader.getReceivedPackets().newReadHead());
+    private Lazy<ArrayTrackRecord<byte[]>.ReadHead> mArpPacketReadHead =
+            LazyKt.lazy(() -> mPacketReader.getReceivedPackets().newReadHead());
+
+    // Ethernet header
+    private static final int ETH_HEADER_LEN = 14;
+
+    // IP header
+    private static final int IPV4_HEADER_LEN = 20;
+    private static final int IPV4_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 12;
+    private static final int IPV4_DST_ADDR_OFFSET = IPV4_SRC_ADDR_OFFSET + 4;
+
+    // UDP header
+    private static final int UDP_HEADER_LEN = 8;
+    private static final int UDP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN;
+    private static final int UDP_SRC_PORT_OFFSET = UDP_HEADER_OFFSET + 0;
+
+    // DHCP header
+    private static final int DHCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN
+            + UDP_HEADER_LEN;
+    private static final int DHCP_MESSAGE_OP_CODE_OFFSET = DHCP_HEADER_OFFSET + 0;
+    private static final int DHCP_TRANSACTION_ID_OFFSET = DHCP_HEADER_OFFSET + 4;
+    private static final int DHCP_OPTION_MAGIC_COOKIE_OFFSET = DHCP_HEADER_OFFSET + 236;
+
+    private static final Inet4Address SERVER_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.100");
+    private static final Inet4Address CLIENT_ADDR =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.2");
+    private static final Inet4Address CLIENT_ADDR_NEW =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.3");
+    private static final Inet4Address INADDR_ANY =
+            (Inet4Address) InetAddresses.parseNumericAddress("0.0.0.0");
+    private static final int PREFIX_LENGTH = 24;
+    private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH);
+    private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress(
+            SERVER_ADDR, PREFIX_LENGTH);
+    private static final String HOSTNAME = "testhostname";
+    private static final int TEST_DEFAULT_MTU = 1500;
+    private static final int TEST_MIN_MTU = 1280;
+    private static final MacAddress ROUTER_MAC = MacAddress.fromString("00:1A:11:22:33:44");
+    private static final byte[] ROUTER_MAC_BYTES = ROUTER_MAC.toByteArray();
+    private static final Inet6Address ROUTER_LINK_LOCAL =
+                (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
+    private static final String TEST_HOST_NAME = "AOSP on Crosshatch";
+    private static final String TEST_HOST_NAME_TRANSLITERATION = "AOSP-on-Crosshatch";
+    private static final String TEST_CAPTIVE_PORTAL_URL = "https://example.com/capportapi";
+    private static final byte[] TEST_HOTSPOT_OUI = new byte[] {
+            (byte) 0x00, (byte) 0x17, (byte) 0xF2
+    };
+    private static final byte TEST_VENDOR_SPECIFIC_TYPE = 0x06;
+
+    private static final String TEST_DEFAULT_SSID = "test_ssid";
+    private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55";
+    private static final String TEST_DHCP_ROAM_SSID = "0001docomo";
+    private static final String TEST_DHCP_ROAM_BSSID = "00:4e:35:17:98:55";
+    private static final String TEST_DHCP_ROAM_L2KEY = "roaming_l2key";
+    private static final String TEST_DHCP_ROAM_CLUSTER = "roaming_cluster";
+    private static final byte[] TEST_AP_OUI = new byte[] { 0x00, 0x1A, 0x11 };
+    private static final byte[] TEST_OEM_OUI = new byte[] {(byte) 0x00, (byte) 0x17, (byte) 0xc3};
+    private static final String TEST_OEM_VENDOR_ID = "vendor-class-identifier";
+    private static final byte[] TEST_OEM_USER_CLASS_INFO = new byte[] {
+            // Instance of User Class: [0]
+            (byte) 0x03, /* UC_Len_0 */ (byte) 0x11, (byte) 0x22, (byte) 0x33,
+            // Instance of User Class: [1]
+            (byte) 0x03, /* UC_Len_1 */ (byte) 0x44, (byte) 0x55, (byte) 0x66,
+    };
+
+    protected class Dependencies extends IpClient.Dependencies {
+        // Can't use SparseIntArray, it doesn't have an easy way to know if a key is not present.
+        private HashMap<String, Integer> mIntConfigProperties = new HashMap<>();
+        private DhcpClient mDhcpClient;
+        private boolean mIsHostnameConfigurationEnabled;
+        private String mHostname;
+        private boolean mIsInterfaceRecovered;
+
+        public void setHostnameConfiguration(final boolean enable, final String hostname) {
+            mIsHostnameConfigurationEnabled = enable;
+            mHostname = hostname;
+        }
+
+        // Enable this flag to simulate the interface has been added back after removing
+        // on the provisioning start. However, the actual tap interface has been removed,
+        // interface parameters query will get null when attempting to restore Interface
+        // MTU. Create a new InterfaceParams instance and return instead just for interface
+        // toggling test case.
+        public void simulateInterfaceRecover() {
+            mIsInterfaceRecovered = true;
+        }
+
+        @Override
+        public InterfaceParams getInterfaceParams(String ifname) {
+            return mIsInterfaceRecovered
+                    ? new InterfaceParams(ifname, 1 /* index */,
+                            MacAddress.fromString("00:11:22:33:44:55"))
+                    : super.getInterfaceParams(ifname);
+        }
+
+        @Override
+        public INetd getNetd(Context context) {
+            return mNetd;
+        }
+
+        @Override
+        public NetworkStackIpMemoryStore getIpMemoryStore(Context context,
+                NetworkStackServiceManager nssManager) {
+            return mIpMemoryStore;
+        }
+
+        @Override
+        public DhcpClient makeDhcpClient(Context context, StateMachine controller,
+                InterfaceParams ifParams, DhcpClient.Dependencies deps) {
+            mDhcpClient = DhcpClient.makeDhcpClient(context, controller, ifParams, deps);
+            return mDhcpClient;
+        }
+
+        @Override
+        public IpReachabilityMonitor getIpReachabilityMonitor(Context context,
+                InterfaceParams ifParams, Handler h, SharedLog log,
+                IpReachabilityMonitor.Callback callback, boolean usingMultinetworkPolicyTracker,
+                IpReachabilityMonitor.Dependencies deps, final INetd netd) {
+            return new IpReachabilityMonitor(context, ifParams, h, log, mCallback,
+                    usingMultinetworkPolicyTracker, deps, netd);
+        }
+
+        @Override
+        public boolean isFeatureEnabled(final Context context, final String name,
+                final boolean defaultEnabled) {
+            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled);
+        }
+
+        @Override
+        public DhcpClient.Dependencies getDhcpClientDependencies(
+                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
+            return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
+                @Override
+                public boolean isFeatureEnabled(final Context context, final String name,
+                        final boolean defaultEnabled) {
+                    return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled);
+                }
+
+                @Override
+                public int getIntDeviceConfig(final String name, int minimumValue,
+                        int maximumValue, int defaultValue) {
+                    return getDeviceConfigPropertyInt(name, 0 /* default value */);
+                }
+
+                @Override
+                public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) {
+                    return mTimeoutWakeLock;
+                }
+
+                @Override
+                public boolean getSendHostnameOption(final Context context) {
+                    return mIsHostnameConfigurationEnabled;
+                }
+
+                @Override
+                public String getDeviceName(final Context context) {
+                    return mIsHostnameConfigurationEnabled ? mHostname : null;
+                }
+            };
+        }
+
+        @Override
+        public IpReachabilityMonitor.Dependencies getIpReachabilityMonitorDeps(Context context,
+                String name) {
+            return new IpReachabilityMonitor.Dependencies() {
+                public void acquireWakeLock(long durationMs) {
+                    // It doesn't matter for the integration test app on whether the wake lock
+                    // is acquired or not.
+                    return;
+                }
+
+                public IpNeighborMonitor makeIpNeighborMonitor(Handler h, SharedLog log,
+                        NeighborEventConsumer cb) {
+                    return new IpNeighborMonitor(h, log, cb);
+                }
+
+                public boolean isFeatureEnabled(final Context context, final String name,
+                        boolean defaultEnabled) {
+                    return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled);
+                }
+            };
+        }
+
+        @Override
+        public int getDeviceConfigPropertyInt(String name, int defaultValue) {
+            Integer value = mIntConfigProperties.get(name);
+            if (value == null) {
+                throw new IllegalStateException("Non-mocked device config property " + name);
+            }
+            return value;
+        }
+
+        public void setDeviceConfigProperty(String name, int value) {
+            mIntConfigProperties.put(name, value);
+        }
+
+        @Override
+        public NetworkQuirkMetrics getNetworkQuirkMetrics() {
+            return new NetworkQuirkMetrics(mNetworkQuirkMetricsDeps);
+        }
+    }
+
+    @NonNull
+    protected abstract IIpClient makeIIpClient(
+            @NonNull String ifaceName, @NonNull IIpClientCallbacks cb);
+
+    protected abstract void setFeatureEnabled(String name, boolean enabled);
+
+    protected abstract boolean isFeatureEnabled(String name, boolean defaultEnabled);
+
+    protected abstract boolean useNetworkStackSignature();
+
+    protected abstract NetworkAttributes getStoredNetworkAttributes(String l2Key, long timeout);
+
+    protected abstract void assertIpMemoryNeverStoreNetworkAttributes(String l2Key, long timeout);
+
+    protected abstract void assertNotifyNeighborLost(Inet6Address targetIp);
+
+    protected abstract void assertNeverNotifyNeighborLost();
+
+    protected final boolean testSkipped() {
+        // TODO: split out a test suite for root tests, and fail hard instead of skipping the test
+        // if it is run on devices where TestNetworkStackServiceClient is not supported
+        return !useNetworkStackSignature()
+                && (mIsSignatureRequiredTest || !TestNetworkStackServiceClient.isSupported());
+    }
+
+    protected void setDhcpFeatures(final boolean isDhcpLeaseCacheEnabled,
+            final boolean isRapidCommitEnabled, final boolean isDhcpIpConflictDetectEnabled,
+            final boolean isIPv6OnlyPreferredEnabled) {
+        setFeatureEnabled(NetworkStackUtils.DHCP_INIT_REBOOT_VERSION, isDhcpLeaseCacheEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION, isRapidCommitEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION,
+                isDhcpIpConflictDetectEnabled);
+        setFeatureEnabled(NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION,
+                isIPv6OnlyPreferredEnabled);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        final Method testMethod = IpClientIntegrationTestCommon.class.getMethod(
+                mTestNameRule.getMethodName());
+        mIsSignatureRequiredTest = testMethod.getAnnotation(SignatureRequiredTest.class) != null;
+        assumeFalse(testSkipped());
+
+        setUpTapInterface();
+        mCb = mock(IIpClientCallbacks.class);
+
+        if (useNetworkStackSignature()) {
+            setUpMocks();
+            setUpIpClient();
+        }
+
+        mIIpClient = makeIIpClient(mIfaceName, mCb);
+    }
+
+    protected void setUpMocks() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mDependencies = new Dependencies();
+        when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
+        when(mContext.getSystemService(eq(ConnectivityManager.class))).thenReturn(mCm);
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        when(mNetworkStackServiceManager.getIpMemoryStoreService())
+                .thenReturn(mIpMemoryStoreService);
+
+        mDependencies.setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67);
+        mDependencies.setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10);
+        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10);
+        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MIN_MS, 10);
+        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MAX_MS, 20);
+        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_ANNOUNCE_DELAY_MS, 10);
+        mDependencies.setDeviceConfigProperty(DhcpClient.ARP_ANNOUNCE_INTERVAL_MS, 10);
+    }
+
+    private void awaitIpClientShutdown() throws Exception {
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onQuit();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (testSkipped()) return;
+        if (mPacketReader != null) {
+            mHandler.post(() -> mPacketReader.stop()); // Also closes the socket
+            mTapFd = null;
+        }
+        if (mPacketReaderThread != null) {
+            mPacketReaderThread.quitSafely();
+        }
+        mIIpClient.shutdown();
+        awaitIpClientShutdown();
+    }
+
+    private void setUpTapInterface() throws Exception {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        // Adopt the shell permission identity to create a test TAP interface.
+        inst.getUiAutomation().adoptShellPermissionIdentity();
+
+        final TestNetworkInterface iface;
+        try {
+            final TestNetworkManager tnm = (TestNetworkManager)
+                    inst.getContext().getSystemService(Context.TEST_NETWORK_SERVICE);
+            iface = tnm.createTapInterface();
+        } finally {
+            // Drop the identity in order to regain the network stack permissions, which the shell
+            // does not have.
+            inst.getUiAutomation().dropShellPermissionIdentity();
+        }
+        mIfaceName = iface.getInterfaceName();
+        mClientMac = getIfaceMacAddr(mIfaceName).toByteArray();
+        mPacketReaderThread = new HandlerThread(
+                IpClientIntegrationTestCommon.class.getSimpleName());
+        mPacketReaderThread.start();
+        mHandler = mPacketReaderThread.getThreadHandler();
+
+        // Detach the FileDescriptor from the ParcelFileDescriptor.
+        // Otherwise, the garbage collector might call the ParcelFileDescriptor's finalizer, which
+        // closes the FileDescriptor and destroys our tap interface. An alternative would be to
+        // make the ParcelFileDescriptor or the TestNetworkInterface a class member so they never
+        // go out of scope.
+        mTapFd = new FileDescriptor();
+        mTapFd.setInt$(iface.getFileDescriptor().detachFd());
+        mPacketReader = new TapPacketReader(mHandler, mTapFd, DATA_BUFFER_LEN);
+        mHandler.post(() -> mPacketReader.start());
+    }
+
+    private MacAddress getIfaceMacAddr(String ifaceName) throws IOException {
+        // InterfaceParams.getByName requires CAP_NET_ADMIN: read the mac address with the shell
+        final String strMacAddr = getOneLineCommandOutput(
+                "su root cat /sys/class/net/" + ifaceName + "/address");
+        return MacAddress.fromString(strMacAddr);
+    }
+
+    private String getOneLineCommandOutput(String cmd) throws IOException {
+        try (ParcelFileDescriptor fd = InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().executeShellCommand(cmd);
+             BufferedReader reader = new BufferedReader(new FileReader(fd.getFileDescriptor()))) {
+            return reader.readLine();
+        }
+    }
+
+    private IpClient makeIpClient() throws Exception {
+        IpClient ipc = new IpClient(mContext, mIfaceName, mCb, mNetworkObserverRegistry,
+                mNetworkStackServiceManager, mDependencies);
+        // Wait for IpClient to enter its initial state. Otherwise, additional setup steps or tests
+        // that mock IpClient's dependencies might interact with those mocks while IpClient is
+        // starting. This would cause UnfinishedStubbingExceptions as mocks cannot be interacted
+        // with while they are being stubbed.
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        return ipc;
+    }
+
+    private void setUpIpClient() throws Exception {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        final IBinder netdIBinder =
+                (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+        mNetd = spy(INetd.Stub.asInterface(netdIBinder));
+        when(mContext.getSystemService(eq(Context.NETD_SERVICE))).thenReturn(netdIBinder);
+        assertNotNull(mNetd);
+
+        mNetworkObserverRegistry = new NetworkObserverRegistry();
+        mNetworkObserverRegistry.register(mNetd);
+        mIpc = makeIpClient();
+
+        // Tell the IpMemoryStore immediately to answer any question about network attributes with a
+        // null response. Otherwise, the DHCP client will wait for two seconds before starting,
+        // while its query to the IpMemoryStore times out.
+        // This does not affect any test that makes the mock memory store return results, because
+        // unlike when(), it is documented that doAnswer() can be called more than once, to change
+        // the behaviour of a mock in the middle of a test.
+        doAnswer(invocation -> {
+            final String l2Key = invocation.getArgument(0);
+            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
+                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
+            return null;
+        }).when(mIpMemoryStore).retrieveNetworkAttributes(any(), any());
+
+        disableIpv6ProvisioningDelays();
+    }
+
+    private <T> T verifyWithTimeout(InOrder inOrder, T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t, timeout(TEST_TIMEOUT_MS));
+        } else {
+            return verify(t, timeout(TEST_TIMEOUT_MS));
+        }
+    }
+
+    private void expectAlarmCancelled(InOrder inOrder, OnAlarmListener listener) {
+        inOrder.verify(mAlarm, timeout(TEST_TIMEOUT_MS)).cancel(eq(listener));
+    }
+
+    private OnAlarmListener expectAlarmSet(InOrder inOrder, String tagMatch, long afterSeconds,
+            Handler handler) {
+        // Allow +/- 3 seconds to prevent flaky tests.
+        final long when = SystemClock.elapsedRealtime() + afterSeconds * 1000;
+        final long min = when - 3 * 1000;
+        final long max = when + 3 * 1000;
+        ArgumentCaptor<OnAlarmListener> captor = ArgumentCaptor.forClass(OnAlarmListener.class);
+        verifyWithTimeout(inOrder, mAlarm).setExact(
+                anyInt(), longThat(x -> x >= min && x <= max),
+                contains(tagMatch), captor.capture(), eq(handler));
+        return captor.getValue();
+    }
+
+    private OnAlarmListener expectAlarmSet(InOrder inOrder, String tagMatch, int afterSeconds) {
+        return expectAlarmSet(inOrder, tagMatch, (long) afterSeconds, mIpc.getHandler());
+    }
+
+    private boolean packetContainsExpectedField(final byte[] packet, final int offset,
+            final byte[] expected) {
+        if (packet.length < offset + expected.length) return false;
+        for (int i = 0; i < expected.length; ++i) {
+            if (packet[offset + i] != expected[i]) return false;
+        }
+        return true;
+    }
+
+    private boolean isDhcpPacket(final byte[] packet) {
+        final ByteBuffer buffer = ByteBuffer.wrap(packet);
+
+        // check the packet length
+        if (packet.length < DHCP_HEADER_OFFSET) return false;
+
+        // check the source port and dest port in UDP header
+        buffer.position(UDP_SRC_PORT_OFFSET);
+        final short udpSrcPort = buffer.getShort();
+        final short udpDstPort = buffer.getShort();
+        if (udpSrcPort != DHCP_CLIENT || udpDstPort != DHCP_SERVER) return false;
+
+        // check DHCP message type
+        buffer.position(DHCP_MESSAGE_OP_CODE_OFFSET);
+        final byte dhcpOpCode = buffer.get();
+        if (dhcpOpCode != DHCP_BOOTREQUEST) return false;
+
+        // check DHCP magic cookie
+        buffer.position(DHCP_OPTION_MAGIC_COOKIE_OFFSET);
+        final int dhcpMagicCookie = buffer.getInt();
+        if (dhcpMagicCookie != DHCP_MAGIC_COOKIE) return false;
+
+        return true;
+    }
+
+    private ArpPacket parseArpPacketOrNull(final byte[] packet) {
+        try {
+            return ArpPacket.parseArpPacket(packet, packet.length);
+        } catch (ArpPacket.ParseException e) {
+            return null;
+        }
+    }
+
+    private NeighborAdvertisement parseNeighborAdvertisementOrNull(final byte[] packet) {
+        try {
+            return NeighborAdvertisement.parse(packet, packet.length);
+        } catch (NeighborAdvertisement.ParseException e) {
+            return null;
+        }
+    }
+
+    private NeighborSolicitation parseNeighborSolicitationOrNull(final byte[] packet) {
+        try {
+            return NeighborSolicitation.parse(packet, packet.length);
+        } catch (NeighborSolicitation.ParseException e) {
+            return null;
+        }
+    }
+
+    private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
+            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
+            final String captivePortalUrl, final Integer ipv6OnlyWaitTime) {
+        return DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
+                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
+                clientAddress /* yourIp */, packet.getClientMac(), leaseTimeSec,
+                NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
+                Collections.singletonList(SERVER_ADDR) /* gateways */,
+                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
+                SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, HOSTNAME,
+                false /* metered */, mtu, captivePortalUrl, ipv6OnlyWaitTime);
+    }
+
+    private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
+            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
+            final String captivePortalUrl) {
+        return buildDhcpOfferPacket(packet, clientAddress, leaseTimeSec, mtu, captivePortalUrl,
+                null /* ipv6OnlyWaitTime */);
+    }
+
+    private static ByteBuffer buildDhcpAckPacket(final DhcpPacket packet,
+            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
+            final boolean rapidCommit, final String captivePortalApiUrl,
+            final Integer ipv6OnlyWaitTime) {
+        return DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
+                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
+                clientAddress /* yourIp */, CLIENT_ADDR /* requestIp */, packet.getClientMac(),
+                leaseTimeSec, NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
+                Collections.singletonList(SERVER_ADDR) /* gateways */,
+                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
+                SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, HOSTNAME,
+                false /* metered */, mtu, rapidCommit, captivePortalApiUrl, ipv6OnlyWaitTime);
+    }
+
+    private static ByteBuffer buildDhcpAckPacket(final DhcpPacket packet,
+            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
+            final boolean rapidCommit, final String captivePortalApiUrl) {
+        return buildDhcpAckPacket(packet, clientAddress, leaseTimeSec, mtu, rapidCommit,
+                captivePortalApiUrl, null /* ipv6OnlyWaitTime */);
+    }
+
+    private static ByteBuffer buildDhcpNakPacket(final DhcpPacket packet) {
+        return DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
+            SERVER_ADDR /* serverIp */, INADDR_ANY /* relayIp */, packet.getClientMac(),
+            false /* broadcast */, "duplicated request IP address");
+    }
+
+    private void sendArpReply(final byte[] clientMac) throws IOException {
+        final ByteBuffer packet = ArpPacket.buildArpPacket(clientMac /* dst */,
+                ROUTER_MAC_BYTES /* srcMac */, INADDR_ANY.getAddress() /* target IP */,
+                clientMac /* target HW address */, CLIENT_ADDR.getAddress() /* sender IP */,
+                (short) ARP_REPLY);
+        mPacketReader.sendResponse(packet);
+    }
+
+    private void sendArpProbe() throws IOException {
+        final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST /* dst */,
+                ROUTER_MAC_BYTES /* srcMac */, CLIENT_ADDR.getAddress() /* target IP */,
+                new byte[ETHER_ADDR_LEN] /* target HW address */,
+                INADDR_ANY.getAddress() /* sender IP */, (short) ARP_REQUEST);
+        mPacketReader.sendResponse(packet);
+    }
+
+    private void startIpClientProvisioning(final ProvisioningConfiguration cfg) throws Exception {
+        mIIpClient.startProvisioning(cfg.toStableParcelable());
+    }
+
+    private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
+            final boolean shouldReplyRapidCommitAck, final boolean isPreconnectionEnabled,
+            final boolean isDhcpIpConflictDetectEnabled, final boolean isIPv6OnlyPreferredEnabled,
+            final String displayName, final ScanResultInfo scanResultInfo,
+            final Layer2Information layer2Info) throws Exception {
+        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withLayer2Information(layer2Info == null
+                        ? new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                              MacAddress.fromString(TEST_DEFAULT_BSSID))
+                        : layer2Info)
+                .withoutIPv6();
+        if (isPreconnectionEnabled) prov.withPreconnection();
+        if (displayName != null) prov.withDisplayName(displayName);
+        if (scanResultInfo != null) prov.withScanResultInfo(scanResultInfo);
+
+        setDhcpFeatures(isDhcpLeaseCacheEnabled, shouldReplyRapidCommitAck,
+                isDhcpIpConflictDetectEnabled, isIPv6OnlyPreferredEnabled);
+
+        startIpClientProvisioning(prov.build());
+        if (!isPreconnectionEnabled) {
+            verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        }
+        verify(mCb, never()).onProvisioningFailure(any());
+    }
+
+    private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
+            final boolean isDhcpRapidCommitEnabled, final boolean isPreconnectionEnabled,
+            final boolean isDhcpIpConflictDetectEnabled, final boolean isIPv6OnlyPreferredEnabled)
+            throws Exception {
+        startIpClientProvisioning(isDhcpLeaseCacheEnabled, isDhcpRapidCommitEnabled,
+                isPreconnectionEnabled, isDhcpIpConflictDetectEnabled, isIPv6OnlyPreferredEnabled,
+                null /* displayName */, null /* ScanResultInfo */, null /* layer2Info */);
+    }
+
+    private void assertIpMemoryStoreNetworkAttributes(final Integer leaseTimeSec,
+            final long startTime, final int mtu) {
+        final NetworkAttributes na = getStoredNetworkAttributes(TEST_L2KEY, TEST_TIMEOUT_MS);
+        assertNotNull(na);
+        assertEquals(CLIENT_ADDR, na.assignedV4Address);
+        if (leaseTimeSec == null || leaseTimeSec.intValue() == DhcpPacket.INFINITE_LEASE) {
+            assertEquals(Long.MAX_VALUE, na.assignedV4AddressExpiry.longValue());
+        } else {
+            // check the lease expiry's scope
+            final long upperBound = startTime + 7_200_000; // start timestamp + 2h
+            final long lowerBound = startTime + 3_600_000; // start timestamp + 1h
+            final long expiry = na.assignedV4AddressExpiry;
+            assertTrue(upperBound > expiry);
+            assertTrue(lowerBound < expiry);
+        }
+        assertEquals(Collections.singletonList(SERVER_ADDR), na.dnsAddresses);
+        assertEquals(new Integer(mtu), na.mtu);
+    }
+
+    private void assertIpMemoryNeverStoreNetworkAttributes() {
+        assertIpMemoryNeverStoreNetworkAttributes(TEST_L2KEY, TEST_TIMEOUT_MS);
+    }
+
+    private void assertHostname(final boolean isHostnameConfigurationEnabled,
+            final String hostname, final String hostnameAfterTransliteration,
+            final List<DhcpPacket> packetList) throws Exception {
+        for (DhcpPacket packet : packetList) {
+            if (!isHostnameConfigurationEnabled || hostname == null) {
+                assertNoHostname(packet.getHostname());
+            } else {
+                assertEquals(packet.getHostname(), hostnameAfterTransliteration);
+            }
+        }
+    }
+
+    private void assertNoHostname(String hostname) {
+        if (ShimUtils.isAtLeastR()) {
+            assertNull(hostname);
+        } else {
+            // Until Q, if no hostname is set, the device falls back to the hostname set via
+            // system property, to avoid breaking Q devices already launched with that setup.
+            assertEquals(SystemProperties.get("net.hostname"), hostname);
+        }
+    }
+
+    // Helper method to complete DHCP 2-way or 4-way handshake
+    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
+            final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
+            final boolean shouldReplyRapidCommitAck, final int mtu,
+            final boolean isDhcpIpConflictDetectEnabled,
+            final boolean isIPv6OnlyPreferredEnabled,
+            final String captivePortalApiUrl, final String displayName,
+            final ScanResultInfo scanResultInfo, final Layer2Information layer2Info)
+            throws Exception {
+        startIpClientProvisioning(isDhcpLeaseCacheEnabled, shouldReplyRapidCommitAck,
+                false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled,
+                isIPv6OnlyPreferredEnabled, displayName, scanResultInfo, layer2Info);
+        return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck, mtu,
+                captivePortalApiUrl);
+    }
+
+    private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
+            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
+            final String captivePortalApiUrl) throws Exception {
+        final List<DhcpPacket> packetList = new ArrayList<>();
+        DhcpPacket packet;
+        while ((packet = getNextDhcpPacket()) != null) {
+            packetList.add(packet);
+            if (packet instanceof DhcpDiscoverPacket) {
+                if (shouldReplyRapidCommitAck) {
+                    mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec,
+                              (short) mtu, true /* rapidCommit */, captivePortalApiUrl));
+                } else {
+                    mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR,
+                            leaseTimeSec, (short) mtu, captivePortalApiUrl));
+                }
+            } else if (packet instanceof DhcpRequestPacket) {
+                final ByteBuffer byteBuffer = isSuccessLease
+                        ? buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec, (short) mtu,
+                                false /* rapidCommit */, captivePortalApiUrl)
+                        : buildDhcpNakPacket(packet);
+                mPacketReader.sendResponse(byteBuffer);
+            } else {
+                fail("invalid DHCP packet");
+            }
+
+            // wait for reply to DHCPOFFER packet if disabling rapid commit option
+            if (shouldReplyRapidCommitAck || !(packet instanceof DhcpDiscoverPacket)) {
+                return packetList;
+            }
+        }
+        fail("No DHCPREQUEST received on interface");
+        return packetList;
+    }
+
+    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
+            final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
+            final boolean isDhcpRapidCommitEnabled, final int mtu,
+            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
+        return performDhcpHandshake(isSuccessLease, leaseTimeSec, isDhcpLeaseCacheEnabled,
+                isDhcpRapidCommitEnabled, mtu, isDhcpIpConflictDetectEnabled,
+                false /* isIPv6OnlyPreferredEnabled */,
+                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */,
+                null /* layer2Info */);
+    }
+
+    private List<DhcpPacket> performDhcpHandshake() throws Exception {
+        return performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                false /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+    }
+
+    private DhcpPacket getNextDhcpPacket(final long timeout) throws Exception {
+        byte[] packet;
+        while ((packet = mDhcpPacketReadHead.getValue()
+                .poll(timeout, this::isDhcpPacket)) != null) {
+            final DhcpPacket dhcpPacket = DhcpPacket.decodeFullPacket(packet, packet.length,
+                    ENCAP_L2);
+            if (dhcpPacket != null) return dhcpPacket;
+        }
+        return null;
+    }
+
+    private DhcpPacket getNextDhcpPacket() throws Exception {
+        final DhcpPacket packet = getNextDhcpPacket(PACKET_TIMEOUT_MS);
+        assertNotNull("No expected DHCP packet received on interface within timeout", packet);
+        return packet;
+    }
+
+    private DhcpPacket getReplyFromDhcpLease(final NetworkAttributes na, boolean timeout)
+            throws Exception {
+        doAnswer(invocation -> {
+            if (timeout) return null;
+            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
+                    .onNetworkAttributesRetrieved(new Status(SUCCESS), TEST_L2KEY, na);
+            return null;
+        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
+        startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */,
+                false /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        return getNextDhcpPacket();
+    }
+
+    private void removeTapInterface(final FileDescriptor fd) {
+        try {
+            Os.close(fd);
+        } catch (ErrnoException e) {
+            fail("Fail to close file descriptor: " + e);
+        }
+    }
+
+    private void verifyAfterIpClientShutdown() throws RemoteException {
+        final LinkProperties emptyLp = new LinkProperties();
+        emptyLp.setInterfaceName(mIfaceName);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(emptyLp);
+    }
+
+    // Verify IPv4-only provisioning success. No need to verify IPv4 provisioning when below cases
+    // happen:
+    // 1. if there's a failure lease, onProvisioningSuccess() won't be called;
+    // 2. if duplicated IPv4 address detection is enabled, verify TIMEOUT will affect ARP packets
+    //    capture running in other test cases.
+    // 3. if IPv6 is enabled, e.g. withoutIPv6() isn't called when starting provisioning.
+    private void verifyIPv4OnlyProvisioningSuccess(final Collection<InetAddress> addresses)
+            throws Exception {
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        assertNotEquals(0, lp.getDnsServers().size());
+        assertEquals(addresses.size(), lp.getAddresses().size());
+        assertTrue(lp.getAddresses().containsAll(addresses));
+    }
+
+    private void doRestoreInitialMtuTest(final boolean shouldChangeMtu,
+            final boolean shouldRemoveTapInterface) throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        int mtu = TEST_DEFAULT_MTU;
+
+        if (shouldChangeMtu) mtu = TEST_MIN_MTU;
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                mtu, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, mtu);
+
+        if (shouldChangeMtu) {
+            // Pretend that ConnectivityService set the MTU.
+            mNetd.interfaceSetMtu(mIfaceName, mtu);
+            assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), mtu);
+        }
+
+        // Sometimes, IpClient receives an update with an empty LinkProperties during startup,
+        // when the link-local address is deleted after interface bringup. Reset expectations
+        // here to ensure that verifyAfterIpClientShutdown does not fail because it sees two
+        // empty LinkProperties changes instead of one.
+        reset(mCb);
+
+        if (shouldRemoveTapInterface) removeTapInterface(mTapFd);
+        try {
+            mIpc.shutdown();
+            awaitIpClientShutdown();
+            if (shouldRemoveTapInterface) {
+                verify(mNetd, never()).interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
+            } else {
+                // Verify that MTU indeed has been restored or not.
+                verify(mNetd, times(shouldChangeMtu ? 1 : 0))
+                        .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
+            }
+            verifyAfterIpClientShutdown();
+        } catch (Exception e) {
+            fail("Exception should not have been thrown after shutdown: " + e);
+        }
+    }
+
+    private DhcpPacket assertDiscoverPacketOnPreconnectionStart() throws Exception {
+        final ArgumentCaptor<List<Layer2PacketParcelable>> l2PacketList =
+                ArgumentCaptor.forClass(List.class);
+
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onPreconnectionStart(l2PacketList.capture());
+        final byte[] payload = l2PacketList.getValue().get(0).payload;
+        DhcpPacket packet = DhcpPacket.decodeFullPacket(payload, payload.length, ENCAP_L2);
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertArrayEquals(INADDR_BROADCAST.getAddress(),
+                Arrays.copyOfRange(payload, IPV4_DST_ADDR_OFFSET, IPV4_DST_ADDR_OFFSET + 4));
+        return packet;
+    }
+
+    private void doIpClientProvisioningWithPreconnectionTest(
+            final boolean shouldReplyRapidCommitAck, final boolean shouldAbortPreconnection,
+            final boolean shouldFirePreconnectionTimeout,
+            final boolean timeoutBeforePreconnectionComplete) throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        final ArgumentCaptor<InterfaceConfigurationParcel> ifConfig =
+                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
+
+        startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */,
+                shouldReplyRapidCommitAck, true /* isDhcpPreConnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        DhcpPacket packet = assertDiscoverPacketOnPreconnectionStart();
+        final int preconnDiscoverTransId = packet.getTransactionId();
+
+        if (shouldAbortPreconnection) {
+            if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
+                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
+            }
+
+            mIpc.notifyPreconnectionComplete(false /* abort */);
+            HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+
+            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
+                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
+            }
+
+            // Either way should get DhcpClient go back to INIT state, and broadcast
+            // DISCOVER with new transaction ID.
+            packet = getNextDhcpPacket();
+            assertTrue(packet instanceof DhcpDiscoverPacket);
+            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
+        } else if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
+            // If timeout fires before success preconnection, DhcpClient will go back to INIT state,
+            // and broadcast DISCOVER with new transaction ID.
+            mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
+            packet = getNextDhcpPacket();
+            assertTrue(packet instanceof DhcpDiscoverPacket);
+            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
+            // any old response would be ignored due to mismatched transaction ID.
+        }
+
+        final short mtu = (short) TEST_DEFAULT_MTU;
+        if (!shouldReplyRapidCommitAck) {
+            mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR,
+                    TEST_LEASE_DURATION_S, mtu, null /* captivePortalUrl */));
+            packet = getNextDhcpPacket();
+            assertTrue(packet instanceof DhcpRequestPacket);
+        }
+        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, TEST_LEASE_DURATION_S,
+                mtu, shouldReplyRapidCommitAck, null /* captivePortalUrl */));
+
+        if (!shouldAbortPreconnection) {
+            mIpc.notifyPreconnectionComplete(true /* success */);
+            HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
+
+            // If timeout fires after successful preconnection, right now DhcpClient will have
+            // already entered BOUND state, the delayed CMD_TIMEOUT command would be ignored. So
+            // this case should be very rare, because the timeout alarm is cancelled when state
+            // machine exits from Preconnecting state.
+            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
+                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
+            }
+        }
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+
+        final LinkAddress ipAddress = new LinkAddress(CLIENT_ADDR, PREFIX_LENGTH);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetCfg(ifConfig.capture());
+        assertEquals(ifConfig.getValue().ifName, mIfaceName);
+        assertEquals(ifConfig.getValue().ipv4Addr, ipAddress.getAddress().getHostAddress());
+        assertEquals(ifConfig.getValue().prefixLength, PREFIX_LENGTH);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    private ArpPacket getNextArpPacket(final long timeout) throws Exception {
+        byte[] packet;
+        while ((packet = mArpPacketReadHead.getValue().poll(timeout, p -> true)) != null) {
+            final ArpPacket arpPacket = parseArpPacketOrNull(packet);
+            if (arpPacket != null) return arpPacket;
+        }
+        return null;
+    }
+
+    private ArpPacket getNextArpPacket() throws Exception {
+        final ArpPacket packet = getNextArpPacket(PACKET_TIMEOUT_MS);
+        assertNotNull("No expected ARP packet received on interface within timeout", packet);
+        return packet;
+    }
+
+    private void assertArpPacket(final ArpPacket packet) {
+        assertEquals(packet.opCode, ARP_REQUEST);
+        assertEquals(packet.targetIp, CLIENT_ADDR);
+        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
+    }
+
+    private void assertArpProbe(final ArpPacket packet) {
+        assertArpPacket(packet);
+        assertEquals(packet.senderIp, INADDR_ANY);
+    }
+
+    private void assertArpAnnounce(final ArpPacket packet) {
+        assertArpPacket(packet);
+        assertEquals(packet.senderIp, CLIENT_ADDR);
+    }
+
+    private void assertGratuitousARP(final ArpPacket packet) {
+        assertEquals(packet.opCode, ARP_REPLY);
+        assertEquals(packet.senderIp, CLIENT_ADDR);
+        assertEquals(packet.targetIp, CLIENT_ADDR);
+        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
+        assertTrue(Arrays.equals(packet.targetHwAddress.toByteArray(), ETHER_BROADCAST));
+    }
+
+    private void doIpAddressConflictDetectionTest(final boolean causeIpAddressConflict,
+            final boolean shouldReplyRapidCommitAck, final boolean isDhcpIpConflictDetectEnabled,
+            final boolean shouldResponseArpReply) throws Exception {
+        final long currentTime = System.currentTimeMillis();
+
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, shouldReplyRapidCommitAck,
+                TEST_DEFAULT_MTU, isDhcpIpConflictDetectEnabled);
+
+        // If we receive an ARP packet here, it's guaranteed to be from IP conflict detection,
+        // because at this time the test interface does not have an IP address and therefore
+        // won't send ARP for anything.
+        if (causeIpAddressConflict) {
+            final ArpPacket arpProbe = getNextArpPacket();
+            assertArpProbe(arpProbe);
+
+            if (shouldResponseArpReply) {
+                sendArpReply(mClientMac);
+            } else {
+                sendArpProbe();
+            }
+            final DhcpPacket packet = getNextDhcpPacket();
+            assertTrue(packet instanceof DhcpDeclinePacket);
+            assertEquals(packet.mServerIdentifier, SERVER_ADDR);
+            assertEquals(packet.mRequestedIp, CLIENT_ADDR);
+
+            verify(mCb, never()).onProvisioningFailure(any());
+            assertIpMemoryNeverStoreNetworkAttributes();
+        } else if (isDhcpIpConflictDetectEnabled) {
+            int arpPacketCount = 0;
+            final List<ArpPacket> packetList = new ArrayList<ArpPacket>();
+            // Total sent ARP packets should be 5 (3 ARP Probes + 2 ARP Announcements)
+            ArpPacket packet;
+            while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
+                packetList.add(packet);
+            }
+            assertEquals(5, packetList.size());
+            assertArpProbe(packetList.get(0));
+            assertArpAnnounce(packetList.get(3));
+        } else {
+            verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
+                    TEST_DEFAULT_MTU);
+        }
+    }
+
+    @Test @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN")
+    public void testInterfaceParams() throws Exception {
+        InterfaceParams params = InterfaceParams.getByName(mIfaceName);
+        assertNotNull(params);
+        assertEquals(mIfaceName, params.name);
+        assertTrue(params.index > 0);
+        assertNotNull(params.macAddr);
+        assertTrue(params.hasMacAddress);
+
+        //  Check interface "lo".
+        params = InterfaceParams.getByName("lo");
+        assertNotNull(params);
+        assertEquals("lo", params.name);
+        assertTrue(params.index > 0);
+        assertNotNull(params.macAddr);
+        assertFalse(params.hasMacAddress);
+    }
+
+    @Test
+    public void testDhcpInit() throws Exception {
+        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
+                false /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test
+    public void testHandleSuccessDhcpLease() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test
+    public void testHandleFailureDhcpLease() throws Exception {
+        performDhcpHandshake(false /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+
+        verify(mCb, never()).onProvisioningSuccess(any());
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test
+    public void testHandleInfiniteLease() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        performDhcpHandshake(true /* isSuccessLease */, INFINITE_LEASE,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(INFINITE_LEASE, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test
+    public void testHandleNoLease() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        performDhcpHandshake(true /* isSuccessLease */, null /* no lease time */,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(null, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.Q) // INIT-REBOOT is enabled on R.
+    public void testHandleDisableInitRebootState() throws Exception {
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                false /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test
+    public void testHandleRapidCommitOption() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, true /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientStartWithCachedInfiniteLease() throws Exception {
+        final DhcpPacket packet = getReplyFromDhcpLease(
+                new NetworkAttributes.Builder()
+                    .setAssignedV4Address(CLIENT_ADDR)
+                    .setAssignedV4AddressExpiry(Long.MAX_VALUE) // lease is always valid
+                    .setMtu(new Integer(TEST_DEFAULT_MTU))
+                    .setCluster(TEST_CLUSTER)
+                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
+                    .build(), false /* timeout */);
+        assertTrue(packet instanceof DhcpRequestPacket);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientStartWithCachedExpiredLease() throws Exception {
+        final DhcpPacket packet = getReplyFromDhcpLease(
+                 new NetworkAttributes.Builder()
+                    .setAssignedV4Address(CLIENT_ADDR)
+                    .setAssignedV4AddressExpiry(EXPIRED_LEASE)
+                    .setMtu(new Integer(TEST_DEFAULT_MTU))
+                    .setCluster(TEST_CLUSTER)
+                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
+                    .build(), false /* timeout */);
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientStartWithNullRetrieveNetworkAttributes() throws Exception {
+        final DhcpPacket packet = getReplyFromDhcpLease(null /* na */, false /* timeout */);
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientStartWithTimeoutRetrieveNetworkAttributes() throws Exception {
+        final DhcpPacket packet = getReplyFromDhcpLease(
+                new NetworkAttributes.Builder()
+                    .setAssignedV4Address(CLIENT_ADDR)
+                    .setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000)
+                    .setMtu(new Integer(TEST_DEFAULT_MTU))
+                    .setCluster(TEST_CLUSTER)
+                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
+                    .build(), true /* timeout */);
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientStartWithCachedLeaseWithoutIPAddress() throws Exception {
+        final DhcpPacket packet = getReplyFromDhcpLease(
+                new NetworkAttributes.Builder()
+                    .setMtu(new Integer(TEST_DEFAULT_MTU))
+                    .setCluster(TEST_CLUSTER)
+                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
+                    .build(), false /* timeout */);
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test
+    public void testDhcpClientRapidCommitEnabled() throws Exception {
+        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
+                true /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testDhcpServerInLinkProperties() throws Exception {
+        assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q);
+
+        performDhcpHandshake();
+        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        assertEquals(SERVER_ADDR, captor.getValue().getDhcpServerAddress());
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu() throws Exception {
+        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu_WithoutMtuChange() throws Exception {
+        doRestoreInitialMtuTest(false /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu_WithException() throws Exception {
+        doThrow(new RemoteException("NetdNativeService::interfaceSetMtu")).when(mNetd)
+                .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
+
+        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTapInterface */);
+        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStopping() throws Exception {
+        doRestoreInitialMtuTest(true /* shouldChangeMtu */, true /* shouldRemoveTapInterface */);
+    }
+
+    @Test
+    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStartingProvisioning()
+            throws Exception {
+        removeTapInterface(mTapFd);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv6()
+                .build();
+
+        startIpClientProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
+        verify(mCb, never()).setNeighborDiscoveryOffload(true);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu_stopIpClientAndRestart() throws Exception {
+        long currentTime = System.currentTimeMillis();
+
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_MIN_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_MIN_MTU);
+
+        // Pretend that ConnectivityService set the MTU.
+        mNetd.interfaceSetMtu(mIfaceName, TEST_MIN_MTU);
+        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
+
+        reset(mCb);
+        reset(mIpMemoryStore);
+
+        // Stop IpClient and then restart provisioning immediately.
+        mIpc.stop();
+        currentTime = System.currentTimeMillis();
+        // Intend to set mtu option to 0, then verify that won't influence interface mtu restore.
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                0 /* mtu */, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, 0 /* mtu */);
+        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_DEFAULT_MTU);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRestoreInitialInterfaceMtu_removeInterfaceAndAddback() throws Exception {
+        doAnswer(invocation -> {
+            final LinkProperties lp = invocation.getArgument(0);
+            assertEquals(lp.getInterfaceName(), mIfaceName);
+            assertEquals(0, lp.getLinkAddresses().size());
+            assertEquals(0, lp.getDnsServers().size());
+
+            mDependencies.simulateInterfaceRecover();
+            return null;
+        }).when(mCb).onProvisioningFailure(any());
+
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv6()
+                .build();
+
+        // Intend to remove the tap interface and force IpClient throw provisioning failure
+        // due to that interface is not found.
+        removeTapInterface(mTapFd);
+        assertNull(InterfaceParams.getByName(mIfaceName));
+
+        startIpClientProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
+
+        // Make sure everything queued by this test was processed (e.g. transition to StoppingState
+        // from ClearingIpAddressState) and tearDown will check if IpClient exits normally or crash.
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+    }
+
+    private boolean isIcmpv6PacketOfType(final byte[] packetBytes, int type) {
+        ByteBuffer packet = ByteBuffer.wrap(packetBytes);
+        return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
+                && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
+                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN) == (byte) type;
+    }
+
+    private boolean isRouterSolicitation(final byte[] packetBytes) {
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_ROUTER_SOLICITATION);
+    }
+
+    private boolean isNeighborAdvertisement(final byte[] packetBytes) {
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_ADVERTISEMENT);
+    }
+
+    private boolean isNeighborSolicitation(final byte[] packetBytes) {
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_SOLICITATION);
+    }
+
+    private NeighborAdvertisement getNextNeighborAdvertisement() throws ParseException {
+        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
+                this::isNeighborAdvertisement);
+        if (packet == null) return null;
+
+        final NeighborAdvertisement na = parseNeighborAdvertisementOrNull(packet);
+        assertNotNull("Invalid neighbour advertisement received", na);
+        return na;
+    }
+
+    private NeighborSolicitation getNextNeighborSolicitation() throws ParseException {
+        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
+                this::isNeighborSolicitation);
+        if (packet == null) return null;
+
+        final NeighborSolicitation ns = parseNeighborSolicitationOrNull(packet);
+        assertNotNull("Invalid neighbour solicitation received", ns);
+        return ns;
+    }
+
+    private void waitForRouterSolicitation() throws ParseException {
+        assertNotNull("No router solicitation received on interface within timeout",
+                mPacketReader.popPacket(PACKET_TIMEOUT_MS, this::isRouterSolicitation));
+    }
+
+    private void sendRouterAdvertisement(boolean waitForRs, short lifetime) throws Exception {
+        final String dnsServer = "2001:4860:4860::64";
+        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
+        ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
+        ByteBuffer ra = buildRaPacket(lifetime, pio, rdnss);
+
+        if (waitForRs) {
+            waitForRouterSolicitation();
+        }
+
+        mPacketReader.sendResponse(ra);
+    }
+
+    private void sendBasicRouterAdvertisement(boolean waitForRs) throws Exception {
+        sendRouterAdvertisement(waitForRs, (short) 1800);
+    }
+
+    private void sendRouterAdvertisementWithZeroLifetime() throws Exception {
+        sendRouterAdvertisement(false /* waitForRs */, (short) 0);
+    }
+
+    // TODO: move this and the following method to a common location and use them in ApfTest.
+    private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
+            throws Exception {
+        return PrefixInformationOption.build(new IpPrefix(prefixString),
+                (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), valid, preferred);
+    }
+
+    private static ByteBuffer buildRdnssOption(int lifetime, String... servers) throws Exception {
+        return RdnssOption.build(lifetime, servers);
+    }
+
+    private static ByteBuffer buildSllaOption() throws Exception {
+        return LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, ROUTER_MAC);
+    }
+
+    private static ByteBuffer buildRaPacket(short lifetime, ByteBuffer... options)
+            throws Exception {
+        final MacAddress dstMac =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
+        return Ipv6Utils.buildRaPacket(ROUTER_MAC /* srcMac */, dstMac,
+                ROUTER_LINK_LOCAL /* srcIp */, IPV6_ADDR_ALL_NODES_MULTICAST /* dstIp */,
+                (byte) 0 /* M=0, O=0 */, lifetime, 0 /* Reachable time, unspecified */,
+                100 /* Retrans time 100ms */, options);
+    }
+
+    private static ByteBuffer buildRaPacket(ByteBuffer... options) throws Exception {
+        return buildRaPacket((short) 1800, options);
+    }
+
+    private void disableIpv6ProvisioningDelays() throws Exception {
+        // Speed up the test by disabling DAD and removing router_solicitation_delay.
+        // We don't need to restore the default value because the interface is removed in tearDown.
+        // TODO: speed up further by not waiting for RS but keying off first IPv6 packet.
+        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "router_solicitation_delay", "0");
+        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "0");
+    }
+
+    private void assertHasAddressThat(String msg, LinkProperties lp,
+            Predicate<LinkAddress> condition) {
+        for (LinkAddress addr : lp.getLinkAddresses()) {
+            if (condition.test(addr)) {
+                return;
+            }
+        }
+        fail(msg + " not found in: " + lp);
+    }
+
+    private boolean hasFlag(LinkAddress addr, int flag) {
+        return (addr.getFlags() & flag) == flag;
+    }
+
+    private boolean isPrivacyAddress(LinkAddress addr) {
+        return addr.isGlobalPreferred() && hasFlag(addr, IFA_F_TEMPORARY);
+    }
+
+    private boolean isStablePrivacyAddress(LinkAddress addr) {
+        // TODO: move away from getting address updates from netd and make this work on Q as well.
+        final int flag = ShimUtils.isAtLeastR() ? IFA_F_STABLE_PRIVACY : 0;
+        return addr.isGlobalPreferred() && hasFlag(addr, flag);
+    }
+
+    private LinkProperties doIpv6OnlyProvisioning() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        final String dnsServer = "2001:4860:4860::64";
+        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
+        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+
+        return doIpv6OnlyProvisioning(inOrder, ra);
+    }
+
+    private LinkProperties doIpv6OnlyProvisioning(InOrder inOrder, ByteBuffer ra) throws Exception {
+        waitForRouterSolicitation();
+        mPacketReader.sendResponse(ra);
+
+        // The lambda below needs to write a LinkProperties to a local variable, but lambdas cannot
+        // write to non-final local variables. So declare a final variable to write to.
+        final AtomicReference<LinkProperties> lpRef = new AtomicReference<>();
+
+        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
+        lpRef.set(captor.getValue());
+
+        // Sometimes provisioning completes as soon as the link-local and the stable address appear,
+        // before the privacy address appears. If so, wait here for the LinkProperties update that
+        // contains all three address. Otherwise, future calls to verify() might get confused.
+        if (captor.getValue().getLinkAddresses().size() == 2) {
+            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(lp -> {
+                lpRef.set(lp);
+                return lp.getLinkAddresses().size() == 3;
+            }));
+        }
+
+        LinkProperties lp = lpRef.get();
+        assertEquals("Should have 3 IPv6 addresses after provisioning: " + lp,
+                3, lp.getLinkAddresses().size());
+        assertHasAddressThat("link-local address", lp, x -> x.getAddress().isLinkLocalAddress());
+        assertHasAddressThat("privacy address", lp, this::isPrivacyAddress);
+        assertHasAddressThat("stable privacy address", lp, this::isStablePrivacyAddress);
+
+        return lp;
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRaRdnss() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        InOrder inOrder = inOrder(mCb);
+        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+
+        final String dnsServer = "2001:4860:4860::64";
+        final String lowlifeDnsServer = "2001:4860:4860::6464";
+
+        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
+        ByteBuffer rdnss1 = buildRdnssOption(60, lowlifeDnsServer);
+        ByteBuffer rdnss2 = buildRdnssOption(600, dnsServer);
+        ByteBuffer ra = buildRaPacket(pio, rdnss1, rdnss2);
+
+        LinkProperties lp = doIpv6OnlyProvisioning(inOrder, ra);
+
+        // Expect that DNS servers with lifetimes below CONFIG_MIN_RDNSS_LIFETIME are not accepted.
+        assertNotNull(lp);
+        assertEquals(1, lp.getDnsServers().size());
+        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
+
+        // If the RDNSS lifetime is above the minimum, the DNS server is accepted.
+        rdnss1 = buildRdnssOption(68, lowlifeDnsServer);
+        ra = buildRaPacket(pio, rdnss1, rdnss2);
+        mPacketReader.sendResponse(ra);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(captor.capture());
+        lp = captor.getValue();
+        assertNotNull(lp);
+        assertEquals(2, lp.getDnsServers().size());
+        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
+        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(lowlifeDnsServer)));
+
+        // Expect that setting RDNSS lifetime of 0 causes loss of provisioning.
+        rdnss1 = buildRdnssOption(0, dnsServer);
+        rdnss2 = buildRdnssOption(0, lowlifeDnsServer);
+        ra = buildRaPacket(pio, rdnss1, rdnss2);
+        mPacketReader.sendResponse(ra);
+
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
+        lp = captor.getValue();
+        assertNotNull(lp);
+        assertEquals(0, lp.getDnsServers().size());
+        reset(mCb);
+    }
+
+    private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception {
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
+                argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));
+
+    }
+
+    private void expectNoNat64PrefixUpdate(InOrder inOrder, IpPrefix unchanged) throws Exception {
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(0)).onLinkPropertiesChange(argThat(
+                lp -> !Objects.equals(unchanged, lp.getNat64Prefix())));
+
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testPref64Option() throws Exception {
+        assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q);
+
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        final String dnsServer = "2001:4860:4860::64";
+        final IpPrefix prefix = new IpPrefix("64:ff9b::/96");
+        final IpPrefix otherPrefix = new IpPrefix("2001:db8:64::/96");
+
+        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
+        ByteBuffer rdnss = buildRdnssOption(600, dnsServer);
+        ByteBuffer pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
+        ByteBuffer ra = buildRaPacket(pio, rdnss, pref64);
+
+        // The NAT64 prefix might be detected before or after provisioning success.
+        // Don't test order between these two events.
+        LinkProperties lp = doIpv6OnlyProvisioning(null /*inOrder*/, ra);
+        expectAlarmSet(null /*inOrder*/, "PREF64", 600);
+
+        // From now on expect events in order.
+        InOrder inOrder = inOrder(mCb, mAlarm);
+        if (lp.getNat64Prefix() != null) {
+            assertEquals(prefix, lp.getNat64Prefix());
+        } else {
+            expectNat64PrefixUpdate(inOrder, prefix);
+        }
+
+        // Increase the lifetime and expect the prefix not to change.
+        pref64 = new StructNdOptPref64(prefix, 1800).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        OnAlarmListener pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1800);
+        expectNoNat64PrefixUpdate(inOrder, prefix);
+        reset(mCb, mAlarm);
+
+        // Reduce the lifetime and expect to reschedule expiry.
+        pref64 = new StructNdOptPref64(prefix, 1500).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1496);
+        expectNoNat64PrefixUpdate(inOrder, prefix);
+        reset(mCb, mAlarm);
+
+        // Withdraw the prefix and expect it to be set to null.
+        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        expectAlarmCancelled(inOrder, pref64Alarm);
+        expectNat64PrefixUpdate(inOrder, null);
+        reset(mCb, mAlarm);
+
+        // Re-announce the prefix.
+        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        expectAlarmSet(inOrder, "PREF64", 600);
+        expectNat64PrefixUpdate(inOrder, prefix);
+        reset(mCb, mAlarm);
+
+        // Announce two prefixes. Don't expect any update because if there is already a NAT64
+        // prefix, any new prefix is ignored.
+        ByteBuffer otherPref64 = new StructNdOptPref64(otherPrefix, 1200).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
+        mPacketReader.sendResponse(ra);
+        expectAlarmSet(inOrder, "PREF64", 600);
+        expectNoNat64PrefixUpdate(inOrder, prefix);
+        reset(mCb, mAlarm);
+
+        // Withdraw the old prefix and continue to announce the new one. Expect a prefix change.
+        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
+        mPacketReader.sendResponse(ra);
+        expectAlarmCancelled(inOrder, pref64Alarm);
+        // Need a different OnAlarmListener local variable because posting it to the handler in the
+        // lambda below requires it to be final.
+        final OnAlarmListener lastAlarm = expectAlarmSet(inOrder, "PREF64", 1200);
+        expectNat64PrefixUpdate(inOrder, otherPrefix);
+        reset(mCb, mAlarm);
+
+        // Simulate prefix expiry.
+        mIpc.getHandler().post(() -> lastAlarm.onAlarm());
+        expectAlarmCancelled(inOrder, pref64Alarm);
+        expectNat64PrefixUpdate(inOrder, null);
+
+        // Announce a non-/96 prefix and expect it to be ignored.
+        IpPrefix invalidPrefix = new IpPrefix("64:ff9b::/64");
+        pref64 = new StructNdOptPref64(invalidPrefix, 1200).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        expectNoNat64PrefixUpdate(inOrder, invalidPrefix);
+
+        // Re-announce the prefix.
+        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
+        ra = buildRaPacket(pio, rdnss, pref64);
+        mPacketReader.sendResponse(ra);
+        final OnAlarmListener clearAlarm = expectAlarmSet(inOrder, "PREF64", 600);
+        expectNat64PrefixUpdate(inOrder, prefix);
+        reset(mCb, mAlarm);
+
+        // Check that the alarm is cancelled when IpClient is stopped.
+        mIpc.stop();
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+        expectAlarmCancelled(inOrder, clearAlarm);
+        expectNat64PrefixUpdate(inOrder, null);
+
+        // Check that even if the alarm was already in the message queue while it was cancelled, it
+        // is safely ignored.
+        mIpc.getHandler().post(() -> clearAlarm.onAlarm());
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+    }
+
+    private void addIpAddressAndWaitForIt(final String iface) throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final String addr1 = "192.0.2.99";
+        final String addr2 = "192.0.2.3";
+        final int prefixLength = 26;
+
+        // Add two IPv4 addresses to the specified interface, and proceed when the NetworkObserver
+        // has seen the second one. This ensures that every other NetworkObserver registered with
+        // mNetworkObserverRegistry - in particular, IpClient's - has seen the addition of the first
+        // address.
+        final LinkAddress trigger = new LinkAddress(addr2 + "/" + prefixLength);
+        NetworkObserver observer = new NetworkObserver() {
+            @Override
+            public void onInterfaceAddressUpdated(LinkAddress address, String ifName) {
+                if (ifName.equals(iface) && address.isSameAddressAs(trigger)) {
+                    latch.countDown();
+                }
+            }
+        };
+
+        mNetworkObserverRegistry.registerObserverForNonblockingCallback(observer);
+        try {
+            mNetd.interfaceAddAddress(iface, addr1, prefixLength);
+            mNetd.interfaceAddAddress(iface, addr2, prefixLength);
+            assertTrue("Trigger IP address " + addr2 + " not seen after " + TEST_TIMEOUT_MS + "ms",
+                    latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            mNetworkObserverRegistry.unregisterObserver(observer);
+        }
+
+        // Wait for IpClient to process the addition of the address.
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+    }
+
+    private void doIPv4OnlyProvisioningAndExitWithLeftAddress() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+
+        // Stop IpClient and expect a final LinkProperties callback with an empty LP.
+        mIIpClient.stop();
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.getAddresses().size() == 0
+                        && x.getRoutes().size() == 0
+                        && x.getDnsServers().size() == 0));
+        reset(mCb);
+
+        // Pretend that something else (e.g., Tethering) used the interface and left an IP address
+        // configured on it. When IpClient starts, it must clear this address before proceeding.
+        // The address must be noticed before startProvisioning is called, or IpClient will
+        // immediately declare provisioning success due to the presence of an IPv4 address.
+        // The address must be IPv4 because IpClient clears IPv6 addresses on startup.
+        //
+        // TODO: once IpClient gets IP addresses directly from netlink instead of from netd, it
+        // may be sufficient to call waitForIdle to see if IpClient has seen the address.
+        addIpAddressAndWaitForIt(mIfaceName);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testIpClientClearingIpAddressState() throws Exception {
+        doIPv4OnlyProvisioningAndExitWithLeftAddress();
+
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .build();
+        startIpClientProvisioning(config);
+
+        sendBasicRouterAdvertisement(true /*waitForRs*/);
+
+        // Check that the IPv4 addresses configured earlier are not in LinkProperties...
+        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        assertFalse(captor.getValue().hasIpv4Address());
+
+        // ... or configured on the interface.
+        InterfaceConfigurationParcel cfg = mNetd.interfaceGetCfg(mIfaceName);
+        assertEquals("0.0.0.0", cfg.ipv4Addr);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testIpClientClearingIpAddressState_enablePreconnection() throws Exception {
+        doIPv4OnlyProvisioningAndExitWithLeftAddress();
+
+        // Enter ClearingIpAddressesState to clear the remaining IPv4 addresses and transition to
+        // PreconnectionState instead of RunningState.
+        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
+                false /* shouldReplyRapidCommitAck */, true /* isDhcpPreConnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        assertDiscoverPacketOnPreconnectionStart();
+
+        // Force to enter RunningState.
+        mIpc.notifyPreconnectionComplete(false /* abort */);
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_success() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_SuccessWithoutRapidCommit() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_Abort() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_AbortWithoutRapiCommit() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutBeforeAbort() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                true /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutBeforeAbortWithoutRapidCommit()
+            throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                true /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutafterAbort() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutAfterAbortWithoutRapidCommit() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutBeforeSuccess() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                true /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutBeforeSuccessWithoutRapidCommit()
+            throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                true /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutAfterSuccess() throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_TimeoutAfterSuccessWithoutRapidCommit()
+            throws Exception {
+        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
+                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
+                false /* timeoutBeforePreconnectionComplete */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpClientPreconnection_WithoutLayer2InfoWhenStartingProv() throws Exception {
+        // For FILS connection, current bssid (also l2key and cluster) is still null when
+        // starting provisioning since the L2 link hasn't been established yet. Ensure that
+        // IpClient won't crash even if initializing an Layer2Info class with null members.
+        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv6()
+                .withPreconnection()
+                .withLayer2Information(new Layer2Information(null /* l2key */, null /* cluster */,
+                        null /* bssid */));
+
+        startIpClientProvisioning(prov.build());
+        assertDiscoverPacketOnPreconnectionStart();
+        verify(mCb).setNeighborDiscoveryOffload(true);
+
+        // Force IpClient transition to RunningState from PreconnectionState.
+        mIIpClient.notifyPreconnectionComplete(false /* success */);
+        HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+    }
+
+    @Test
+    public void testDhcpDecline_conflictByArpReply() throws Exception {
+        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
+                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                true /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_conflictByArpProbe() throws Exception {
+        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
+                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_EnableFlagWithoutIpConflict() throws Exception {
+        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
+                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_WithoutIpConflict() throws Exception {
+        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
+                false /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_WithRapidCommitWithoutIpConflict() throws Exception {
+        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
+                true /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_WithRapidCommitConflictByArpReply() throws Exception {
+        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
+                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                true /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_WithRapidCommitConflictByArpProbe() throws Exception {
+        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
+                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test
+    public void testDhcpDecline_EnableFlagWithRapidCommitWithoutIpConflict() throws Exception {
+        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
+                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
+                false /* shouldResponseArpReply */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testHostname_enableConfig() throws Exception {
+        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
+                TEST_HOST_NAME);
+
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */);
+
+        assertEquals(2, sentPackets.size());
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertHostname(true, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testHostname_disableConfig() throws Exception {
+        mDependencies.setHostnameConfiguration(false /* isHostnameConfigurationEnabled */,
+                TEST_HOST_NAME);
+
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */);
+
+        assertEquals(2, sentPackets.size());
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertHostname(false, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testHostname_enableConfigWithNullHostname() throws Exception {
+        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
+                null /* hostname */);
+
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */);
+
+        assertEquals(2, sentPackets.size());
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertHostname(true, null /* hostname */, null /* hostnameAfterTransliteration */,
+                sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    private void runDhcpClientCaptivePortalApiTest(boolean featureEnabled,
+            boolean serverSendsOption) throws Exception {
+        startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
+                false /* shouldReplyRapidCommitAck */, false /* isPreConnectionEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        final DhcpPacket discover = getNextDhcpPacket();
+        assertTrue(discover instanceof DhcpDiscoverPacket);
+        assertEquals(featureEnabled, discover.hasRequestedParam(DhcpPacket.DHCP_CAPTIVE_PORTAL));
+
+        // Send Offer and handle Request -> Ack
+        final String serverSentUrl = serverSendsOption ? TEST_CAPTIVE_PORTAL_URL : null;
+        mPacketReader.sendResponse(buildDhcpOfferPacket(discover, CLIENT_ADDR,
+                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, serverSentUrl));
+        final int testMtu = 1345;
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                false /* shouldReplyRapidCommitAck */, testMtu, serverSentUrl);
+
+        final Uri expectedUrl = featureEnabled && serverSendsOption
+                ? Uri.parse(TEST_CAPTIVE_PORTAL_URL) : null;
+        // LinkProperties will be updated multiple times. Wait for it to contain DHCP-obtained info,
+        // such as MTU.
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
+                argThat(lp -> lp.getMtu() == testMtu));
+
+        // Ensure that the URL was set as expected in the callbacks.
+        // Can't verify the URL up to Q as there is no such attribute in LinkProperties.
+        if (!ShimUtils.isAtLeastR()) return;
+        verify(mCb, atLeastOnce()).onLinkPropertiesChange(captor.capture());
+        assertTrue(captor.getAllValues().stream().anyMatch(
+                lp -> Objects.equals(expectedUrl, lp.getCaptivePortalApiUrl())));
+    }
+
+    @Test
+    public void testDhcpClientCaptivePortalApiEnabled() throws Exception {
+        // Only run the test on platforms / builds where the API is enabled
+        assumeTrue(CaptivePortalDataShimImpl.isSupported());
+        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, true /* serverSendsOption */);
+    }
+
+    @Test
+    public void testDhcpClientCaptivePortalApiEnabled_NoUrl() throws Exception {
+        // Only run the test on platforms / builds where the API is enabled
+        assumeTrue(CaptivePortalDataShimImpl.isSupported());
+        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, false /* serverSendsOption */);
+    }
+
+    @Test
+    public void testDhcpClientCaptivePortalApiDisabled() throws Exception {
+        // Only run the test on platforms / builds where the API is disabled
+        assumeFalse(CaptivePortalDataShimImpl.isSupported());
+        runDhcpClientCaptivePortalApiTest(false /* featureEnabled */, true /* serverSendsOption */);
+    }
+
+    private ScanResultInfo makeScanResultInfo(final int id, final String ssid,
+            final String bssid, final byte[] oui, final byte type, final byte[] data) {
+        final ByteBuffer payload = ByteBuffer.allocate(4 + data.length);
+        payload.put(oui);
+        payload.put(type);
+        payload.put(data);
+        payload.flip();
+        final ScanResultInfo.InformationElement ie =
+                new ScanResultInfo.InformationElement(id /* IE id */, payload);
+        return new ScanResultInfo(ssid, bssid, Collections.singletonList(ie));
+    }
+
+    private ScanResultInfo makeScanResultInfo(final int id, final byte[] oui, final byte type) {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        return makeScanResultInfo(id, TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID, oui, type, data);
+    }
+
+    private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        return makeScanResultInfo(0xdd, ssid, bssid, TEST_AP_OUI, (byte) 0x06, data);
+    }
+
+    private void doUpstreamHotspotDetectionTest(final int id, final String displayName,
+            final String ssid, final byte[] oui, final byte type, final byte[] data,
+            final boolean expectMetered) throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(id, ssid, TEST_DEFAULT_BSSID, oui, type,
+                data);
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */,
+                false /* isIPv6OnlyPreferredEnabled */,
+                null /* captivePortalApiUrl */, displayName, info /* scanResultInfo */,
+                null /* layer2Info */);
+        assertEquals(2, sentPackets.size());
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+
+        ArgumentCaptor<DhcpResultsParcelable> captor =
+                ArgumentCaptor.forClass(DhcpResultsParcelable.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
+        DhcpResults lease = fromStableParcelable(captor.getValue());
+        assertNotNull(lease);
+        assertEquals(lease.getIpAddress().getAddress(), CLIENT_ADDR);
+        assertEquals(lease.getGateway(), SERVER_ADDR);
+        assertEquals(1, lease.getDnsServers().size());
+        assertTrue(lease.getDnsServers().contains(SERVER_ADDR));
+        assertEquals(lease.getServerAddress(), SERVER_ADDR);
+        assertEquals(lease.getMtu(), TEST_DEFAULT_MTU);
+
+        if (expectMetered) {
+            assertEquals(lease.vendorInfo, DhcpPacket.VENDOR_INFO_ANDROID_METERED);
+        } else {
+            assertNull(lease.vendorInfo);
+        }
+
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection() throws Exception {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
+                true /* expectMetered */);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection_incorrectIeId() throws Exception {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        doUpstreamHotspotDetectionTest(0xdc, "\"ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
+                false /* expectMetered */);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection_incorrectOUI() throws Exception {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x1A, (byte) 0x11 }, (byte) 0x06, data,
+                false /* expectMetered */);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection_incorrectSsid() throws Exception {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        doUpstreamHotspotDetectionTest(0xdd, "\"another ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
+                false /* expectMetered */);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection_incorrectType() throws Exception {
+        byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x0a, data,
+                false /* expectMetered */);
+    }
+
+    @Test
+    public void testUpstreamHotspotDetection_zeroLengthData() throws Exception {
+        byte[] data = new byte[0];
+        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
+                true /* expectMetered */);
+    }
+
+    private void forceLayer2Roaming() throws Exception {
+        final Layer2InformationParcelable roamingInfo = new Layer2InformationParcelable();
+        roamingInfo.bssid = MacAddress.fromString(TEST_DHCP_ROAM_BSSID);
+        roamingInfo.l2Key = TEST_DHCP_ROAM_L2KEY;
+        roamingInfo.cluster = TEST_DHCP_ROAM_CLUSTER;
+        mIIpClient.updateLayer2Information(roamingInfo);
+    }
+
+    private void doDhcpRoamingTest(final boolean hasMismatchedIpAddress, final String displayName,
+            final MacAddress bssid, final boolean expectRoaming) throws Exception {
+        long currentTime = System.currentTimeMillis();
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER, bssid);
+
+        doAnswer(invocation -> {
+            // we don't rely on the Init-Reboot state to renew previous cached IP lease.
+            // Just return null and force state machine enter INIT state.
+            final String l2Key = invocation.getArgument(0);
+            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
+                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
+            return null;
+        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
+
+        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
+                null /* hostname */);
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */,
+                false /* isIPv6OnlyPreferredEnabled */,
+                null /* captivePortalApiUrl */, displayName, null /* scanResultInfo */,
+                layer2Info);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+
+        // simulate the roaming by updating bssid.
+        forceLayer2Roaming();
+
+        currentTime = System.currentTimeMillis();
+        reset(mIpMemoryStore);
+        reset(mCb);
+        if (!expectRoaming) {
+            assertIpMemoryNeverStoreNetworkAttributes();
+            return;
+        }
+        // check DHCPREQUEST broadcast sent to renew IP address.
+        DhcpPacket packet;
+        packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpRequestPacket);
+        assertEquals(packet.mClientIp, CLIENT_ADDR);    // client IP
+        assertNull(packet.mRequestedIp);                // requested IP option
+        assertNull(packet.mServerIdentifier);           // server ID
+
+        mPacketReader.sendResponse(buildDhcpAckPacket(packet,
+                hasMismatchedIpAddress ? CLIENT_ADDR_NEW : CLIENT_ADDR, TEST_LEASE_DURATION_S,
+                (short) TEST_DEFAULT_MTU, false /* rapidcommit */, null /* captivePortalUrl */));
+        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
+        if (hasMismatchedIpAddress) {
+            // notifyFailure
+            ArgumentCaptor<DhcpResultsParcelable> captor =
+                    ArgumentCaptor.forClass(DhcpResultsParcelable.class);
+            verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
+            DhcpResults lease = fromStableParcelable(captor.getValue());
+            assertNull(lease);
+
+            // roll back to INIT state.
+            packet = getNextDhcpPacket();
+            assertTrue(packet instanceof DhcpDiscoverPacket);
+        } else {
+            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
+                    TEST_DEFAULT_MTU);
+        }
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpRoaming() throws Exception {
+        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
+                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpRoaming_invalidBssid() throws Exception {
+        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
+                MacAddress.fromString(TEST_DHCP_ROAM_BSSID), false /* expectRoaming */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpRoaming_nullBssid() throws Exception {
+        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
+                null /* BSSID */, false /* expectRoaming */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpRoaming_invalidDisplayName() throws Exception {
+        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"test-ssid\"" /* display name */,
+                MacAddress.fromString(TEST_DEFAULT_BSSID), false /* expectRoaming */);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDhcpRoaming_mismatchedLeasedIpAddress() throws Exception {
+        doDhcpRoamingTest(true /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
+                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */);
+    }
+
+    private void performDualStackProvisioning() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+        final String dnsServer = "2001:4860:4860::64";
+        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
+        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+
+        doIpv6OnlyProvisioning(inOrder, ra);
+
+        // Start IPv4 provisioning and wait until entire provisioning completes.
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(x -> {
+            if (!x.isIpv4Provisioned() || !x.isIpv6Provisioned()) return false;
+            lpFuture.complete(x);
+            return true;
+        }));
+
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
+        assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
+
+        reset(mCb);
+    }
+
+    private void doDualStackProvisioning(boolean shouldDisableAcceptRa) throws Exception {
+        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
+
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .build();
+
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_DISABLE_ACCEPT_RA_VERSION,
+                shouldDisableAcceptRa);
+        // Enable rapid commit to accelerate DHCP handshake to shorten test duration,
+        // not strictly necessary.
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        mIpc.startProvisioning(config);
+
+        performDualStackProvisioning();
+    }
+
+    @Test @SignatureRequiredTest(reason = "signature perms are required due to mocked callabck")
+    public void testIgnoreIpv6ProvisioningLoss_disableIPv6Stack() throws Exception {
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+
+        // Send RA with 0-lifetime and wait until all IPv6-related default route and DNS servers
+        // have been removed, then verify if there is IPv4-only info left in the LinkProperties.
+        sendRouterAdvertisementWithZeroLifetime();
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
+                argThat(x -> {
+                    final boolean isOnlyIPv4Provisioned = (x.getLinkAddresses().size() == 1
+                            && x.getDnsServers().size() == 1
+                            && x.getAddresses().get(0) instanceof Inet4Address
+                            && x.getDnsServers().get(0) instanceof Inet4Address);
+
+                    if (!isOnlyIPv4Provisioned) return false;
+                    lpFuture.complete(x);
+                    return true;
+                }));
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
+        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
+
+        final ArgumentCaptor<Integer> quirkEvent = ArgumentCaptor.forClass(Integer.class);
+        verify(mNetworkQuirkMetricsDeps, timeout(TEST_TIMEOUT_MS)).writeStats(quirkEvent.capture());
+        assertEquals((long) quirkEvent.getValue(),
+                (long) NetworkQuirkEvent.QE_IPV6_PROVISIONING_ROUTER_LOST.ordinal());
+    }
+
+    @Test @SignatureRequiredTest(reason = "signature perms are required due to mocked callabck")
+    public void testIgnoreIpv6ProvisioningLoss_disableAcceptRa() throws Exception {
+        doDualStackProvisioning(true /* shouldDisableAcceptRa */);
+
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+
+        // Send RA with 0-lifetime and wait until all global IPv6 addresses, IPv6-related default
+        // route and DNS servers have been removed, then verify if there is IPv4-only, IPv6 link
+        // local address and route to fe80::/64 info left in the LinkProperties.
+        sendRouterAdvertisementWithZeroLifetime();
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
+                argThat(x -> {
+                    // Only IPv4 provisioned and IPv6 link-local address
+                    final boolean isIPv6LinkLocalAndIPv4OnlyProvisioned =
+                            (x.getLinkAddresses().size() == 2
+                                    && x.getDnsServers().size() == 1
+                                    && x.getAddresses().get(0) instanceof Inet4Address
+                                    && x.getDnsServers().get(0) instanceof Inet4Address);
+
+                    if (!isIPv6LinkLocalAndIPv4OnlyProvisioned) return false;
+                    lpFuture.complete(x);
+                    return true;
+                }));
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
+        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
+        assertTrue(lp.getAddresses().get(1).isLinkLocalAddress());
+
+        reset(mCb);
+
+        // Send an RA to verify that global IPv6 addresses won't be configured on the interface.
+        sendBasicRouterAdvertisement(false /* waitForRs */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(0)).onLinkPropertiesChange(any());
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDualStackProvisioning() throws Exception {
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+
+        verify(mCb, never()).onProvisioningFailure(any());
+    }
+
+    private DhcpPacket verifyDhcpPacketRequestsIPv6OnlyPreferredOption(
+            Class<? extends DhcpPacket> packetType) throws Exception {
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packetType.isInstance(packet));
+        assertTrue(packet.hasRequestedParam(DHCP_IPV6_ONLY_PREFERRED));
+        return packet;
+    }
+
+    private void doIPv6OnlyPreferredOptionTest(final Integer ipv6OnlyWaitTime,
+            final Inet4Address clientAddress) throws Exception {
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .build();
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, false /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, true /* isIPv6OnlyPreferredEnabled */);
+        startIpClientProvisioning(config);
+
+        final DhcpPacket packet =
+                verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
+
+        // Respond DHCPOFFER with IPv6-Only preferred option and offered address.
+        mPacketReader.sendResponse(buildDhcpOfferPacket(packet, clientAddress,
+                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, null /* captivePortalUrl */,
+                ipv6OnlyWaitTime));
+    }
+
+    private void doDiscoverIPv6OnlyPreferredOptionTest(final int optionSecs,
+            final long expectedWaitSecs) throws Exception {
+        doIPv6OnlyPreferredOptionTest(optionSecs, CLIENT_ADDR);
+        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT",
+                expectedWaitSecs, mDependencies.mDhcpClient.getHandler());
+        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());
+        // Implicitly check that the client never sent a DHCPREQUEST to request the offered address.
+        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption() throws Exception {
+        doDiscoverIPv6OnlyPreferredOptionTest(TEST_IPV6_ONLY_WAIT_S, TEST_IPV6_ONLY_WAIT_S);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_LowerIPv6OnlyWait() throws Exception {
+        doDiscoverIPv6OnlyPreferredOptionTest(TEST_LOWER_IPV6_ONLY_WAIT_S,
+                TEST_LOWER_IPV6_ONLY_WAIT_S);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_ZeroIPv6OnlyWait() throws Exception {
+        doDiscoverIPv6OnlyPreferredOptionTest(TEST_ZERO_IPV6_ONLY_WAIT_S,
+                TEST_LOWER_IPV6_ONLY_WAIT_S);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_MaxIPv6OnlyWait() throws Exception {
+        doDiscoverIPv6OnlyPreferredOptionTest((int) TEST_MAX_IPV6_ONLY_WAIT_S, 0xffffffffL);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_ZeroIPv6OnlyWaitWithOfferedAnyAddress()
+            throws Exception {
+        doIPv6OnlyPreferredOptionTest(TEST_ZERO_IPV6_ONLY_WAIT_S, IPV4_ADDR_ANY);
+
+        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 300,
+                mDependencies.mDhcpClient.getHandler());
+        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());
+
+        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_enabledPreconnection() throws Exception {
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withPreconnection()
+                .build();
+
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, true /* isIPv6OnlyPreferredEnabled */);
+        startIpClientProvisioning(config);
+
+        final DhcpPacket packet = assertDiscoverPacketOnPreconnectionStart();
+        verify(mCb).setNeighborDiscoveryOffload(true);
+
+        // Force IpClient transition to RunningState from PreconnectionState.
+        mIpc.notifyPreconnectionComplete(true /* success */);
+        HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+
+        // DHCP server SHOULD NOT honor the Rapid-Commit option if the response would
+        // contain the IPv6-only Preferred option to the client, instead respond with
+        // a DHCPOFFER.
+        mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR, TEST_LEASE_DURATION_S,
+                (short) TEST_DEFAULT_MTU, null /* captivePortalUrl */, TEST_IPV6_ONLY_WAIT_S));
+
+        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 1800,
+                mDependencies.mDhcpClient.getHandler());
+        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());
+
+        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testDiscoverIPv6OnlyPreferredOption_NoIPv6OnlyPreferredOption() throws Exception {
+        doIPv6OnlyPreferredOptionTest(null /* ipv6OnlyWaitTime */, CLIENT_ADDR);
+
+        // The IPv6-only Preferred option SHOULD be included in the Parameter Request List option
+        // in DHCPREQUEST messages after receiving a DHCPOFFER without this option.
+        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpRequestPacket.class);
+    }
+
+    private void startFromInitRebootStateWithIPv6OnlyPreferredOption(final Integer ipv6OnlyWaitTime,
+            final long expectedWaitSecs) throws Exception {
+        doAnswer(invocation -> {
+            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
+                    .onNetworkAttributesRetrieved(new Status(SUCCESS), TEST_L2KEY,
+                            new NetworkAttributes.Builder()
+                                .setAssignedV4Address(CLIENT_ADDR)
+                                .setAssignedV4AddressExpiry(Long.MAX_VALUE) // lease is always valid
+                                .setMtu(new Integer(TEST_DEFAULT_MTU))
+                                .setCluster(TEST_CLUSTER)
+                                .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
+                                .build());
+            return null;
+        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
+
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                          MacAddress.fromString(TEST_DEFAULT_BSSID)))
+                .build();
+
+        setDhcpFeatures(true /* isDhcpLeaseCacheEnabled */, false /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, true /* isIPv6OnlyPreferredEnabled */);
+        startIpClientProvisioning(config);
+
+        final DhcpPacket packet =
+                verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpRequestPacket.class);
+
+        // Respond DHCPACK with IPv6-Only preferred option.
+        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR,
+                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, false /* rapidcommit */,
+                null /* captivePortalUrl */, ipv6OnlyWaitTime));
+
+        if (ipv6OnlyWaitTime != null) {
+            expectAlarmSet(null /* inOrder */, "TIMEOUT", expectedWaitSecs,
+                    mDependencies.mDhcpClient.getHandler());
+        }
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRequestIPv6OnlyPreferredOption() throws Exception {
+        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_IPV6_ONLY_WAIT_S,
+                TEST_IPV6_ONLY_WAIT_S);
+
+        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
+        // IPv6-Only preferred option(default value) in the DHCPACK packet.
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRequestIPv6OnlyPreferredOption_LowerIPv6OnlyWait() throws Exception {
+        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_LOWER_IPV6_ONLY_WAIT_S,
+                TEST_LOWER_IPV6_ONLY_WAIT_S);
+
+        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
+        // IPv6-Only preferred option(less than MIN_V6ONLY_WAIT_MS) in the DHCPACK packet.
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRequestIPv6OnlyPreferredOption_ZeroIPv6OnlyWait() throws Exception {
+        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_ZERO_IPV6_ONLY_WAIT_S,
+                TEST_LOWER_IPV6_ONLY_WAIT_S);
+
+        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
+        // IPv6-Only preferred option(0) in the DHCPACK packet.
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRequestIPv6OnlyPreferredOption_MaxIPv6OnlyWait() throws Exception {
+        startFromInitRebootStateWithIPv6OnlyPreferredOption((int) TEST_MAX_IPV6_ONLY_WAIT_S,
+                0xffffffffL);
+
+        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
+        // IPv6-Only preferred option(MAX_UNSIGNED_INTEGER: 0xFFFFFFFF) in the DHCPACK packet.
+        assertIpMemoryNeverStoreNetworkAttributes();
+    }
+
+    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
+    public void testRequestIPv6OnlyPreferredOption_NoIPv6OnlyPreferredOption() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        startFromInitRebootStateWithIPv6OnlyPreferredOption(null /* ipv6OnlyWaitTime */,
+                0 /* expectedWaitSecs */);
+
+        // Client processes DHCPACK packet normally and transits to the ConfiguringInterfaceState
+        // due to the null V6ONLY_WAIT.
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    private static int getNumOpenFds() {
+        return new File("/proc/" + Os.getpid() + "/fd").listFiles().length;
+    }
+
+    private void shutdownAndRecreateIpClient() throws Exception {
+        mIpc.shutdown();
+        awaitIpClientShutdown();
+        mIpc = makeIpClient();
+    }
+
+    @Test @SignatureRequiredTest(reason = "Only counts FDs from the current process. TODO: fix")
+    public void testNoFdLeaks() throws Exception {
+        // Shut down and restart IpClient once to ensure that any fds that are opened the first
+        // time it runs do not cause the test to fail.
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+        shutdownAndRecreateIpClient();
+
+        // Unfortunately we cannot use a large number of iterations as it would make the test run
+        // too slowly. On crosshatch-eng each iteration takes ~250ms.
+        final int iterations = 10;
+        final int before = getNumOpenFds();
+        for (int i = 0; i < iterations; i++) {
+            doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+            shutdownAndRecreateIpClient();
+            // The last time this loop runs, mIpc will be shut down in tearDown.
+        }
+        final int after = getNumOpenFds();
+
+        // Check that the number of open fds is the same as before.
+        // If this exact match becomes flaky, we could add some tolerance here (e.g., allow 2-3
+        // extra fds), since it's likely that any leak would at least leak one FD per loop.
+        assertEquals("Fd leak after " + iterations + " iterations: ", before, after);
+    }
+
+    // TODO: delete when DhcpOption is @JavaOnlyImmutable.
+    private static DhcpOption makeDhcpOption(final byte type, final byte[] value) {
+        final DhcpOption opt = new DhcpOption();
+        opt.type = type;
+        opt.value = value;
+        return opt;
+    }
+
+    private static final List<DhcpOption> TEST_OEM_DHCP_OPTIONS = Arrays.asList(
+            // DHCP_USER_CLASS
+            makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
+            // DHCP_VENDOR_CLASS_ID
+            makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes())
+    );
+
+    private DhcpPacket doCustomizedDhcpOptionsTest(final List<DhcpOption> options,
+             final ScanResultInfo info) throws Exception {
+        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                        MacAddress.fromString(TEST_DEFAULT_BSSID)))
+                .withScanResultInfo(info)
+                .withDhcpOptions(options)
+                .withoutIPv6();
+
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, false /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+
+        startIpClientProvisioning(prov.build());
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, never()).onProvisioningFailure(any());
+
+        return getNextDhcpPacket();
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
+        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_nullDhcpOptions() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_nullScanResultInfo() throws Exception {
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS,
+                null /* scanResultInfo */);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_disallowedOui() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */,
+                new byte[]{ 0x00, 0x11, 0x22} /* oui */, (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_invalidIeId() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x10 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_disallowedOption() throws Exception {
+        final List<DhcpOption> options = Arrays.asList(
+                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
+                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
+                // Option 26: MTU
+                makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU)));
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
+        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
+        assertNull(packet.mMtu);
+    }
+
+    @Test
+    public void testCustomizedDhcpOptions_disallowedParamRequestOption() throws Exception {
+        final List<DhcpOption> options = Arrays.asList(
+                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
+                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
+                // NTP_SERVER
+                makeDhcpOption((byte) 42, null));
+        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specificIE */, TEST_OEM_OUI,
+                (byte) 0x17 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
+        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
+        assertFalse(packet.hasRequestedParam((byte) 42 /* NTP_SERVER */));
+    }
+
+    private void assertGratuitousNa(final NeighborAdvertisement na) throws Exception {
+        final MacAddress etherMulticast =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
+        final LinkAddress target = new LinkAddress(na.naHdr.target, 64);
+
+        assertEquals(etherMulticast, na.ethHdr.dstMac);
+        assertEquals(ETH_P_IPV6, na.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, na.ipv6Hdr.nextHeader);
+        assertEquals(0xff, na.ipv6Hdr.hopLimit);
+        assertTrue(na.ipv6Hdr.srcIp.isLinkLocalAddress());
+        assertEquals(IPV6_ADDR_ALL_ROUTERS_MULTICAST, na.ipv6Hdr.dstIp);
+        assertEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, na.icmpv6Hdr.type);
+        assertEquals(0, na.icmpv6Hdr.code);
+        assertEquals(0, na.naHdr.flags);
+        assertTrue(target.isGlobalPreferred());
+    }
+
+    @Test
+    public void testGratuitousNaForNewGlobalUnicastAddresses() throws Exception {
+        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION,
+                true /* isGratuitousNaEnabled */);
+        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, false));
+        startIpClientProvisioning(config);
+
+        doIpv6OnlyProvisioning();
+
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        NeighborAdvertisement packet;
+        while ((packet = getNextNeighborAdvertisement()) != null) {
+            assertGratuitousNa(packet);
+            naList.add(packet);
+        }
+        assertEquals(2, naList.size()); // privacy address and stable privacy address
+    }
+
+    private void startGratuitousArpAndNaAfterRoamingTest(boolean isGratuitousArpNaRoamingEnabled,
+            boolean hasIpv4, boolean hasIpv6) throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_DEFAULT_BSSID));
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
+        final ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withLayer2Information(layer2Info)
+                .withScanResultInfo(scanResultInfo)
+                .withDisplayName("ssid");
+        if (!hasIpv4) prov.withoutIPv4();
+        if (!hasIpv6) prov.withoutIPv6();
+
+        // Enable rapid commit to accelerate DHCP handshake to shorten test duration,
+        // not strictly necessary.
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        if (isGratuitousArpNaRoamingEnabled) {
+            setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, true);
+            assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false));
+        }
+        startIpClientProvisioning(prov.build());
+    }
+
+    private void waitForGratuitousArpAndNaPacket(final List<ArpPacket> arpList,
+            final List<NeighborAdvertisement> naList) throws Exception {
+        NeighborAdvertisement na;
+        ArpPacket garp;
+        do {
+            na = getNextNeighborAdvertisement();
+            if (na != null) {
+                assertGratuitousNa(na);
+                naList.add(na);
+            }
+            garp = getNextArpPacket(TEST_TIMEOUT_MS);
+            if (garp != null) {
+                assertGratuitousARP(garp);
+                arpList.add(garp);
+            }
+        } while (na != null || garp != null);
+    }
+
+    @Test
+    public void testGratuitousArpAndNaAfterRoaming() throws Exception {
+        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
+                true /* hasIpv4 */, true /* hasIpv6 */);
+        performDualStackProvisioning();
+        forceLayer2Roaming();
+
+        final List<ArpPacket> arpList = new ArrayList<>();
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        waitForGratuitousArpAndNaPacket(arpList, naList);
+        assertEquals(2, naList.size()); // privacy address and stable privacy address
+        assertEquals(1, arpList.size()); // IPv4 address
+    }
+
+    @Test
+    public void testGratuitousArpAndNaAfterRoaming_disableExpFlag() throws Exception {
+        startGratuitousArpAndNaAfterRoamingTest(false /* isGratuitousArpNaRoamingEnabled */,
+                true /* hasIpv4 */, true /* hasIpv6 */);
+        performDualStackProvisioning();
+        forceLayer2Roaming();
+
+        final List<ArpPacket> arpList = new ArrayList<>();
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        waitForGratuitousArpAndNaPacket(arpList, naList);
+        assertEquals(0, naList.size());
+        assertEquals(0, arpList.size());
+    }
+
+    @Test
+    public void testGratuitousArpAndNaAfterRoaming_IPv6OnlyNetwork() throws Exception {
+        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
+                false /* hasIpv4 */, true /* hasIpv6 */);
+        doIpv6OnlyProvisioning();
+        forceLayer2Roaming();
+
+        final List<ArpPacket> arpList = new ArrayList<>();
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        waitForGratuitousArpAndNaPacket(arpList, naList);
+        assertEquals(2, naList.size());
+        assertEquals(0, arpList.size());
+    }
+
+    @Test
+    public void testGratuitousArpAndNaAfterRoaming_IPv4OnlyNetwork() throws Exception {
+        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
+                true /* hasIpv4 */, false /* hasIpv6 */);
+
+        // Start IPv4 provisioning and wait until entire provisioning completes.
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        forceLayer2Roaming();
+
+        final List<ArpPacket> arpList = new ArrayList<>();
+        final List<NeighborAdvertisement> naList = new ArrayList<>();
+        waitForGratuitousArpAndNaPacket(arpList, naList);
+        assertEquals(0, naList.size());
+        assertEquals(1, arpList.size());
+    }
+
+    private void assertNeighborSolicitation(final NeighborSolicitation ns,
+            final Inet6Address target) {
+        assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
+        assertEquals(0xff, ns.ipv6Hdr.hopLimit);
+        assertTrue(ns.ipv6Hdr.srcIp.isLinkLocalAddress());
+        assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
+        assertEquals(0, ns.icmpv6Hdr.code);
+        assertEquals(0, ns.nsHdr.reserved);
+        assertEquals(target, ns.nsHdr.target);
+        assertEquals(ns.slla.linkLayerAddress, ns.ethHdr.srcMac);
+    }
+
+    private void assertUnicastNeighborSolicitation(final NeighborSolicitation ns,
+            final MacAddress dstMac, final Inet6Address dstIp, final Inet6Address target) {
+        assertEquals(dstMac, ns.ethHdr.dstMac);
+        assertEquals(dstIp, ns.ipv6Hdr.dstIp);
+        assertNeighborSolicitation(ns, target);
+    }
+
+    private void assertMulticastNeighborSolicitation(final NeighborSolicitation ns,
+            final Inet6Address target) {
+        final MacAddress etherMulticast =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(ns.ipv6Hdr.dstIp);
+        assertEquals(etherMulticast, ns.ethHdr.dstMac);
+        assertTrue(ns.ipv6Hdr.dstIp.isMulticastAddress());
+        assertNeighborSolicitation(ns, target);
+    }
+
+    private NeighborSolicitation waitForUnicastNeighborSolicitation(final MacAddress dstMac,
+            final Inet6Address dstIp, final Inet6Address targetIp) throws Exception {
+        NeighborSolicitation ns;
+        while ((ns = getNextNeighborSolicitation()) != null) {
+            // Filter out the NSes used for duplicate address detetction, the target address
+            // is the global IPv6 address inside these NSes.
+            if (ns.nsHdr.target.isLinkLocalAddress()) break;
+        }
+        assertNotNull("No unicast Neighbor solicitation received on interface within timeout", ns);
+        assertUnicastNeighborSolicitation(ns, dstMac, dstIp, targetIp);
+        return ns;
+    }
+
+    private List<NeighborSolicitation> waitForMultipleNeighborSolicitations() throws Exception {
+        NeighborSolicitation ns;
+        final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
+        while ((ns = getNextNeighborSolicitation()) != null) {
+            // Filter out the NSes used for duplicate address detetction, the target address
+            // is the global IPv6 address inside these NSes.
+            if (ns.nsHdr.target.isLinkLocalAddress()) {
+                nsList.add(ns);
+            }
+        }
+        assertFalse(nsList.isEmpty());
+        return nsList;
+    }
+
+    // Override this function with disabled experiment flag by default, in order not to
+    // affect those tests which are just related to basic IpReachabilityMonitor infra.
+    private void prepareIpReachabilityMonitorTest() throws Exception {
+        prepareIpReachabilityMonitorTest(false /* isMulticastResolicitEnabled */);
+    }
+
+    private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled)
+            throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                       MacAddress.fromString(TEST_DEFAULT_BSSID)))
+                .withScanResultInfo(info)
+                .withDisplayName(TEST_DEFAULT_SSID)
+                .withoutIPv4()
+                .build();
+        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
+                isMulticastResolicitEnabled);
+        startIpClientProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        doIpv6OnlyProvisioning();
+
+        // Simulate the roaming.
+        forceLayer2Roaming();
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_probeFailed() throws Exception {
+        prepareIpReachabilityMonitorTest();
+
+        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
+        assertEquals(MIN_NUD_SOLICIT_NUM, nsList.size());
+        for (NeighborSolicitation ns : nsList) {
+            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
+                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+        }
+        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_probeReachable() throws Exception {
+        prepareIpReachabilityMonitorTest();
+
+        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
+                ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+
+        // Reply Neighbor Advertisement and check notifyLost callback won't be triggered.
+        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+        final ByteBuffer na = NeighborAdvertisement.build(ROUTER_MAC /* srcMac */,
+                ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
+                ns.ipv6Hdr.srcIp /* dstIp */, flag, ROUTER_LINK_LOCAL /* target */);
+        mPacketReader.sendResponse(na);
+        assertNeverNotifyNeighborLost();
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_mcastResoclicitProbeFailed() throws Exception {
+        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+
+        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
+        int expectedSize = MIN_NUD_SOLICIT_NUM + NUD_MCAST_RESOLICIT_NUM;
+        assertEquals(expectedSize, nsList.size()); // 5 unicast NSes + 3 multicast NSes
+        for (NeighborSolicitation ns : nsList.subList(0, MIN_NUD_SOLICIT_NUM)) {
+            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
+                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+        }
+        for (NeighborSolicitation ns : nsList.subList(MIN_NUD_SOLICIT_NUM, nsList.size())) {
+            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
+        }
+        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_mcastResoclicitProbeReachableWithSameLinkLayerAddress()
+            throws Exception {
+        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+
+        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
+                ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+
+        // Reply Neighbor Advertisement and check notifyLost callback won't be triggered.
+        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+        final ByteBuffer na = NeighborAdvertisement.build(ROUTER_MAC /* srcMac */,
+                ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
+                ns.ipv6Hdr.srcIp /* dstIp */, flag, ROUTER_LINK_LOCAL /* target */);
+        mPacketReader.sendResponse(na);
+        assertNeverNotifyNeighborLost();
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_mcastResoclicitProbeReachableWithDiffLinkLayerAddress()
+            throws Exception {
+        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+
+        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
+                ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+
+        // Reply Neighbor Advertisement with a different link-layer address and check notifyLost
+        // callback will be triggered. Override flag must be set, which indicates that the
+        // advertisement should override an existing cache entry and update the cached link-layer
+        // address, otherwise, kernel won't transit to REACHABLE state with a different link-layer
+        // address.
+        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+                | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+        final MacAddress newMac = MacAddress.fromString("00:1a:11:22:33:55");
+        final ByteBuffer na = NeighborAdvertisement.build(newMac /* srcMac */,
+                ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
+                ns.ipv6Hdr.srcIp /* dstIp */, flag, ROUTER_LINK_LOCAL /* target */);
+        mPacketReader.sendResponse(na);
+        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */);
+    }
+}
diff --git a/tests/integration/src/android/net/ip/IpClientRootTest.kt b/tests/integration/src/android/net/ip/IpClientRootTest.kt
new file mode 100644
index 0000000..8a99e4f
--- /dev/null
+++ b/tests/integration/src/android/net/ip/IpClientRootTest.kt
@@ -0,0 +1,274 @@
+/*
+ * 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 android.net.ip
+
+import android.Manifest
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.net.IIpMemoryStore
+import android.net.IIpMemoryStoreCallbacks
+import android.net.NetworkStackIpMemoryStore
+import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener
+import android.net.ipmemorystore.NetworkAttributes
+import android.net.ipmemorystore.Status
+import android.net.networkstack.TestNetworkStackServiceClient
+import android.os.Process
+import android.provider.DeviceConfig
+import android.util.ArrayMap
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.DeviceConfigUtils
+import java.lang.System.currentTimeMillis
+import java.net.Inet6Address
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.BeforeClass
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+// Stable AIDL method 5 in INetworkStackConnector is allowTestUid
+private const val ALLOW_TEST_UID_INDEX = 5
+
+/**
+ * Tests for IpClient, run with root access but no signature permissions.
+ *
+ * Tests run from this class interact with the real network stack process and can affect system
+ * state, e.g. by changing flags.
+ * State should be restored at the end of the test, but is not guaranteed if the test process is
+ * terminated during the run.
+ */
+class IpClientRootTest : IpClientIntegrationTestCommon() {
+    companion object {
+        private val TAG = IpClientRootTest::class.java.simpleName
+        private val automation by lazy { InstrumentationRegistry.getInstrumentation().uiAutomation }
+        private lateinit var nsClient: TestNetworkStackServiceClient
+        private lateinit var mStore: NetworkStackIpMemoryStore
+        private val mContext = InstrumentationRegistry.getInstrumentation().getContext()
+
+        private class IpMemoryStoreCallbacks(
+            private val fetchedFuture: CompletableFuture<IIpMemoryStore>
+        ) : IIpMemoryStoreCallbacks.Stub() {
+            override fun onIpMemoryStoreFetched(ipMemoryStore: IIpMemoryStore) {
+                fetchedFuture.complete(ipMemoryStore)
+            }
+            override fun getInterfaceVersion() = IIpMemoryStoreCallbacks.VERSION
+            override fun getInterfaceHash() = IIpMemoryStoreCallbacks.HASH
+        }
+
+        @JvmStatic @BeforeClass
+        fun setUpClass() {
+            // Connect to the NetworkStack only once, as it is relatively expensive (~50ms plus any
+            // polling time waiting for the test UID to be allowed), and there should be minimal
+            // side-effects between tests compared to reconnecting every time.
+            automation.adoptShellPermissionIdentity(Manifest.permission.NETWORK_SETTINGS)
+            try {
+                automation.executeShellCommand("su root service call network_stack " +
+                        "$ALLOW_TEST_UID_INDEX i32 " + Process.myUid())
+                // Connecting to the test service does not require allowing the test UID (binding is
+                // always allowed with NETWORK_SETTINGS permissions on debuggable builds), but
+                // allowing the test UID is required to call any method on the service.
+                nsClient = TestNetworkStackServiceClient.connect()
+                // Wait for oneway call to be processed: unfortunately there is no easy way to wait
+                // for a success callback via the service shell command.
+                // TODO: build a small native util that also waits for the success callback, bundle
+                // it in the test APK, and run it as shell command as root instead.
+                mStore = getIpMemoryStore()
+            } finally {
+                automation.dropShellPermissionIdentity()
+            }
+        }
+
+        private fun getIpMemoryStore(): NetworkStackIpMemoryStore {
+            // Until the test UID is allowed, oneway binder calls will not receive any reply.
+            // Call fetchIpMemoryStore (which has limited side-effects) repeatedly until any call
+            // gets a callback.
+            val limit = currentTimeMillis() + TEST_TIMEOUT_MS
+            val fetchedFuture = CompletableFuture<IIpMemoryStore>()
+            Log.i(TAG, "Starting multiple attempts to fetch IpMemoryStore; failures are expected")
+            while (currentTimeMillis() < limit) {
+                try {
+                    nsClient.fetchIpMemoryStore(IpMemoryStoreCallbacks(fetchedFuture))
+                    // The future may be completed by any previous call to fetchIpMemoryStore.
+                    val ipMemoryStore = fetchedFuture.get(20, TimeUnit.MILLISECONDS)
+                    Log.i(TAG, "Obtained IpMemoryStore: " + ipMemoryStore)
+                    return NetworkStackIpMemoryStore(mContext, ipMemoryStore)
+                } catch (e: TimeoutException) {
+                    // Fall through
+                }
+            }
+            fail("fail to get the IpMemoryStore instance within timeout")
+        }
+
+        @JvmStatic @AfterClass
+        fun tearDownClass() {
+            nsClient.disconnect()
+            automation.adoptShellPermissionIdentity(Manifest.permission.NETWORK_SETTINGS)
+            try {
+                // Reset the test UID as -1.
+                // This may not be called if the test process is terminated before completing,
+                // however this is fine as the test UID is only usable on userdebug builds, and
+                // the system does not reuse UIDs across apps until reboot.
+                automation.executeShellCommand("su root service call network_stack " +
+                        "$ALLOW_TEST_UID_INDEX i32 -1")
+            } finally {
+                automation.dropShellPermissionIdentity()
+            }
+        }
+    }
+
+    private val originalFlagValues = ArrayMap<String, String>()
+
+    /**
+     * Wrapper class for IIpClientCallbacks.
+     *
+     * Used to delegate method calls to mock interfaces used to verify the calls, while using
+     * real implementations of the binder stub (such as [asBinder]) to properly receive the calls.
+     */
+    private class BinderCbWrapper(base: IIpClientCallbacks) :
+            IIpClientCallbacks.Stub(), IIpClientCallbacks by base {
+        // asBinder is implemented by both base class and delegate: specify explicitly
+        override fun asBinder() = super.asBinder()
+    }
+
+    @After
+    fun tearDownFlags() {
+        if (testSkipped()) return
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        try {
+            for ((key, value) in originalFlagValues.entries) {
+                if (key == null) continue
+                DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, key,
+                        value, false /* makeDefault */)
+            }
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+    }
+
+    @After
+    fun tearDownIpMemoryStore() {
+        if (testSkipped()) return
+        val latch = CountDownLatch(1)
+
+        // Delete the IpMemoryStore entry corresponding to TEST_L2KEY, make sure each test starts
+        // from a clean state.
+        mStore.delete(TEST_L2KEY, true) { _, _ -> latch.countDown() }
+        assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+    }
+
+    override fun useNetworkStackSignature() = false
+
+    override fun makeIIpClient(ifaceName: String, cbMock: IIpClientCallbacks): IIpClient {
+        val ipClientCaptor = ArgumentCaptor.forClass(IIpClient::class.java)
+        // Older versions of NetworkStack do not clear the calling identity when creating IpClient,
+        // so READ_DEVICE_CONFIG is required to initialize it (b/168577898).
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG)
+        try {
+            nsClient.makeIpClient(ifaceName, BinderCbWrapper(cbMock))
+            verify(cbMock, timeout(TEST_TIMEOUT_MS)).onIpClientCreated(ipClientCaptor.capture())
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+        return ipClientCaptor.value
+    }
+
+    override fun setFeatureEnabled(feature: String, enabled: Boolean) {
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        try {
+            // Do not use computeIfAbsent as it would overwrite null values (flag originally unset)
+            if (!originalFlagValues.containsKey(feature)) {
+                originalFlagValues[feature] =
+                        DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature)
+            }
+            // The feature is enabled if the flag is lower than the package version.
+            // Package versions follow a standard format with 9 digits.
+            // TODO: consider resetting flag values on reboot when set to special values like "1" or
+            // "999999999"
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature,
+                    if (enabled) "1" else "999999999", false)
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+    }
+
+    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        try {
+            return DeviceConfigUtils.isFeatureEnabled(mContext, DeviceConfig.NAMESPACE_CONNECTIVITY,
+                    name, defaultEnabled)
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+    }
+
+    private class TestAttributesRetrievedListener : OnNetworkAttributesRetrievedListener {
+        private val future = CompletableFuture<NetworkAttributes?>()
+        override fun onNetworkAttributesRetrieved(
+            status: Status,
+            key: String,
+            attr: NetworkAttributes?
+        ) {
+            // NetworkAttributes associated to specific l2key retrieved from IpMemoryStore might be
+            // null according to testcase context, hence, make sure the callback is triggered with
+            // success and the l2key param return from callback matches, which also prevents the
+            // case that the NetworkAttributes haven't been stored within CompletableFuture polling
+            // timeout.
+            if (key != TEST_L2KEY || status.resultCode != Status.SUCCESS) {
+                fail("retrieved the network attributes associated to L2Key: " + key +
+                        " status: " + status.resultCode + " attributes: " + attr)
+            }
+            future.complete(attr)
+        }
+
+        fun getBlockingNetworkAttributes(timeout: Long): NetworkAttributes? {
+            return future.get(timeout, TimeUnit.MILLISECONDS)
+        }
+    }
+
+    override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes {
+        val listener = TestAttributesRetrievedListener()
+        mStore.retrieveNetworkAttributes(l2Key, listener)
+        val na = listener.getBlockingNetworkAttributes(timeout)
+        assertNotNull(na)
+        return na
+    }
+
+    override fun assertIpMemoryNeverStoreNetworkAttributes(l2Key: String, timeout: Long) {
+        val listener = TestAttributesRetrievedListener()
+        mStore.retrieveNetworkAttributes(l2Key, listener)
+        assertNull(listener.getBlockingNetworkAttributes(timeout))
+    }
+
+    override fun assertNotifyNeighborLost(targetIp: Inet6Address) {
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(anyString())
+    }
+
+    override fun assertNeverNotifyNeighborLost() {
+        verify(mCb, never()).onReachabilityLost(anyString())
+    }
+}
diff --git a/tests/integration/src/android/net/networkstack/TestNetworkStackServiceClient.kt b/tests/integration/src/android/net/networkstack/TestNetworkStackServiceClient.kt
new file mode 100644
index 0000000..bc3e5e0
--- /dev/null
+++ b/tests/integration/src/android/net/networkstack/TestNetworkStackServiceClient.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 android.net.networkstack
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY
+import android.net.INetworkStackConnector
+import android.os.IBinder
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.fail
+
+/**
+ * A [NetworkStackClientBase] that binds to [com.android.server.TestNetworkStackService]
+ */
+class TestNetworkStackServiceClient private constructor() : NetworkStackClientBase() {
+    companion object {
+        private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+        private val networkStackVersion by lazy {
+            val component = getNetworkStackComponent()
+            val info = context.packageManager.getPackageInfo(component.packageName, 0 /* flags */)
+            info.longVersionCode
+        }
+
+        /**
+         * Create a [TestNetworkStackServiceClient] and connect it to the NetworkStack.
+         */
+        @JvmStatic
+        fun connect(): TestNetworkStackServiceClient {
+            return TestNetworkStackServiceClient().apply { init() }
+        }
+
+        @JvmStatic
+        fun isSupported(): Boolean {
+            // TestNetworkStackService was introduced in NetworkStack version 301100000.
+            // It is also available at HEAD in development branches, where the version code is
+            // 300000000.
+            return networkStackVersion == 300000000L || networkStackVersion >= 301100000L
+        }
+
+        private fun getNetworkStackComponent(): ComponentName {
+            val connectorIntent = Intent(INetworkStackConnector::class.java.name)
+            return connectorIntent.resolveSystemService(context.packageManager, MATCH_SYSTEM_ONLY)
+                    ?: fail("TestNetworkStackService not found")
+        }
+    }
+
+    private val serviceConnection = object : ServiceConnection {
+        override fun onServiceConnected(name: ComponentName, service: IBinder) {
+            onNetworkStackConnected(INetworkStackConnector.Stub.asInterface(service))
+        }
+
+        override fun onServiceDisconnected(name: ComponentName) = Unit
+    }
+
+    private fun init() {
+        val bindIntent = Intent(INetworkStackConnector::class.java.name + ".Test")
+        bindIntent.component = getNetworkStackComponent()
+        context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)
+    }
+
+    fun disconnect() {
+        InstrumentationRegistry.getInstrumentation().context.unbindService(serviceConnection)
+    }
+}
\ No newline at end of file
diff --git a/tests/integration/src/android/net/util/NetworkStackUtilsIntegrationTest.kt b/tests/integration/src/android/net/util/NetworkStackUtilsIntegrationTest.kt
new file mode 100644
index 0000000..0ec43a5
--- /dev/null
+++ b/tests/integration/src/android/net/util/NetworkStackUtilsIntegrationTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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 android.net.util
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.dhcp.DhcpPacket
+import android.os.HandlerThread
+import android.system.Os
+import android.system.OsConstants.AF_INET
+import android.system.OsConstants.AF_PACKET
+import android.system.OsConstants.ARPHRD_ETHER
+import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.OsConstants.SOCK_NONBLOCK
+import androidx.test.platform.app.InstrumentationRegistry
+import android.system.OsConstants.SOCK_RAW
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.StructTimeval
+import com.android.net.module.util.Ipv6Utils
+import com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST
+import com.android.net.module.util.structs.PrefixInformationOption
+import com.android.testutils.ArpRequestFilter
+import com.android.testutils.ETHER_HEADER_LENGTH
+import com.android.testutils.IPV4_HEADER_LENGTH
+import com.android.testutils.IPv4UdpFilter
+import com.android.testutils.TapPacketReader
+import com.android.testutils.UDP_HEADER_LENGTH
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Before
+import org.junit.Test
+import java.io.FileDescriptor
+import java.net.Inet4Address
+import kotlin.reflect.KClass
+import java.net.Inet6Address
+import java.nio.ByteBuffer
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class NetworkStackUtilsIntegrationTest {
+    private val inst by lazy { InstrumentationRegistry.getInstrumentation() }
+    private val context by lazy { inst.context }
+
+    private val TEST_TIMEOUT_MS = 10_000L
+    private val TEST_MTU = 1500
+    private val TEST_TARGET_IPV4_ADDR = parseNumericAddress("192.0.2.42") as Inet4Address
+    private val TEST_SRC_MAC = MacAddress.fromString("BA:98:76:54:32:10")
+    private val TEST_TARGET_MAC = MacAddress.fromString("01:23:45:67:89:0A")
+    private val TEST_INET6ADDR_1 = parseNumericAddress("2001:db8::1") as Inet6Address
+    private val TEST_INET6ADDR_2 = parseNumericAddress("2001:db8::2") as Inet6Address
+
+    private val readerHandler = HandlerThread(
+            NetworkStackUtilsIntegrationTest::class.java.simpleName)
+    private lateinit var iface: TestNetworkInterface
+    private lateinit var reader: TapPacketReader
+
+    @Before
+    fun setUp() {
+        inst.uiAutomation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS)
+        try {
+            val tnm = context.assertHasService(TestNetworkManager::class)
+            iface = tnm.createTapInterface()
+        } finally {
+            inst.uiAutomation.dropShellPermissionIdentity()
+        }
+        readerHandler.start()
+        reader = TapPacketReader(readerHandler.threadHandler, iface.fileDescriptor.fileDescriptor,
+                1500 /* maxPacketSize */)
+        readerHandler.threadHandler.post { reader.start() }
+    }
+
+    @After
+    fun tearDown() {
+        readerHandler.quitSafely()
+        if (this::iface.isInitialized) iface.fileDescriptor.close()
+    }
+
+    @Test
+    fun testAddArpEntry() {
+        val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+        SocketUtils.bindSocketToInterface(socket, iface.interfaceName)
+
+        NetworkStackUtils.addArpEntry(TEST_TARGET_IPV4_ADDR, TEST_TARGET_MAC, iface.interfaceName,
+                socket)
+
+        // Fake DHCP packet: would not be usable as a DHCP offer (most IPv4 addresses are all-zero,
+        // no gateway or DNS servers, etc).
+        // Using a DHCP packet to replicate actual usage of the API: it is used in DhcpServer to
+        // send packets to clients before their IP address has been assigned.
+        val buffer = DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_BOOTP, 123 /* transactionId */,
+                false /* broadcast */, IPV4_ADDR_ANY /* serverIpAddr */,
+                IPV4_ADDR_ANY /* relayIp */, IPV4_ADDR_ANY /* yourIp */,
+                TEST_TARGET_MAC.toByteArray(), 3600 /* timeout */, IPV4_ADDR_ANY /* netMask */,
+                IPV4_ADDR_ANY /* bcAddr */, emptyList<Inet4Address>() /* gateways */,
+                emptyList<Inet4Address>() /* dnsServers */,
+                IPV4_ADDR_ANY /* dhcpServerIdentifier */, null /* domainName */,
+                null /* hostname */, false /* metered */, 1500 /* mtu */,
+                null /* captivePortalUrl */)
+        // Not using .array as per errorprone "ByteBufferBackingArray" recommendation
+        val originalPacket = buffer.readAsArray()
+
+        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size /* bytesCount */,
+                0 /* flags */, TEST_TARGET_IPV4_ADDR, DhcpPacket.DHCP_CLIENT.toInt() /* port */)
+
+        // Verify the packet was sent to the mac address specified in the ARP entry
+        // Also accept ARP requests, but expect that none is sent before the UDP packet
+        // IPv6 NS may be sent on the interface but will be filtered out
+        val sentPacket = reader.poll(TEST_TIMEOUT_MS, IPv4UdpFilter().or(ArpRequestFilter()))
+                ?: fail("Packet was not sent on the interface")
+
+        val sentTargetAddr = MacAddress.fromBytes(sentPacket.copyOfRange(0, ETHER_ADDR_LEN))
+        assertEquals(TEST_TARGET_MAC, sentTargetAddr, "Destination ethernet address does not match")
+
+        val sentDhcpPacket = sentPacket.copyOfRange(
+                ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH, sentPacket.size)
+
+        assertArrayEquals("Sent packet != original packet", originalPacket, sentDhcpPacket)
+    }
+
+    @Test
+    fun testAttachRaFilter() {
+        val socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6)
+        val ifParams = InterfaceParams.getByName(iface.interfaceName)
+                ?: fail("Could not obtain interface params for ${iface.interfaceName}")
+        val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_IPV6, ifParams.index)
+        Os.bind(socket, socketAddr)
+        Os.setsockoptTimeval(socket, SOL_SOCKET, SO_RCVTIMEO,
+                StructTimeval.fromMillis(TEST_TIMEOUT_MS))
+
+        // Verify that before setting any filter, the socket receives pings
+        val echo = Ipv6Utils.buildEchoRequestPacket(TEST_SRC_MAC, TEST_TARGET_MAC, TEST_INET6ADDR_1,
+                TEST_INET6ADDR_2)
+        reader.sendResponse(echo)
+        echo.rewind()
+        assertNextPacketEquals(socket, echo.readAsArray(), "ICMPv6 echo")
+
+        NetworkStackUtils.attachRaFilter(socket, ARPHRD_ETHER)
+        // Send another echo, then an RA. After setting the filter expect only the RA.
+        echo.rewind()
+        reader.sendResponse(echo)
+        val pio = PrefixInformationOption.build(IpPrefix("2001:db8:1::/64"),
+                0.toByte() /* flags */, 3600 /* validLifetime */, 1800 /* preferredLifetime */)
+        val ra = Ipv6Utils.buildRaPacket(TEST_SRC_MAC, TEST_TARGET_MAC,
+                TEST_INET6ADDR_1 /* routerAddr */, IPV6_ADDR_ALL_NODES_MULTICAST,
+                0.toByte() /* flags */, 1800 /* lifetime */, 0 /* reachableTime */,
+                0 /* retransTimer */, pio)
+        reader.sendResponse(ra)
+        ra.rewind()
+
+        assertNextPacketEquals(socket, ra.readAsArray(), "ICMPv6 RA")
+    }
+
+    private fun assertNextPacketEquals(socket: FileDescriptor, expected: ByteArray, descr: String) {
+        val buffer = ByteArray(TEST_MTU)
+        val readPacket = Os.read(socket, buffer, 0 /* byteOffset */, buffer.size)
+        assertTrue(readPacket > 0, "$descr not received")
+        assertEquals(expected.size, readPacket, "Received packet size does not match for $descr")
+        assertArrayEquals("Received packet != expected $descr",
+                expected, buffer.copyOfRange(0, readPacket))
+    }
+}
+
+private fun ByteBuffer.readAsArray(): ByteArray {
+    val out = ByteArray(remaining())
+    get(out)
+    return out
+}
+
+private fun <T : Any> Context.assertHasService(manager: KClass<T>) = getSystemService(manager.java)
+        ?: fail("Could not find service $manager")
diff --git a/tests/lib/Android.bp b/tests/lib/Android.bp
deleted file mode 100644
index 47b950d..0000000
--- a/tests/lib/Android.bp
+++ /dev/null
@@ -1,56 +0,0 @@
-//
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-java_library {
-    name: "net-tests-utils-multivariant",
-    srcs: [
-        "multivariant/**/*.java",
-        "multivariant/**/*.kt",
-    ],
-    host_supported: true,
-    static_libs: [
-        "kotlin-test",
-        "junit",
-    ],
-}
-
-java_library {
-    name: "net-tests-utils",
-    srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
-        ":net-module-utils-srcs-for-tests",
-    ],
-    defaults: ["lib_mockito_extended"],
-    libs: [
-        "androidx.annotation_annotation",
-    ],
-    static_libs: [
-        "androidx.test.ext.junit",
-        "net-tests-utils-multivariant",
-    ],
-}
-
-java_defaults {
-    name: "lib_mockito_extended",
-    static_libs: [
-        "mockito-target-extended-minus-junit4"
-    ],
-    jni_libs: [
-         "libdexmakerjvmtiagent",
-         "libstaticjvmtiagent",
-    ],
-}
diff --git a/tests/lib/multivariant/com/android/testutils/ConcurrentUtils.kt b/tests/lib/multivariant/com/android/testutils/ConcurrentUtils.kt
deleted file mode 100644
index a365af5..0000000
--- a/tests/lib/multivariant/com/android/testutils/ConcurrentUtils.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import kotlin.system.measureTimeMillis
-
-// For Java usage
-fun durationOf(fn: Runnable) = measureTimeMillis { fn.run() }
-
-fun CountDownLatch.await(timeoutMs: Long): Boolean = await(timeoutMs, TimeUnit.MILLISECONDS)
diff --git a/tests/lib/multivariant/com/android/testutils/ExceptionUtils.java b/tests/lib/multivariant/com/android/testutils/ExceptionUtils.java
deleted file mode 100644
index e7dbed5..0000000
--- a/tests/lib/multivariant/com/android/testutils/ExceptionUtils.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils;
-
-import java.util.function.Supplier;
-
-public class ExceptionUtils {
-    /**
-     * Like a Consumer, but declared to throw an exception.
-     * @param <T>
-     */
-    @FunctionalInterface
-    public interface ThrowingConsumer<T> {
-        void accept(T t) throws Exception;
-    }
-
-    /**
-     * Like a Supplier, but declared to throw an exception.
-     * @param <T>
-     */
-    @FunctionalInterface
-    public interface ThrowingSupplier<T> {
-        T get() throws Exception;
-    }
-
-    /**
-     * Like a Runnable, but declared to throw an exception.
-     */
-    @FunctionalInterface
-    public interface ThrowingRunnable {
-        void run() throws Exception;
-    }
-
-
-    public static <T> Supplier<T> ignoreExceptions(ThrowingSupplier<T> func) {
-        return () -> {
-            try {
-                return func.get();
-            } catch (Exception e) {
-                return null;
-            }
-        };
-    }
-
-    public static Runnable ignoreExceptions(ThrowingRunnable r) {
-        return () -> {
-            try {
-                r.run();
-            } catch (Exception e) {
-            }
-        };
-    }
-}
diff --git a/tests/lib/multivariant/com/android/testutils/FileUtils.kt b/tests/lib/multivariant/com/android/testutils/FileUtils.kt
deleted file mode 100644
index 678f977..0000000
--- a/tests/lib/multivariant/com/android/testutils/FileUtils.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-// This function is private because the 2 is hardcoded here, and is not correct if not called
-// directly from __LINE__ or __FILE__.
-private fun callerStackTrace(): StackTraceElement = try {
-    throw RuntimeException()
-} catch (e: RuntimeException) {
-    e.stackTrace[2] // 0 is here, 1 is get() in __FILE__ or __LINE__
-}
-val __FILE__: String get() = callerStackTrace().fileName
-val __LINE__: Int get() = callerStackTrace().lineNumber
diff --git a/tests/lib/multivariant/com/android/testutils/MiscAsserts.kt b/tests/lib/multivariant/com/android/testutils/MiscAsserts.kt
deleted file mode 100644
index 32c22c2..0000000
--- a/tests/lib/multivariant/com/android/testutils/MiscAsserts.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import com.android.testutils.ExceptionUtils.ThrowingRunnable
-import java.lang.reflect.Modifier
-import kotlin.system.measureTimeMillis
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-
-private const val TAG = "Connectivity unit test"
-
-fun <T> assertEmpty(ts: Array<T>) = ts.size.let { len ->
-    assertEquals(0, len, "Expected empty array, but length was $len")
-}
-
-fun <T> assertLength(expected: Int, got: Array<T>) = got.size.let { len ->
-    assertEquals(expected, len, "Expected array of length $expected, but was $len for $got")
-}
-
-// Bridge method to help write this in Java. If you're writing Kotlin, consider using native
-// kotlin.test.assertFailsWith instead, as that method is reified and inlined.
-fun <T : Exception> assertThrows(expected: Class<T>, block: ThrowingRunnable): T {
-    return assertFailsWith(expected.kotlin) { block.run() }
-}
-
-fun <T : Exception> assertThrows(msg: String, expected: Class<T>, block: ThrowingRunnable): T {
-    return assertFailsWith(expected.kotlin, msg) { block.run() }
-}
-
-fun <T> assertEqualBothWays(o1: T, o2: T) {
-    assertTrue(o1 == o2)
-    assertTrue(o2 == o1)
-}
-
-fun <T> assertNotEqualEitherWay(o1: T, o2: T) {
-    assertFalse(o1 == o2)
-    assertFalse(o2 == o1)
-}
-
-fun assertStringContains(got: String, want: String) {
-    assertTrue(got.contains(want), "$got did not contain \"${want}\"")
-}
-
-fun assertContainsExactly(actual: IntArray, vararg expected: Int) {
-    // IntArray#sorted() returns a list, so it's fine to test with equals()
-    assertEquals(actual.sorted(), expected.sorted(),
-            "$actual does not contain exactly $expected")
-}
-
-fun assertContainsStringsExactly(actual: Array<String>, vararg expected: String) {
-    assertEquals(actual.sorted(), expected.sorted(),
-            "$actual does not contain exactly $expected")
-}
-
-fun <T> assertContainsAll(list: Collection<T>, vararg elems: T) {
-    assertContainsAll(list, elems.asList())
-}
-
-fun <T> assertContainsAll(list: Collection<T>, elems: Collection<T>) {
-    elems.forEach { assertTrue(list.contains(it), "$it not in list") }
-}
-
-fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: Runnable) {
-    assertRunsInAtMost(descr, timeLimit) { fn.run() }
-}
-
-fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: () -> Unit) {
-    val timeTaken = measureTimeMillis(fn)
-    val msg = String.format("%s: took %dms, limit was %dms", descr, timeTaken, timeLimit)
-    assertTrue(timeTaken <= timeLimit, msg)
-}
-
-/**
- * Verifies that the number of nonstatic fields in a java class equals a given count.
- * Note: this is essentially not useful for Kotlin code where fields are not really a thing.
- *
- * This assertion serves as a reminder to update test code around it if fields are added
- * after the test is written.
- * @param count Expected number of nonstatic fields in the class.
- * @param clazz Class to test.
- */
-fun <T> assertFieldCountEquals(count: Int, clazz: Class<T>) {
-    assertEquals(count, clazz.declaredFields.filter {
-        !Modifier.isStatic(it.modifiers) && !Modifier.isTransient(it.modifiers)
-    }.size)
-}
diff --git a/tests/lib/multivariant/com/android/testutils/PacketFilter.kt b/tests/lib/multivariant/com/android/testutils/PacketFilter.kt
deleted file mode 100644
index cd8d6a5..0000000
--- a/tests/lib/multivariant/com/android/testutils/PacketFilter.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.testutils
-
-import java.util.function.Predicate
-
-const val ETHER_TYPE_OFFSET = 12
-const val ETHER_HEADER_LENGTH = 14
-const val IPV4_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 9
-const val IPV4_CHECKSUM_OFFSET = ETHER_HEADER_LENGTH + 10
-const val IPV4_HEADER_LENGTH = 20
-const val IPV4_UDP_OFFSET = ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH
-const val BOOTP_OFFSET = IPV4_UDP_OFFSET + 8
-const val BOOTP_TID_OFFSET = BOOTP_OFFSET + 4
-const val BOOTP_CLIENT_MAC_OFFSET = BOOTP_OFFSET + 28
-const val DHCP_OPTIONS_OFFSET = BOOTP_OFFSET + 240
-
-/**
- * A [Predicate] that matches a [ByteArray] if it contains the specified [bytes] at the specified
- * [offset].
- */
-class OffsetFilter(val offset: Int, vararg val bytes: Byte) : Predicate<ByteArray> {
-    override fun test(packet: ByteArray) =
-            bytes.withIndex().all { it.value == packet[offset + it.index] }
-}
-
-/**
- * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv4 datagram.
- */
-class IPv4UdpFilter : Predicate<ByteArray> {
-    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x00 /* IPv4 */).and(
-            OffsetFilter(IPV4_PROTOCOL_OFFSET, 17 /* UDP */))
-    override fun test(t: ByteArray) = impl.test(t)
-}
-
-/**
- * A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
- */
-class DhcpClientPacketFilter : Predicate<ByteArray> {
-    private val impl = IPv4UdpFilter()
-            .and(OffsetFilter(IPV4_UDP_OFFSET /* source port */, 0x00, 0x44 /* 68 */))
-            .and(OffsetFilter(IPV4_UDP_OFFSET + 2 /* dest port */, 0x00, 0x43 /* 67 */))
-    override fun test(t: ByteArray) = impl.test(t)
-}
-
-/**
- * A [Predicate] that matches a [ByteArray] if it contains a ethernet-encapped DHCP packet that
- * contains the specified option with the specified [bytes] as value.
- */
-class DhcpOptionFilter(val option: Byte, vararg val bytes: Byte) : Predicate<ByteArray> {
-    override fun test(packet: ByteArray): Boolean {
-        val option = findDhcpOption(packet, option) ?: return false
-        return option.contentEquals(bytes)
-    }
-}
-
-/**
- * Find a DHCP option in a packet and return its value, if found.
- */
-fun findDhcpOption(packet: ByteArray, option: Byte): ByteArray? =
-        findOptionOffset(packet, option, DHCP_OPTIONS_OFFSET)?.let {
-            val optionLen = packet[it + 1]
-            return packet.copyOfRange(it + 2 /* type, length bytes */, it + 2 + optionLen)
-        }
-
-private tailrec fun findOptionOffset(packet: ByteArray, option: Byte, searchOffset: Int): Int? {
-    if (packet.size <= searchOffset + 2 /* type, length bytes */) return null
-
-    return if (packet[searchOffset] == option) searchOffset else {
-        val optionLen = packet[searchOffset + 1]
-        findOptionOffset(packet, option, searchOffset + 2 + optionLen)
-    }
-}
diff --git a/tests/lib/multivariant/com/android/testutils/TrackRecord.kt b/tests/lib/multivariant/com/android/testutils/TrackRecord.kt
deleted file mode 100644
index 3cdea12..0000000
--- a/tests/lib/multivariant/com/android/testutils/TrackRecord.kt
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.locks.Condition
-import java.util.concurrent.locks.ReentrantLock
-import kotlin.concurrent.withLock
-
-/**
- * A List that additionally offers the ability to append via the add() method, and to retrieve
- * an element by its index optionally waiting for it to become available.
- */
-interface TrackRecord<E> : List<E> {
-    /**
-     * Adds an element to this queue, waking up threads waiting for one. Returns true, as
-     * per the contract for List.
-     */
-    fun add(e: E): Boolean
-
-    /**
-     * Returns the first element after {@param pos}, possibly blocking until one is available, or
-     * null if no such element can be found within the timeout.
-     * If a predicate is given, only elements matching the predicate are returned.
-     *
-     * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
-     * @param pos the position at which to start polling.
-     * @param predicate an optional predicate to filter elements to be returned.
-     * @return an element matching the predicate, or null if timeout.
-     */
-    fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean = { true }): E?
-}
-
-/**
- * A thread-safe implementation of TrackRecord that is backed by an ArrayList.
- *
- * This class also supports the creation of a read-head for easier single-thread access.
- * Refer to the documentation of {@link ArrayTrackRecord.ReadHead}.
- */
-class ArrayTrackRecord<E> : TrackRecord<E> {
-    private val lock = ReentrantLock()
-    private val condition = lock.newCondition()
-    // Backing store. This stores the elements in this ArrayTrackRecord.
-    private val elements = ArrayList<E>()
-
-    // The list iterator for RecordingQueue iterates over a snapshot of the collection at the
-    // time the operator is created. Because TrackRecord is only ever mutated by appending,
-    // that makes this iterator thread-safe as it sees an effectively immutable List.
-    class ArrayTrackRecordIterator<E>(
-        private val list: ArrayList<E>,
-        start: Int,
-        private val end: Int
-    ) : ListIterator<E> {
-        var index = start
-        override fun hasNext() = index < end
-        override fun next() = list[index++]
-        override fun hasPrevious() = index > 0
-        override fun nextIndex() = index + 1
-        override fun previous() = list[--index]
-        override fun previousIndex() = index - 1
-    }
-
-    // List<E> implementation
-    override val size get() = lock.withLock { elements.size }
-    override fun contains(element: E) = lock.withLock { elements.contains(element) }
-    override fun containsAll(elements: Collection<E>) = lock.withLock {
-        this.elements.containsAll(elements)
-    }
-    override operator fun get(index: Int) = lock.withLock { elements[index] }
-    override fun indexOf(element: E): Int = lock.withLock { elements.indexOf(element) }
-    override fun lastIndexOf(element: E): Int = lock.withLock { elements.lastIndexOf(element) }
-    override fun isEmpty() = lock.withLock { elements.isEmpty() }
-    override fun listIterator(index: Int) = ArrayTrackRecordIterator(elements, index, size)
-    override fun listIterator() = listIterator(0)
-    override fun iterator() = listIterator()
-    override fun subList(fromIndex: Int, toIndex: Int): List<E> = lock.withLock {
-        elements.subList(fromIndex, toIndex)
-    }
-
-    // TrackRecord<E> implementation
-    override fun add(e: E): Boolean {
-        lock.withLock {
-            elements.add(e)
-            condition.signalAll()
-        }
-        return true
-    }
-    override fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean) = lock.withLock {
-        elements.getOrNull(pollForIndexReadLocked(timeoutMs, pos, predicate))
-    }
-
-    // For convenience
-    fun getOrNull(pos: Int, predicate: (E) -> Boolean) = lock.withLock {
-        if (pos < 0 || pos > size) null else elements.subList(pos, size).find(predicate)
-    }
-
-    // Returns the index of the next element whose position is >= pos matching the predicate, if
-    // necessary waiting until such a time that such an element is available, with a timeout.
-    // If no such element is found within the timeout -1 is returned.
-    private fun pollForIndexReadLocked(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean): Int {
-        val deadline = System.currentTimeMillis() + timeoutMs
-        var index = pos
-        do {
-            while (index < elements.size) {
-                if (predicate(elements[index])) return index
-                ++index
-            }
-        } while (condition.await(deadline - System.currentTimeMillis()))
-        return -1
-    }
-
-    /**
-     * Returns a ReadHead over this ArrayTrackRecord. The returned ReadHead is tied to the
-     * current thread.
-     */
-    fun newReadHead() = ReadHead()
-
-    /**
-     * ReadHead is an object that helps users of ArrayTrackRecord keep track of how far
-     * it has read this far in the ArrayTrackRecord. A ReadHead is always associated with
-     * a single instance of ArrayTrackRecord. Multiple ReadHeads can be created and used
-     * on the same instance of ArrayTrackRecord concurrently, and the ArrayTrackRecord
-     * instance can also be used concurrently. ReadHead maintains the current index that is
-     * the next to be read, and calls this the "mark".
-     *
-     * A ReadHead delegates all TrackRecord methods to its associated ArrayTrackRecord, and
-     * inherits its thread-safe properties. However, the additional methods that ReadHead
-     * offers on top of TrackRecord do not share these properties and can only be used by
-     * the thread that created the ReadHead. This is because by construction it does not
-     * make sense to use a ReadHead on multiple threads concurrently (see below for details).
-     *
-     * In a ReadHead, {@link poll(Long, (E) -> Boolean)} works similarly to a LinkedBlockingQueue.
-     * It can be called repeatedly and will return the elements as they arrive.
-     *
-     * Intended usage looks something like this :
-     * val TrackRecord<MyObject> record = ArrayTrackRecord().newReadHead()
-     * Thread().start {
-     *   // do stuff
-     *   record.add(something)
-     *   // do stuff
-     * }
-     *
-     * val obj1 = record.poll(timeout)
-     * // do something with obj1
-     * val obj2 = record.poll(timeout)
-     * // do something with obj2
-     *
-     * The point is that the caller does not have to track the mark like it would have to if
-     * it was using ArrayTrackRecord directly.
-     *
-     * Note that if multiple threads were using poll() concurrently on the same ReadHead, what
-     * happens to the mark and the return values could be well defined, but it could not
-     * be useful because there is no way to provide either a guarantee not to skip objects nor
-     * a guarantee about the mark position at the exit of poll(). This is even more true in the
-     * presence of a predicate to filter returned elements, because one thread might be
-     * filtering out the events the other is interested in.
-     * Instead, this use case is supported by creating multiple ReadHeads on the same instance
-     * of ArrayTrackRecord. Each ReadHead is then guaranteed to see all events always and
-     * guarantees are made on the value of the mark upon return. {@see poll(Long, (E) -> Boolean)}
-     * for details. Be careful to create each ReadHead on the thread it is meant to be used on.
-     *
-     * Users of a ReadHead can ask for the current position of the mark at any time. This mark
-     * can be used later to replay the history of events either on this ReadHead, on the associated
-     * ArrayTrackRecord or on another ReadHead associated with the same ArrayTrackRecord. It
-     * might look like this in the reader thread :
-     *
-     * val markAtStart = record.mark
-     * // Start processing interesting events
-     * while (val element = record.poll(timeout) { it.isInteresting() }) {
-     *   // Do something with element
-     * }
-     * // Look for stuff that happened while searching for interesting events
-     * val firstElementReceived = record.getOrNull(markAtStart)
-     * val firstSpecialElement = record.getOrNull(markAtStart) { it.isSpecial() }
-     * // Get the first special element since markAtStart, possibly blocking until one is available
-     * val specialElement = record.poll(timeout, markAtStart) { it.isSpecial() }
-     */
-    inner class ReadHead : TrackRecord<E> by this@ArrayTrackRecord {
-        private val owningThread = Thread.currentThread()
-        private var readHead = 0
-
-        /**
-         * @return the current value of the mark.
-         */
-        var mark
-            get() = readHead.also { checkThread() }
-            set(v: Int) = rewind(v)
-        fun rewind(v: Int) {
-            checkThread()
-            readHead = v
-        }
-
-        private fun checkThread() = check(Thread.currentThread() == owningThread) {
-            "Must be called by the thread that created this object"
-        }
-
-        /**
-         * Returns the first element after the mark, optionally blocking until one is available, or
-         * null if no such element can be found within the timeout.
-         * If a predicate is given, only elements matching the predicate are returned.
-         *
-         * Upon return the mark will be set to immediately after the returned element, or after
-         * the last element in the queue if null is returned. This means this method will always
-         * skip elements that do not match the predicate, even if it returns null.
-         *
-         * This method can only be used by the thread that created this ManagedRecordingQueue.
-         * If used on another thread, this throws IllegalStateException.
-         *
-         * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
-         * @param predicate an optional predicate to filter elements to be returned.
-         * @return an element matching the predicate, or null if timeout.
-         */
-        fun poll(timeoutMs: Long, predicate: (E) -> Boolean = { true }): E? {
-            checkThread()
-            lock.withLock {
-                val index = pollForIndexReadLocked(timeoutMs, readHead, predicate)
-                readHead = if (index < 0) size else index + 1
-                return getOrNull(index)
-            }
-        }
-
-        /**
-         * Returns the first element after the mark or null. This never blocks.
-         *
-         * This method can only be used by the thread that created this ManagedRecordingQueue.
-         * If used on another thread, this throws IllegalStateException.
-         */
-        fun peek(): E? = getOrNull(readHead).also { checkThread() }
-    }
-}
-
-// Private helper
-private fun Condition.await(timeoutMs: Long) = this.await(timeoutMs, TimeUnit.MILLISECONDS)
diff --git a/tests/lib/src/com/android/testutils/ConcurrentIntepreter.kt b/tests/lib/src/com/android/testutils/ConcurrentIntepreter.kt
deleted file mode 100644
index 25b1e0f..0000000
--- a/tests/lib/src/com/android/testutils/ConcurrentIntepreter.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-package com.android.testutils
-
-import android.os.SystemClock
-import java.util.concurrent.CyclicBarrier
-import kotlin.system.measureTimeMillis
-import kotlin.test.assertEquals
-import kotlin.test.assertFails
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-
-// The table contains pairs associating a regexp with the code to run. The statement is matched
-// against each matcher in sequence and when a match is found the associated code is run, passing
-// it the TrackRecord under test and the result of the regexp match.
-typealias InterpretMatcher<T> = Pair<Regex, (ConcurrentIntepreter<T>, T, MatchResult) -> Any?>
-
-// The default unit of time for interpreted tests
-val INTERPRET_TIME_UNIT = 40L // ms
-
-/**
- * A small interpreter for testing parallel code. The interpreter will read a list of lines
- * consisting of "|"-separated statements. Each column runs in a different concurrent thread
- * and all threads wait for each other in between lines. Each statement is split on ";" then
- * matched with regular expressions in the instructionTable constant, which contains the
- * code associated with each statement. The interpreter supports an object being passed to
- * the interpretTestSpec() method to be passed in each lambda (think about the object under
- * test), and an optional transform function to be executed on the object at the start of
- * every thread.
- *
- * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
- * value but can be passed to the constructor. Whitespace is ignored.
- *
- * The interpretation table has to be passed as an argument. It's a table associating a regexp
- * with the code that should execute, as a function taking three arguments : the interpreter,
- * the regexp match, and the object. See the individual tests for the DSL of that test.
- * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
- * constant below for an example of how to write an interpreting table.
- * Some expressions already exist by default and can be used by all interpreters. They include :
- * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
- * 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.
- * EXPR // any text : comments are ignored.
- */
-open class ConcurrentIntepreter<T>(
-    localInterpretTable: List<InterpretMatcher<T>>,
-    val interpretTimeUnit: Long = INTERPRET_TIME_UNIT
-) {
-    private val interpretTable: List<InterpretMatcher<T>> =
-            localInterpretTable + getDefaultInstructions()
-
-    // Split the line into multiple statements separated by ";" and execute them. Return whatever
-    // the last statement returned.
-    fun interpretMultiple(instr: String, r: T): Any? {
-        return instr.split(";").map { interpret(it.trim(), r) }.last()
-    }
-
-    // Match the statement to a regex and interpret it.
-    fun interpret(instr: String, r: T): Any? {
-        val (matcher, code) =
-                interpretTable.find { instr matches it.first } ?: throw SyntaxException(instr)
-        val match = matcher.matchEntire(instr) ?: throw SyntaxException(instr)
-        return code(this, r, match)
-    }
-
-    // Spins as many threads as needed by the test spec and interpret each program concurrently,
-    // having all threads waiting on a CyclicBarrier after each line.
-    // |lineShift| says how many lines after the call the spec starts. This is used for error
-    // reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
-    // than the line at which the expression starts.
-    fun interpretTestSpec(
-        spec: String,
-        initial: T,
-        lineShift: Int = 0,
-        threadTransform: (T) -> T = { it }
-    ) {
-        // For nice stack traces
-        val callSite = getCallingMethod()
-        val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
-        // |threads| contains arrays of strings that make up the statements of a thread : in other
-        // words, it's an array that contains a list of statements for each column in the spec.
-        val threadCount = lines[0].size
-        assertTrue(lines.all { it.size == threadCount })
-        val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
-        val barrier = CyclicBarrier(threadCount)
-        var crash: InterpretException? = null
-        threadInstructions.mapIndexed { threadIndex, instructions ->
-            Thread {
-                val threadLocal = threadTransform(initial)
-                barrier.await()
-                var lineNum = 0
-                instructions.forEach {
-                    if (null != crash) return@Thread
-                    lineNum += 1
-                    try {
-                        interpretMultiple(it, threadLocal)
-                    } catch (e: Throwable) {
-                        // If fail() or some exception was called, the thread will come here ; if
-                        // the exception isn't caught the process will crash, which is not nice for
-                        // testing. Instead, catch the exception, cancel other threads, and report
-                        // nicely. Catch throwable because fail() is AssertionError, which inherits
-                        // from Error.
-                        crash = InterpretException(threadIndex, it,
-                                callSite.lineNumber + lineNum + lineShift,
-                                callSite.className, callSite.methodName, callSite.fileName, e)
-                    }
-                    barrier.await()
-                }
-            }.also { it.start() }
-        }.forEach { it.join() }
-        // If the test failed, crash with line number
-        crash?.let { throw it }
-    }
-
-    // Helper to get the stack trace for a calling method
-    private fun getCallingStackTrace(): Array<StackTraceElement> {
-        try {
-            throw RuntimeException()
-        } catch (e: RuntimeException) {
-            return e.stackTrace
-        }
-    }
-
-    // Find the calling method. This is the first method in the stack trace that is annotated
-    // with @Test.
-    fun getCallingMethod(): StackTraceElement {
-        val stackTrace = getCallingStackTrace()
-        return stackTrace.find { element ->
-            val clazz = Class.forName(element.className)
-            // Because the stack trace doesn't list the formal arguments, find all methods with
-            // this name and return this name if any of them is annotated with @Test.
-            clazz.declaredMethods
-                    .filter { method -> method.name == element.methodName }
-                    .any { method -> method.getAnnotation(org.junit.Test::class.java) != null }
-        } ?: stackTrace[3]
-        // If no method is annotated return the 4th one, because that's what it usually is :
-        // 0 is getCallingStackTrace, 1 is this method, 2 is ConcurrentInterpreter#interpretTestSpec
-    }
-}
-
-private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
-    // Interpret an empty line as doing nothing.
-    Regex("") to { _, _, _ -> null },
-    // Ignore comments.
-    Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
-    // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
-    Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
-        val time = measureTimeMillis { i.interpret(r.strArg(1), t) }
-        assertTrue(time in r.timeArg(2)..r.timeArg(3), "$time not in ${r.timeArg(2)..r.timeArg(3)}")
-    },
-    // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
-    Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
-        i.interpret(r.strArg(1), t).also {
-            if ("null" == r.strArg(2)) assertNull(it) else assertEquals(r.intArg(2), it)
-        }
-    },
-    // Interpret sleep. Optional argument for the count, in INTERPRET_TIME_UNIT units.
-    Regex("""sleep(\((\d+)\))?""") to { i, t, r ->
-        SystemClock.sleep(if (r.strArg(2).isEmpty()) i.interpretTimeUnit else r.timeArg(2))
-    },
-    Regex("""(.*)\s*fails""") to { i, t, r ->
-        assertFails { i.interpret(r.strArg(1), t) }
-    }
-)
-
-class SyntaxException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause)
-class InterpretException(
-    threadIndex: Int,
-    instr: String,
-    lineNum: Int,
-    className: String,
-    methodName: String,
-    fileName: String,
-    cause: Throwable
-) : RuntimeException("Failure: $instr", cause) {
-    init {
-        stackTrace = arrayOf(StackTraceElement(
-                className,
-                "$methodName:thread$threadIndex",
-                fileName,
-                lineNum)) + super.getStackTrace()
-    }
-}
-
-// Some small helpers to avoid to say the large ".groupValues[index].trim()" every time
-fun MatchResult.strArg(index: Int) = this.groupValues[index].trim()
-fun MatchResult.intArg(index: Int) = strArg(index).toInt()
-fun MatchResult.timeArg(index: Int) = INTERPRET_TIME_UNIT * intArg(index)
diff --git a/tests/lib/src/com/android/testutils/DevSdkIgnoreRule.kt b/tests/lib/src/com/android/testutils/DevSdkIgnoreRule.kt
deleted file mode 100644
index 4a83f6f..0000000
--- a/tests/lib/src/com/android/testutils/DevSdkIgnoreRule.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.testutils
-
-import android.os.Build
-import org.junit.Assume.assumeTrue
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Returns true if the development SDK version of the device is in the provided range.
- *
- * If the device is not using a release SDK, the development SDK is considered to be higher than
- * [Build.VERSION.SDK_INT].
- */
-fun isDevSdkInRange(minExclusive: Int?, maxInclusive: Int?): Boolean {
-    // In-development API n+1 will have SDK_INT == n and CODENAME != REL.
-    // Stable API n has SDK_INT == n and CODENAME == REL.
-    val release = "REL" == Build.VERSION.CODENAME
-    val sdkInt = Build.VERSION.SDK_INT
-    val devApiLevel = sdkInt + if (release) 0 else 1
-
-    return (minExclusive == null || devApiLevel > minExclusive) &&
-            (maxInclusive == null || devApiLevel <= maxInclusive)
-}
-
-/**
- * A test rule to ignore tests based on the development SDK level.
- *
- * If the device is not using a release SDK, the development SDK is considered to be higher than
- * [Build.VERSION.SDK_INT].
- *
- * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
- * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
- */
-class DevSdkIgnoreRule @JvmOverloads constructor(
-    private val ignoreClassUpTo: Int? = null,
-    private val ignoreClassAfter: Int? = null
-) : TestRule {
-    override fun apply(base: Statement, description: Description): Statement {
-        return IgnoreBySdkStatement(base, description)
-    }
-
-    /**
-     * Ignore the test for any development SDK that is strictly after [value].
-     *
-     * If the device is not using a release SDK, the development SDK is considered to be higher
-     * than [Build.VERSION.SDK_INT].
-     */
-    annotation class IgnoreAfter(val value: Int)
-
-    /**
-     * Ignore the test for any development SDK that lower than or equal to [value].
-     *
-     * If the device is not using a release SDK, the development SDK is considered to be higher
-     * than [Build.VERSION.SDK_INT].
-     */
-    annotation class IgnoreUpTo(val value: Int)
-
-    private inner class IgnoreBySdkStatement(
-        private val base: Statement,
-        private val description: Description
-    ) : Statement() {
-        override fun evaluate() {
-            val ignoreAfter = description.getAnnotation(IgnoreAfter::class.java)
-            val ignoreUpTo = description.getAnnotation(IgnoreUpTo::class.java)
-
-            val message = "Skipping test for build ${Build.VERSION.CODENAME} " +
-                    "with SDK ${Build.VERSION.SDK_INT}"
-            assumeTrue(message, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
-            assumeTrue(message, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
-            base.evaluate()
-        }
-    }
-}
\ No newline at end of file
diff --git a/tests/lib/src/com/android/testutils/DevSdkIgnoreRunner.kt b/tests/lib/src/com/android/testutils/DevSdkIgnoreRunner.kt
deleted file mode 100644
index 73b2843..0000000
--- a/tests/lib/src/com/android/testutils/DevSdkIgnoreRunner.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.testutils
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
-import org.junit.runner.Description
-import org.junit.runner.Runner
-import org.junit.runner.notification.RunNotifier
-
-/**
- * A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
- *
- * Generally [DevSdkIgnoreRule] should be used for that purpose (using rules is preferable over
- * replacing the test runner), however JUnit runners inspect all methods in the test class before
- * processing test rules. This may cause issues if the test methods are referencing classes that do
- * not exist on the SDK of the device the test is run on.
- *
- * This runner inspects [IgnoreAfter] and [IgnoreUpTo] annotations on the test class, and will skip
- * the whole class if they do not match the development SDK as defined in [DevSdkIgnoreRule].
- * Otherwise, it will delegate to [AndroidJUnit4] to run the test as usual.
- *
- * Example usage:
- *
- *     @RunWith(DevSdkIgnoreRunner::class)
- *     @IgnoreUpTo(Build.VERSION_CODES.Q)
- *     class MyTestClass { ... }
- */
-class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner() {
-    private val baseRunner = klass.let {
-        val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
-        val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
-
-        if (isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value)) AndroidJUnit4(klass) else null
-    }
-
-    override fun run(notifier: RunNotifier) {
-        if (baseRunner != null) {
-            baseRunner.run(notifier)
-            return
-        }
-
-        // Report a single, skipped placeholder test for this class, so that the class is still
-        // visible as skipped in test results.
-        notifier.fireTestIgnored(
-                Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
-    }
-
-    override fun getDescription(): Description {
-        return baseRunner?.description ?: Description.createSuiteDescription(klass)
-    }
-
-    override fun testCount(): Int {
-        // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
-        return baseRunner?.testCount() ?: 1
-    }
-}
\ No newline at end of file
diff --git a/tests/lib/src/com/android/testutils/FakeDns.kt b/tests/lib/src/com/android/testutils/FakeDns.kt
deleted file mode 100644
index 825d748..0000000
--- a/tests/lib/src/com/android/testutils/FakeDns.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import android.net.DnsResolver
-import android.net.InetAddresses
-import android.os.Looper
-import android.os.Handler
-import com.android.internal.annotations.GuardedBy
-import java.net.InetAddress
-import java.util.concurrent.Executor
-import org.mockito.invocation.InvocationOnMock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
-
-const val TYPE_UNSPECIFIED = -1
-// TODO: Integrate with NetworkMonitorTest.
-class FakeDns(val mockResolver: DnsResolver) {
-    class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
-        fun match(host: String, type: Int) = hostname.equals(host) && type == type
-    }
-
-    @GuardedBy("answers")
-    val answers = ArrayList<DnsEntry>()
-
-    fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
-        return answers.firstOrNull { it.match(hostname, type) }
-    }
-
-    fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
-        val ans = DnsEntry(hostname, type, generateAnswer(answer))
-        // Replace or remove the existing one.
-        when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
-            -1 -> answers.add(ans)
-            else -> answers[index] = ans
-        }
-    }
-
-    private fun generateAnswer(answer: Array<String>) =
-            answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
-
-    fun startMocking() {
-        // Mock DnsResolver.query() w/o type
-        doAnswer {
-            mockAnswer(it, 1, -1, 3, 5)
-        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
-                any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
-        // Mock DnsResolver.query() w/ type
-        doAnswer {
-            mockAnswer(it, 1, 2, 4, 6)
-        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
-                anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
-        any() /*callback*/)
-    }
-
-    private fun mockAnswer(
-        it: InvocationOnMock,
-        posHos: Int,
-        posType: Int,
-        posExecutor: Int,
-        posCallback: Int
-    ) {
-        val hostname = it.arguments[posHos] as String
-        val executor = it.arguments[posExecutor] as Executor
-        val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
-        var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
-        val answer = getAnswer(hostname, type)
-
-        if (!answer?.addresses.isNullOrEmpty()) {
-            Handler(Looper.getMainLooper()).post({ executor.execute({
-                    callback.onAnswer(answer?.addresses, 0); }) })
-        }
-    }
-
-    /** Clears all entries. */
-    fun clearAll() = synchronized(answers) {
-        answers.clear()
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/HandlerUtils.kt b/tests/lib/src/com/android/testutils/HandlerUtils.kt
deleted file mode 100644
index fa36209..0000000
--- a/tests/lib/src/com/android/testutils/HandlerUtils.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import android.os.ConditionVariable
-import android.os.Handler
-import android.os.HandlerThread
-import java.util.concurrent.Executor
-import kotlin.system.measureTimeMillis
-import kotlin.test.fail
-
-/**
- * Block until the specified Handler or HandlerThread becomes idle, or until timeoutMs has passed.
- */
-fun HandlerThread.waitForIdle(timeoutMs: Int) = threadHandler.waitForIdle(timeoutMs.toLong())
-fun HandlerThread.waitForIdle(timeoutMs: Long) = threadHandler.waitForIdle(timeoutMs)
-fun Handler.waitForIdle(timeoutMs: Int) = waitForIdle(timeoutMs.toLong())
-fun Handler.waitForIdle(timeoutMs: Long) {
-    val cv = ConditionVariable(false)
-    post(cv::open)
-    if (!cv.block(timeoutMs)) {
-        fail("Handler did not become idle after ${timeoutMs}ms")
-    }
-}
-
-/**
- * Block until the given Serial Executor becomes idle, or until timeoutMs has passed.
- */
-fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) {
-    val cv = ConditionVariable()
-    executor.execute(cv::open)
-    if (!cv.block(timeoutMs)) {
-        fail("Executor did not become idle after ${timeoutMs}ms")
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/NetworkStatsUtils.kt b/tests/lib/src/com/android/testutils/NetworkStatsUtils.kt
deleted file mode 100644
index 8324b25..0000000
--- a/tests/lib/src/com/android/testutils/NetworkStatsUtils.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.testutils
-
-import android.net.NetworkStats
-import kotlin.test.assertTrue
-
-@JvmOverloads
-fun orderInsensitiveEquals(
-    leftStats: NetworkStats,
-    rightStats: NetworkStats,
-    compareTime: Boolean = false
-): Boolean {
-    if (leftStats == rightStats) return true
-    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
-        return false
-    }
-
-    // While operations such as add/subtract will preserve empty entries. This will make
-    // the result be hard to verify during test. Remove them before comparing since they
-    // are not really affect correctness.
-    // TODO (b/152827872): Remove empty entries after addition/subtraction.
-    val leftTrimmedEmpty = leftStats.removeEmptyEntries()
-    val rightTrimmedEmpty = rightStats.removeEmptyEntries()
-
-    if (leftTrimmedEmpty.size() != rightTrimmedEmpty.size()) return false
-    val left = NetworkStats.Entry()
-    val right = NetworkStats.Entry()
-    // Order insensitive compare.
-    for (i in 0 until leftTrimmedEmpty.size()) {
-        leftTrimmedEmpty.getValues(i, left)
-        val j: Int = rightTrimmedEmpty.findIndexHinted(left.iface, left.uid, left.set, left.tag,
-                left.metered, left.roaming, left.defaultNetwork, i)
-        if (j == -1) return false
-        rightTrimmedEmpty.getValues(j, right)
-        if (left != right) return false
-    }
-    return true
-}
-
-/**
- * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
- * necessarily the same.
- *
- * @note {@code elapsedRealtime} is not compared by default, given that in test cases that is not
- *       usually used.
- */
-@JvmOverloads
-fun assertNetworkStatsEquals(
-    expected: NetworkStats,
-    actual: NetworkStats,
-    compareTime: Boolean = false
-) {
-    assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
-            "expected: " + expected + " but was: " + actual)
-}
-
-/**
- * Assert that after being parceled then unparceled, {@link NetworkStats} is equal to the original
- * object.
- */
-fun assertParcelingIsLossless(stats: NetworkStats) {
-    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
-}
diff --git a/tests/lib/src/com/android/testutils/ParcelUtils.kt b/tests/lib/src/com/android/testutils/ParcelUtils.kt
deleted file mode 100644
index 5784f7c..0000000
--- a/tests/lib/src/com/android/testutils/ParcelUtils.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import android.os.Parcel
-import android.os.Parcelable
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-/**
- * Return a new instance of `T` after being parceled then unparceled.
- */
-fun <T : Parcelable> parcelingRoundTrip(source: T): T {
-    val creator: Parcelable.Creator<T>
-    try {
-        creator = source.javaClass.getField("CREATOR").get(null) as Parcelable.Creator<T>
-    } catch (e: IllegalAccessException) {
-        fail("Missing CREATOR field: " + e.message)
-    } catch (e: NoSuchFieldException) {
-        fail("Missing CREATOR field: " + e.message)
-    }
-
-    var p = Parcel.obtain()
-    source.writeToParcel(p, /* flags */ 0)
-    p.setDataPosition(0)
-    val marshalled = p.marshall()
-    p = Parcel.obtain()
-    p.unmarshall(marshalled, 0, marshalled.size)
-    p.setDataPosition(0)
-    return creator.createFromParcel(p)
-}
-
-/**
- * Assert that after being parceled then unparceled, `source` is equal to the original
- * object. If a customized equals function is provided, uses the provided one.
- */
-@JvmOverloads
-fun <T : Parcelable> assertParcelingIsLossless(
-    source: T,
-    equals: (T, T) -> Boolean = { a, b -> a == b }
-) {
-    val actual = parcelingRoundTrip(source)
-    assertTrue(equals(source, actual), "Expected $source, but was $actual")
-}
-
-@JvmOverloads
-fun <T : Parcelable> assertParcelSane(
-    obj: T,
-    fieldCount: Int,
-    equals: (T, T) -> Boolean = { a, b -> a == b }
-) {
-    assertFieldCountEquals(fieldCount, obj::class.java)
-    assertParcelingIsLossless(obj, equals)
-}
diff --git a/tests/lib/src/com/android/testutils/TapPacketReader.java b/tests/lib/src/com/android/testutils/TapPacketReader.java
deleted file mode 100644
index e55ed44..0000000
--- a/tests/lib/src/com/android/testutils/TapPacketReader.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.testutils;
-
-import android.net.util.PacketReader;
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.function.Predicate;
-
-import kotlin.Lazy;
-import kotlin.LazyKt;
-
-public class TapPacketReader extends PacketReader {
-    private final FileDescriptor mTapFd;
-    private final ArrayTrackRecord<byte[]> mReceivedPackets = new ArrayTrackRecord<>();
-    private final Lazy<ArrayTrackRecord<byte[]>.ReadHead> mReadHead =
-            LazyKt.lazy(mReceivedPackets::newReadHead);
-
-    public TapPacketReader(Handler h, FileDescriptor tapFd, int maxPacketSize) {
-        super(h, maxPacketSize);
-        mTapFd = tapFd;
-    }
-
-    @Override
-    protected FileDescriptor createFd() {
-        return mTapFd;
-    }
-
-    @Override
-    protected void handlePacket(byte[] recvbuf, int length) {
-        final byte[] newPacket = Arrays.copyOf(recvbuf, length);
-        if (!mReceivedPackets.add(newPacket)) {
-            throw new AssertionError("More than " + Integer.MAX_VALUE + " packets outstanding!");
-        }
-    }
-
-    /**
-     * Get the next packet that was received on the interface.
-     */
-    @Nullable
-    public byte[] popPacket(long timeoutMs) {
-        return mReadHead.getValue().poll(timeoutMs, packet -> true);
-    }
-
-    /**
-     * Get the next packet that was received on the interface and matches the specified filter.
-     */
-    @Nullable
-    public byte[] popPacket(long timeoutMs, @NonNull Predicate<byte[]> filter) {
-        return mReadHead.getValue().poll(timeoutMs, filter::test);
-    }
-
-    public void sendResponse(final ByteBuffer packet) throws IOException {
-        try (FileOutputStream out = new FileOutputStream(mTapFd)) {
-            byte[] packetBytes = new byte[packet.limit()];
-            packet.get(packetBytes);
-            packet.flip();  // So we can reuse it in the future.
-            out.write(packetBytes);
-        }
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/TestNetworkTracker.kt b/tests/lib/src/com/android/testutils/TestNetworkTracker.kt
deleted file mode 100644
index 4bd9ae8..0000000
--- a/tests/lib/src/com/android/testutils/TestNetworkTracker.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.testutils
-
-import android.content.Context
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.LinkAddress
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import android.net.StringNetworkSpecifier
-import android.net.TestNetworkInterface
-import android.net.TestNetworkManager
-import android.os.Binder
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.TimeUnit
-
-/**
- * Create a test network based on a TUN interface.
- *
- * This method will block until the test network is available. Requires
- * [android.Manifest.permission.CHANGE_NETWORK_STATE] and
- * [android.Manifest.permission.MANAGE_TEST_NETWORKS].
- */
-fun initTestNetwork(context: Context, interfaceAddr: LinkAddress, setupTimeoutMs: Long = 10_000L):
-        TestNetworkTracker {
-    val tnm = context.getSystemService(TestNetworkManager::class.java)
-    val iface = tnm.createTunInterface(arrayOf(interfaceAddr))
-    return TestNetworkTracker(context, iface, tnm, setupTimeoutMs)
-}
-
-/**
- * Utility class to create and track test networks.
- *
- * This class is not thread-safe.
- */
-class TestNetworkTracker internal constructor(
-    val context: Context,
-    val iface: TestNetworkInterface,
-    tnm: TestNetworkManager,
-    setupTimeoutMs: Long
-) {
-    private val cm = context.getSystemService(ConnectivityManager::class.java)
-    private val binder = Binder()
-
-    private val networkCallback: NetworkCallback
-    val network: Network
-
-    init {
-        val networkFuture = CompletableFuture<Network>()
-        val networkRequest = NetworkRequest.Builder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                // Test networks do not have NOT_VPN or TRUSTED capabilities by default
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .setNetworkSpecifier(StringNetworkSpecifier(iface.interfaceName))
-                .build()
-        networkCallback = object : NetworkCallback() {
-            override fun onAvailable(network: Network) {
-                networkFuture.complete(network)
-            }
-        }
-        cm.requestNetwork(networkRequest, networkCallback)
-
-        try {
-            tnm.setupTestNetwork(iface.interfaceName, binder)
-            network = networkFuture.get(setupTimeoutMs, TimeUnit.MILLISECONDS)
-        } catch (e: Throwable) {
-            teardown()
-            throw e
-        }
-    }
-
-    fun teardown() {
-        cm.unregisterNetworkCallback(networkCallback)
-    }
-}
\ No newline at end of file
diff --git a/tests/lib/src/com/android/testutils/TestableNetworkCallback.kt b/tests/lib/src/com/android/testutils/TestableNetworkCallback.kt
deleted file mode 100644
index b63e137..0000000
--- a/tests/lib/src/com/android/testutils/TestableNetworkCallback.kt
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.LinkProperties
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
-import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
-import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
-import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
-import com.android.testutils.RecorderCallback.CallbackEntry.Losing
-import com.android.testutils.RecorderCallback.CallbackEntry.Lost
-import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
-import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
-import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
-import kotlin.reflect.KClass
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-object NULL_NETWORK : Network(-1)
-object ANY_NETWORK : Network(-2)
-
-private val Int.capabilityName get() = NetworkCapabilities.capabilityNameOf(this)
-
-open class RecorderCallback private constructor(
-    private val backingRecord: ArrayTrackRecord<CallbackEntry>
-) : NetworkCallback() {
-    public constructor() : this(ArrayTrackRecord())
-    protected constructor(src: RecorderCallback?): this(src?.backingRecord ?: ArrayTrackRecord())
-
-    sealed class CallbackEntry {
-        // To get equals(), hashcode(), componentN() etc for free, the child classes of
-        // this class are data classes. But while data classes can inherit from other classes,
-        // they may only have visible members in the constructors, so they couldn't declare
-        // a constructor with a non-val arg to pass to CallbackEntry. Instead, force all
-        // subclasses to implement a `network' property, which can be done in a data class
-        // constructor by specifying override.
-        abstract val network: Network
-
-        data class Available(override val network: Network) : CallbackEntry()
-        data class CapabilitiesChanged(
-            override val network: Network,
-            val caps: NetworkCapabilities
-        ) : CallbackEntry()
-        data class LinkPropertiesChanged(
-            override val network: Network,
-            val lp: LinkProperties
-        ) : CallbackEntry()
-        data class Suspended(override val network: Network) : CallbackEntry()
-        data class Resumed(override val network: Network) : CallbackEntry()
-        data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
-        data class Lost(override val network: Network) : CallbackEntry()
-        data class Unavailable private constructor(
-            override val network: Network
-        ) : CallbackEntry() {
-            constructor() : this(NULL_NETWORK)
-        }
-        data class BlockedStatus(
-            override val network: Network,
-            val blocked: Boolean
-        ) : CallbackEntry()
-
-        // Convenience constants for expecting a type
-        companion object {
-            @JvmField
-            val AVAILABLE = Available::class
-            @JvmField
-            val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
-            @JvmField
-            val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
-            @JvmField
-            val SUSPENDED = Suspended::class
-            @JvmField
-            val RESUMED = Resumed::class
-            @JvmField
-            val LOSING = Losing::class
-            @JvmField
-            val LOST = Lost::class
-            @JvmField
-            val UNAVAILABLE = Unavailable::class
-            @JvmField
-            val BLOCKED_STATUS = BlockedStatus::class
-        }
-    }
-
-    val history = backingRecord.newReadHead()
-    val mark get() = history.mark
-
-    override fun onAvailable(network: Network) {
-        history.add(Available(network))
-    }
-
-    // PreCheck is not used in the tests today. For backward compatibility with existing tests that
-    // expect the callbacks not to record this, do not listen to PreCheck here.
-
-    override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
-        history.add(CapabilitiesChanged(network, caps))
-    }
-
-    override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
-        history.add(LinkPropertiesChanged(network, lp))
-    }
-
-    override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
-        history.add(BlockedStatus(network, blocked))
-    }
-
-    override fun onNetworkSuspended(network: Network) {
-        history.add(Suspended(network))
-    }
-
-    override fun onNetworkResumed(network: Network) {
-        history.add(Resumed(network))
-    }
-
-    override fun onLosing(network: Network, maxMsToLive: Int) {
-        history.add(Losing(network, maxMsToLive))
-    }
-
-    override fun onLost(network: Network) {
-        history.add(Lost(network))
-    }
-
-    override fun onUnavailable() {
-        history.add(Unavailable())
-    }
-}
-
-private const val DEFAULT_TIMEOUT = 200L // ms
-
-open class TestableNetworkCallback private constructor(
-    src: TestableNetworkCallback?,
-    val defaultTimeoutMs: Long = DEFAULT_TIMEOUT
-) : RecorderCallback(src) {
-    @JvmOverloads
-    constructor(timeoutMs: Long = DEFAULT_TIMEOUT): this(null, timeoutMs)
-
-    fun createLinkedCopy() = TestableNetworkCallback(this, defaultTimeoutMs)
-
-    // The last available network, or null if any network was lost since the last call to
-    // onAvailable. TODO : fix this by fixing the tests that rely on this behavior
-    val lastAvailableNetwork: Network?
-        get() = when (val it = history.lastOrNull { it is Available || it is Lost }) {
-            is Available -> it.network
-            else -> null
-        }
-
-    fun pollForNextCallback(timeoutMs: Long = defaultTimeoutMs): CallbackEntry {
-        return history.poll(timeoutMs) ?: fail("Did not receive callback after ${timeoutMs}ms")
-    }
-
-    // Make open for use in ConnectivityServiceTest which is the only one knowing its handlers.
-    @JvmOverloads
-    open fun assertNoCallback(timeoutMs: Long = defaultTimeoutMs) {
-        val cb = history.poll(timeoutMs)
-        if (null != cb) fail("Expected no callback but got $cb")
-    }
-
-    // Expects a callback of the specified type on the specified network within the timeout.
-    // If no callback arrives, or a different callback arrives, fail. Returns the callback.
-    inline fun <reified T : CallbackEntry> expectCallback(
-        network: Network = ANY_NETWORK,
-        timeoutMs: Long = defaultTimeoutMs
-    ): T = pollForNextCallback(timeoutMs).let {
-        if (it !is T || (ANY_NETWORK !== network && it.network != network)) {
-            fail("Unexpected callback : $it, expected ${T::class} with Network[$network]")
-        } else {
-            it
-        }
-    }
-
-    // Expects a callback of the specified type matching the predicate within the timeout.
-    // Any callback that doesn't match the predicate will be skipped. Fails only if
-    // no matching callback is received within the timeout.
-    inline fun <reified T : CallbackEntry> eventuallyExpect(
-        timeoutMs: Long = defaultTimeoutMs,
-        from: Int = mark,
-        crossinline predicate: (T) -> Boolean = { true }
-    ): T = eventuallyExpectOrNull(timeoutMs, from, predicate).also {
-        assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms")
-    } as T
-
-    // TODO (b/157405399) straighten and unify the method names
-    inline fun <reified T : CallbackEntry> eventuallyExpectOrNull(
-        timeoutMs: Long = defaultTimeoutMs,
-        from: Int = mark,
-        crossinline predicate: (T) -> Boolean = { true }
-    ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
-
-    fun expectCallbackThat(
-        timeoutMs: Long = defaultTimeoutMs,
-        valid: (CallbackEntry) -> Boolean
-    ) = pollForNextCallback(timeoutMs).also { assertTrue(valid(it), "Unexpected callback : $it") }
-
-    fun expectCapabilitiesThat(
-        net: Network,
-        tmt: Long = defaultTimeoutMs,
-        valid: (NetworkCapabilities) -> Boolean
-    ): CapabilitiesChanged {
-        return expectCallback<CapabilitiesChanged>(net, tmt).also {
-            assertTrue(valid(it.caps), "Capabilities don't match expectations ${it.caps}")
-        }
-    }
-
-    fun expectLinkPropertiesThat(
-        net: Network,
-        tmt: Long = defaultTimeoutMs,
-        valid: (LinkProperties) -> Boolean
-    ): LinkPropertiesChanged {
-        return expectCallback<LinkPropertiesChanged>(net, tmt).also {
-            assertTrue(valid(it.lp), "LinkProperties don't match expectations ${it.lp}")
-        }
-    }
-
-    // Expects onAvailable and the callbacks that follow it. These are:
-    // - onSuspended, iff the network was suspended when the callbacks fire.
-    // - onCapabilitiesChanged.
-    // - onLinkPropertiesChanged.
-    // - onBlockedStatusChanged.
-    //
-    // @param network the network to expect the callbacks on.
-    // @param suspended whether to expect a SUSPENDED callback.
-    // @param validated the expected value of the VALIDATED capability in the
-    //        onCapabilitiesChanged callback.
-    // @param tmt how long to wait for the callbacks.
-    fun expectAvailableCallbacks(
-        net: Network,
-        suspended: Boolean = false,
-        validated: Boolean = true,
-        blocked: Boolean = false,
-        tmt: Long = defaultTimeoutMs
-    ) {
-        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
-    // these when there is no user left.
-    fun expectAvailableAndSuspendedCallbacks(
-        net: Network,
-        validated: Boolean,
-        tmt: Long = defaultTimeoutMs
-    ) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
-
-    fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) {
-        expectCallback<BlockedStatus>(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.
-    // TODO: this is likely a bug. Fix it and remove this method.
-    fun expectAvailableDoubleValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
-        val mark = history.mark
-        expectAvailableCallbacks(net, tmt = tmt)
-        val firstCaps = history.poll(tmt, mark) { it is CapabilitiesChanged }
-        assertEquals(firstCaps, expectCallback<CapabilitiesChanged>(net, tmt))
-    }
-
-    // Expects the available callbacks where the onCapabilitiesChanged must not have validated,
-    // then expects another onCapabilitiesChanged that has the validated bit set. This is used
-    // when a network connects and satisfies a callback, and then immediately validates.
-    fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
-        expectAvailableCallbacks(net, validated = false, 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.
-    interface HasNetwork {
-        val network: Network
-    }
-
-    @JvmOverloads
-    open fun <T : CallbackEntry> expectCallback(
-        type: KClass<T>,
-        n: Network?,
-        timeoutMs: Long = defaultTimeoutMs
-    ) = pollForNextCallback(timeoutMs).also {
-        val network = n ?: NULL_NETWORK
-        // TODO : remove this .java access if the tests ever use kotlin-reflect. At the time of
-        // this writing this would be the only use of this library in the tests.
-        assertTrue(type.java.isInstance(it) && it.network == network,
-                "Unexpected callback : $it, expected ${type.java} with Network[$network]")
-    } as T
-
-    @JvmOverloads
-    open fun <T : CallbackEntry> expectCallback(
-        type: KClass<T>,
-        n: HasNetwork?,
-        timeoutMs: Long = defaultTimeoutMs
-    ) = expectCallback(type, n?.network, timeoutMs)
-
-    fun expectAvailableCallbacks(
-        n: HasNetwork,
-        suspended: Boolean,
-        validated: Boolean,
-        blocked: Boolean,
-        timeoutMs: Long
-    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
-
-    fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
-        expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
-    }
-
-    fun expectAvailableCallbacksValidated(n: HasNetwork) {
-        expectAvailableCallbacks(n.network)
-    }
-
-    fun expectAvailableCallbacksValidatedAndBlocked(n: HasNetwork) {
-        expectAvailableCallbacks(n.network, blocked = true)
-    }
-
-    fun expectAvailableCallbacksUnvalidated(n: HasNetwork) {
-        expectAvailableCallbacks(n.network, validated = false)
-    }
-
-    fun expectAvailableCallbacksUnvalidatedAndBlocked(n: HasNetwork) {
-        expectAvailableCallbacks(n.network, validated = false, blocked = true)
-    }
-
-    fun expectAvailableDoubleValidatedCallbacks(n: HasNetwork) {
-        expectAvailableDoubleValidatedCallbacks(n.network, defaultTimeoutMs)
-    }
-
-    fun expectAvailableThenValidatedCallbacks(n: HasNetwork) {
-        expectAvailableThenValidatedCallbacks(n.network, defaultTimeoutMs)
-    }
-
-    @JvmOverloads
-    fun expectLinkPropertiesThat(
-        n: HasNetwork,
-        tmt: Long = defaultTimeoutMs,
-        valid: (LinkProperties) -> Boolean
-    ) = expectLinkPropertiesThat(n.network, tmt, valid)
-
-    @JvmOverloads
-    fun expectCapabilitiesThat(
-        n: HasNetwork,
-        tmt: Long = defaultTimeoutMs,
-        valid: (NetworkCapabilities) -> Boolean
-    ) = expectCapabilitiesThat(n.network, tmt, valid)
-
-    @JvmOverloads
-    fun expectCapabilitiesWith(
-        capability: Int,
-        n: HasNetwork,
-        timeoutMs: Long = defaultTimeoutMs
-    ): NetworkCapabilities {
-        return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps
-    }
-
-    @JvmOverloads
-    fun expectCapabilitiesWithout(
-        capability: Int,
-        n: HasNetwork,
-        timeoutMs: Long = defaultTimeoutMs
-    ): NetworkCapabilities {
-        return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps
-    }
-
-    fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
-        expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/TestableNetworkStatsProvider.kt b/tests/lib/src/com/android/testutils/TestableNetworkStatsProvider.kt
deleted file mode 100644
index a4ef770..0000000
--- a/tests/lib/src/com/android/testutils/TestableNetworkStatsProvider.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.testutils
-
-import android.net.netstats.provider.NetworkStatsProvider
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-private const val DEFAULT_TIMEOUT_MS = 200L
-
-open class TestableNetworkStatsProvider(
-    val defaultTimeoutMs: Long = DEFAULT_TIMEOUT_MS
-) : NetworkStatsProvider() {
-    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()
-    }
-
-    val history = ArrayTrackRecord<CallbackType>().newReadHead()
-    // See ReadHead#mark
-    val mark get() = history.mark
-
-    override fun onRequestStatsUpdate(token: Int) {
-        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))
-    }
-
-    fun expectOnRequestStatsUpdate(token: Int, timeout: Long = defaultTimeoutMs) {
-        assertEquals(CallbackType.OnRequestStatsUpdate(token), history.poll(timeout))
-    }
-
-    fun expectOnSetLimit(iface: String?, quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
-        assertEquals(CallbackType.OnSetLimit(iface, quotaBytes), history.poll(timeout))
-    }
-
-    fun expectOnSetAlert(quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
-        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(timeout))
-    }
-
-    fun pollForNextCallback(timeout: Long = defaultTimeoutMs) =
-        history.poll(timeout) ?: fail("Did not receive callback after ${timeout}ms")
-
-    inline fun <reified T : CallbackType> expectCallback(
-        timeout: Long = defaultTimeoutMs,
-        predicate: (T) -> Boolean = { true }
-    ): T {
-        return pollForNextCallback(timeout).also { assertTrue(it is T && predicate(it)) } as T
-    }
-
-    // Expects a callback of the specified type matching the predicate within the timeout.
-    // Any callback that doesn't match the predicate will be skipped. Fails only if
-    // no matching callback is received within the timeout.
-    // TODO : factorize the code for this with the identical call in TestableNetworkCallback.
-    // There should be a common superclass doing this generically.
-    // TODO : have a better error message to have this fail. Right now the failure when no
-    // matching callback arrives comes from the casting to a non-nullable T.
-    // TODO : in fact, completely removing this method and have clients use
-    // history.poll(timeout, index, predicate) directly might be simpler.
-    inline fun <reified T : CallbackType> eventuallyExpect(
-        timeoutMs: Long = defaultTimeoutMs,
-        from: Int = mark,
-        crossinline predicate: (T) -> Boolean = { true }
-    ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T
-
-    fun drainCallbacks() {
-        history.mark = history.size
-    }
-
-    @JvmOverloads
-    fun assertNoCallback(timeout: Long = defaultTimeoutMs) {
-        val cb = history.poll(timeout)
-        cb?.let { fail("Expected no callback but got $cb") }
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderBinder.kt b/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderBinder.kt
deleted file mode 100644
index 4d9f884..0000000
--- a/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderBinder.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.testutils
-
-import android.net.netstats.provider.INetworkStatsProvider
-import kotlin.test.assertEquals
-import kotlin.test.fail
-
-private const val DEFAULT_TIMEOUT_MS = 200L
-
-open class TestableNetworkStatsProviderBinder : INetworkStatsProvider.Stub() {
-    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()
-    }
-
-    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
-
-    override fun onRequestStatsUpdate(token: Int) {
-        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))
-    }
-
-    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 expectOnSetAlert(quotaBytes: Long) {
-        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(DEFAULT_TIMEOUT_MS))
-    }
-
-    @JvmOverloads
-    fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
-        val cb = history.poll(timeout)
-        cb?.let { fail("Expected no callback but got $cb") }
-    }
-}
diff --git a/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt b/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
deleted file mode 100644
index abce700..0000000
--- a/tests/lib/src/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.testutils
-
-import android.net.NetworkStats
-import android.net.netstats.provider.INetworkStatsProviderCallback
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import kotlin.test.fail
-
-private const val DEFAULT_TIMEOUT_MS = 3000L
-
-open class TestableNetworkStatsProviderCbBinder : INetworkStatsProviderCallback.Stub() {
-    sealed class CallbackType {
-        data class NotifyStatsUpdated(
-            val token: Int,
-            val ifaceStats: NetworkStats,
-            val uidStats: NetworkStats
-        ) : CallbackType()
-        object NotifyLimitReached : CallbackType()
-        object NotifyAlertReached : CallbackType()
-        object Unregister : CallbackType()
-    }
-
-    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
-
-    override fun notifyStatsUpdated(token: Int, ifaceStats: NetworkStats, uidStats: NetworkStats) {
-        history.add(CallbackType.NotifyStatsUpdated(token, ifaceStats, uidStats))
-    }
-
-    override fun notifyLimitReached() {
-        history.add(CallbackType.NotifyLimitReached)
-    }
-
-    override fun notifyAlertReached() {
-        history.add(CallbackType.NotifyAlertReached)
-    }
-
-    override fun unregister() {
-        history.add(CallbackType.Unregister)
-    }
-
-    fun expectNotifyStatsUpdated() {
-        val event = history.poll(DEFAULT_TIMEOUT_MS)
-        assertTrue(event is CallbackType.NotifyStatsUpdated)
-    }
-
-    fun expectNotifyStatsUpdated(ifaceStats: NetworkStats, uidStats: NetworkStats) {
-        val event = history.poll(DEFAULT_TIMEOUT_MS)!!
-        if (event !is CallbackType.NotifyStatsUpdated) {
-            throw Exception("Expected NotifyStatsUpdated callback, but got ${event::class}")
-        }
-        // TODO: verify token.
-        assertNetworkStatsEquals(ifaceStats, event.ifaceStats)
-        assertNetworkStatsEquals(uidStats, event.uidStats)
-    }
-
-    fun expectNotifyLimitReached() =
-            assertEquals(CallbackType.NotifyLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
-
-    fun expectNotifyAlertReached() =
-            assertEquals(CallbackType.NotifyAlertReached, history.poll(DEFAULT_TIMEOUT_MS))
-
-    // Assert there is no callback in current queue.
-    fun assertNoCallback() {
-        val cb = history.poll(0)
-        cb?.let { fail("Expected no callback but got $cb") }
-    }
-}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index f6f4b86..5a6444f 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_defaults {
     name: "NetworkStackTestsDefaults",
     platform_apis: true,
@@ -46,10 +50,14 @@
 // Tests for NetworkStackNext.
 android_test {
     name: "NetworkStackNextTests",
+    target_sdk_version: "current",
+    min_sdk_version: "29",
     srcs: [], // TODO: tests that only apply to the current, non-stable API can be added here
-    test_suites: ["device-tests"],
+    test_suites: ["general-tests"],
+    test_mainline_modules: ["CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex"],
     defaults: ["NetworkStackTestsDefaults"],
     static_libs: ["NetworkStackApiCurrentLib"],
+    compile_multilib: "both", // Workaround for b/147785146 for mainline-presubmit
 }
 
 // Library containing the unit tests. This is used by the coverage test target to pull in the
@@ -62,14 +70,17 @@
     static_libs: ["NetworkStackApiStableLib"],
     visibility: [
         "//packages/modules/NetworkStack/tests/integration",
-        "//frameworks/base/packages/Tethering/tests/integration",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
     ]
 }
 
 android_test {
     name: "NetworkStackTests",
     min_sdk_version: "29",
-    test_suites: ["device-tests", "mts"],
+    target_sdk_version: "30",
+    test_suites: ["general-tests", "mts"],
+    test_mainline_modules: ["CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex"],
     defaults: ["NetworkStackTestsDefaults"],
     static_libs: ["NetworkStackApiStableLib"],
     compile_multilib: "both",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index e3b232b..000863a 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -15,7 +15,6 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.server.networkstack.tests">
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
 
     <!-- DO NOT add privapp permissions here: they are inherited by
          NetworkStackCoverageTests, which is not signed by the platform key,
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index fa1f420..0c9087f 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 cc_library_shared {
     name: "libnetworkstacktestsjni",
     srcs: [
diff --git a/tests/unit/lint-baseline.xml b/tests/unit/lint-baseline.xml
new file mode 100644
index 0000000..0bfcaa9
--- /dev/null
+++ b/tests/unit/lint-baseline.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.NetworkCapabilities()`"
+        errorLine1="    private val EMPTY_CAPABILITIES = NetworkCapabilities()"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt"
+            line="134"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `android.net.NetworkCapabilities()`"
+        errorLine1="    private val VALIDATED_CAPABILITIES = NetworkCapabilities()"
+        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt"
+            line="135"
+            column="42"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `new android.net.NetworkCapabilities`"
+        errorLine1="            new NetworkCapabilities()"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java"
+            line="57"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `new android.net.NetworkCapabilities`"
+        errorLine1="        NetworkCapabilities nc = new NetworkCapabilities();"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java"
+            line="109"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `new android.net.NetworkCapabilities`"
+        errorLine1="        nc = new NetworkCapabilities();"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java"
+            line="117"
+            column="14"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `new android.net.NetworkCapabilities`"
+        errorLine1="        nc = new NetworkCapabilities();"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java"
+            line="123"
+            column="14"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 29): `new android.net.NetworkCapabilities`"
+        errorLine1="        nc = new NetworkCapabilities();"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/NetworkStack/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java"
+            line="129"
+            column="14"/>
+    </issue>
+
+</issues>
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 6e969ec..b6de3a1 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -26,8 +26,8 @@
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_STREAM;
 
-import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.server.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -67,9 +67,9 @@
 
 import com.android.internal.util.HexDump;
 import com.android.net.module.util.Inet4AddressUtils;
+import com.android.net.module.util.NetworkStackConstants;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.server.networkstack.tests.R;
-import com.android.server.util.NetworkStackConstants;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -1056,6 +1056,10 @@
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
     private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
+    private static final byte[] IPV6_SOLICITED_NODE_MULTICAST_ADDRESS = {
+            (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            (byte) 0xff, (byte) 0xab, (byte) 0xcd, (byte) 0xef,
+    };
 
     private static final int ICMP6_TYPE_OFFSET           = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
     private static final int ICMP6_ROUTER_SOLICITATION   = 133;
@@ -1241,6 +1245,14 @@
         put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS);
         assertDrop(program, packet.array());
 
+        // Verify ICMPv6 NA to ff02::2 is dropped
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify ICMPv6 NA to Solicited-Node Multicast is passed
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_SOLICITED_NODE_MULTICAST_ADDRESS);
+        assertPass(program, packet.array());
+
         // Verify ICMPv6 RS to any is dropped
         packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION);
         assertDrop(program, packet.array());
diff --git a/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
index 81685ef..7b0af69 100644
--- a/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -22,7 +22,7 @@
 import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC;
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
index 9d2a630..d0c49d3 100644
--- a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
@@ -55,6 +55,7 @@
 
 import com.android.internal.util.HexDump;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -85,6 +86,7 @@
     // doesn't use == instead of equals when comparing addresses.
     private static final Inet4Address ANY = v4Address("0.0.0.0");
     private static final byte[] TEST_EMPTY_OPTIONS_SKIP_LIST = new byte[0];
+    private static final int TEST_IPV6_ONLY_WAIT_S = 1800; // 30 min
 
     private static final byte[] CLIENT_MAC = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
 
@@ -97,6 +99,11 @@
         DhcpPacket.testOverrideVendorId = "android-dhcp-???";
     }
 
+    @After
+    public void tearDown() {
+        DhcpPacket.testOverrideVendorId = null;
+    }
+
     class TestDhcpPacket extends DhcpPacket {
         private byte mType;
         // TODO: Make this a map of option numbers to bytes instead.
@@ -484,6 +491,53 @@
         assertTrue(dhcpResults.captivePortalApiUrl.length() > 0);
     }
 
+    private void runIPv6OnlyPreferredOption(boolean enabled) throws Exception {
+        // CHECKSTYLE:OFF Generated code
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+                // IP header.
+                "45100158000040004011B5CEC0A80164C0A80102" +
+                // UDP header
+                "004300440144CE63" +
+                // BOOTP header
+                "02010600B8BF41E60000000000000000C0A80102C0A8016400000000" +
+                // MAC address.
+                "22B3614EE01200000000000000000000" +
+                // Server name and padding.
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                // File.
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                // Options
+                "638253633501023604C0A80164330400000E103A04000007083B0400000C4E01" +
+                "04FFFFFF001C04C0A801FF0304C0A801640604C0A801640C0C74657374686F73" +
+                "746E616D651A0205DC" +
+                // Option 108 (0x6c, IPv6-Only preferred option), length 4 (0x04), 1800s
+                "6C0400000708" +
+                // End of options.
+                "FF"));
+        // CHECKSTYLE:ON Generated code
+
+        final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
+                enabled ? TEST_EMPTY_OPTIONS_SKIP_LIST
+                        : new byte[] { DhcpPacket.DHCP_IPV6_ONLY_PREFERRED });
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        assertEquals(offerPacket.mIpv6OnlyWaitTime,
+                enabled ? new Integer(TEST_IPV6_ONLY_WAIT_S) : null);
+    }
+
+    @Test
+    public void testIPv6OnlyPreferredOption() throws Exception {
+        runIPv6OnlyPreferredOption(true /* enabled */);
+    }
+
+    @Test
+    public void testIPv6OnlyPreferredOption_Disable() throws Exception {
+        runIPv6OnlyPreferredOption(false /* enabled */);
+    }
+
     @Test
     public void testBadIpPacket() throws Exception {
         final byte[] packet = HexDump.hexStringToByteArray(
@@ -1066,7 +1120,7 @@
         ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                 DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr,
                 false /* do unicast */, DhcpClient.DEFAULT_REQUESTED_PARAMS,
-                false /* rapid commit */, testHostname);
+                false /* rapid commit */, testHostname, null /* customized DHCP options */);
 
         final byte[] headers = new byte[] {
             // Ethernet header.
diff --git a/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java b/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
index 7d899e0..8ab7404 100644
--- a/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
@@ -20,7 +20,7 @@
 import static android.net.dhcp.DhcpResultsParcelableUtil.fromStableParcelable;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
 
-import static com.android.testutils.MiscAssertsKt.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
 
 import static org.junit.Assert.assertEquals;
 
@@ -48,9 +48,9 @@
     @Before
     public void setUp() {
         mDhcpResults = new DhcpResults();
-        mDhcpResults.ipAddress = new LinkAddress(parseNumericAddress("2001:db8::42"), 64);
+        mDhcpResults.ipAddress = new LinkAddress(parseNumericAddress("192.168.42.19"), 25);
         mDhcpResults.gateway = parseNumericAddress("192.168.42.42");
-        mDhcpResults.dnsServers.add(parseNumericAddress("2001:db8::43"));
+        mDhcpResults.dnsServers.add(parseNumericAddress("8.8.8.8"));
         mDhcpResults.dnsServers.add(parseNumericAddress("192.168.43.43"));
         mDhcpResults.domains = "example.com";
         mDhcpResults.serverAddress = (Inet4Address) parseNumericAddress("192.168.44.44");
@@ -120,4 +120,16 @@
         final DhcpResults unparceled = fromStableParcelable(toStableParcelable(mDhcpResults));
         assertEquals(mDhcpResults, unparceled);
     }
-}
+
+    @Test
+    public void testToString() {
+        final String expected = ""
+                + "android.net.DhcpResultsParcelable{baseConfiguration: IP address 192.168.42.19/25"
+                + " Gateway 192.168.42.42  DNS servers: [ 8.8.8.8 192.168.43.43 ]"
+                + " Domains example.com, leaseDuration: 3600, mtu: 1450,"
+                + " serverAddress: 192.168.44.44, vendorInfo: TEST_VENDOR_INFO,"
+                + " serverHostName: dhcp.example.com,"
+                + " captivePortalApiUrl: https://example.com/testapi}";
+        assertEquals(expected, toStableParcelable(mDhcpResults).toString());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/src/android/net/dhcp/DhcpServerTest.java b/tests/unit/src/android/net/dhcp/DhcpServerTest.java
index d313b94..58f9a36 100644
--- a/tests/unit/src/android/net/dhcp/DhcpServerTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpServerTest.java
@@ -53,12 +53,13 @@
 import android.net.dhcp.DhcpServer.Clock;
 import android.net.dhcp.DhcpServer.Dependencies;
 import android.net.util.SharedLog;
+import android.os.ConditionVariable;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.net.module.util.Inet4AddressUtils;
-import com.android.testutils.HandlerUtilsKt;
+import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
 import org.junit.Before;
@@ -130,11 +131,30 @@
     private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor;
 
     @NonNull
-    private DhcpServer mServer;
+    private MyDhcpServer mServer;
 
     @Nullable
     private String mPrevShareClassloaderProp;
 
+    private class MyDhcpServer extends DhcpServer {
+        private final ConditionVariable mCv = new ConditionVariable(false);
+
+        MyDhcpServer(Context context, String ifName, DhcpServingParams params, SharedLog log,
+                Dependencies deps) {
+            super(context, ifName, params, log, deps);
+        }
+
+        @Override
+        protected void onQuitting() {
+            super.onQuitting();
+            mCv.open();
+        }
+
+        public void waitForShutdown() {
+            assertTrue(mCv.block(TEST_TIMEOUT_MS));
+        }
+    }
+
     private final INetworkStackStatusCallback mAssertSuccessCallback =
             new INetworkStackStatusCallback.Stub() {
         @Override
@@ -167,7 +187,7 @@
 
     private void startServer() throws Exception {
         mServer.start(mAssertSuccessCallback);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
     }
 
     @Before
@@ -183,7 +203,7 @@
         when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME);
         when(mPacketListener.start()).thenReturn(true);
 
-        mServer = new DhcpServer(mContext, TEST_IFACE, makeServingParams(),
+        mServer = new MyDhcpServer(mContext, TEST_IFACE, makeServingParams(),
                 new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps);
     }
 
@@ -191,7 +211,7 @@
     public void tearDown() throws Exception {
         verify(mRepository, never()).addLeaseCallbacks(eq(null));
         mServer.stop(mAssertSuccessCallback);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        mServer.waitForShutdown();
         verify(mPacketListener, times(1)).stop();
     }
 
@@ -205,7 +225,7 @@
     @Test
     public void testStartWithCallbacks() throws Exception {
         mServer.start(mAssertSuccessCallback, mEventCallbacks);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
         verify(mRepository).addLeaseCallbacks(eq(mEventCallbacks));
     }
 
@@ -222,7 +242,7 @@
                 (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                 false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
         mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         assertResponseSentTo(TEST_CLIENT_ADDR);
         final DhcpOfferPacket packet = assertOffer(getPacket());
@@ -241,7 +261,7 @@
                 (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                 false /* broadcast */, INADDR_ANY /* srcIp */, true /* rapidCommit */);
         mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         assertResponseSentTo(TEST_CLIENT_ADDR);
         final DhcpAckPacket packet = assertAck(getPacket());
@@ -260,7 +280,7 @@
                 (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                 false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
         mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         assertResponseSentTo(INADDR_BROADCAST);
         final DhcpNakPacket packet = assertNak(getPacket());
@@ -289,7 +309,7 @@
         request.mHostName = TEST_HOSTNAME;
         request.mRequestedParams = new byte[] { DHCP_HOST_NAME };
         mServer.sendMessage(CMD_RECEIVE_PACKET, request);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         assertResponseSentTo(TEST_CLIENT_ADDR);
         final DhcpAckPacket packet = assertAck(getPacket());
@@ -307,7 +327,7 @@
 
         final DhcpRequestPacket request = makeRequestSelectingPacket();
         mServer.sendMessage(CMD_RECEIVE_PACKET, request);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         assertResponseSentTo(INADDR_BROADCAST);
         final DhcpNakPacket packet = assertNak(getPacket());
@@ -322,7 +342,7 @@
                 TEST_SERVER_ADDR, TEST_CLIENT_ADDR,
                 INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES);
         mServer.sendMessage(CMD_RECEIVE_PACKET, release);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         verify(mRepository, times(1))
                 .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR));
@@ -348,7 +368,7 @@
                 INADDR_ANY /* nextIp */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                 TEST_CLIENT_ADDR /* requestedIp */, TEST_SERVER_ADDR /* serverIdentifier */);
         mServer.sendMessage(CMD_RECEIVE_PACKET, decline);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
 
         verify(mRepository).markAndReleaseDeclinedLease(isNull(), eq(TEST_CLIENT_MAC),
                 eq(TEST_CLIENT_ADDR));
@@ -373,7 +393,7 @@
         params.changePrefixOnDecline = changePrefixOnDecline;
 
         mServer.updateParams(params, mAssertSuccessCallback);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
     }
 
     @Test
@@ -382,7 +402,7 @@
                 eq(TEST_CLIENT_ADDR))).thenReturn(true);
 
         mServer.start(mAssertSuccessCallback, mEventCallbacks);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
         verify(mRepository).addLeaseCallbacks(eq(mEventCallbacks));
 
         // Enable changePrefixOnDecline
@@ -416,7 +436,7 @@
                 (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                 false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
         mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
-        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
         assertResponseSentTo(clientAddr);
         final DhcpOfferPacket packet = assertOffer(getPacket());
         assertMatchesLease(packet, serverAddr, clientAddr, null);
diff --git a/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java b/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
index 6506eba..290fd69 100644
--- a/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
@@ -34,7 +34,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.Inet4AddressUtils;
-import com.android.testutils.MiscAssertsKt;
+import com.android.testutils.MiscAsserts;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -199,7 +199,7 @@
         assertEquals(params.singleClientAddr, parceled.singleClientAddr);
         assertEquals(params.changePrefixOnDecline, parceled.changePrefixOnDecline);
 
-        MiscAssertsKt.assertFieldCountEquals(10, DhcpServingParamsParcel.class);
+        MiscAsserts.assertFieldCountEquals(10, DhcpServingParamsParcel.class);
     }
 
     @Test(expected = InvalidParameterException.class)
diff --git a/tests/unit/src/android/net/ip/ConntrackMonitorTest.java b/tests/unit/src/android/net/ip/ConntrackMonitorTest.java
new file mode 100644
index 0000000..6e9078e
--- /dev/null
+++ b/tests/unit/src/android/net/ip/ConntrackMonitorTest.java
@@ -0,0 +1,334 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
+import static android.net.netlink.ConntrackMessage.Tuple;
+import static android.net.netlink.ConntrackMessage.TupleIpv4;
+import static android.net.netlink.ConntrackMessage.TupleProto;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+
+
+/**
+ * Tests for ConntrackMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMonitorTest {
+    private static final long TIMEOUT_MS = 10_000L;
+
+    @Mock private SharedLog mLog;
+    @Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
+
+    private final HandlerThread mHandlerThread = new HandlerThread(
+            ConntrackMonitorTest.class.getSimpleName());
+
+    // Late init since the handler thread has been started.
+    private Handler mHandler;
+    private TestConntrackMonitor mConntrackMonitor;
+
+    // A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
+    // allows the test to write test packets to the socket pair via [sendMessage].
+    private class TestConntrackMonitor extends ConntrackMonitor {
+        TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+                @NonNull ConntrackEventConsumer cb) {
+            super(h, log, cb);
+
+            mReadFd = new FileDescriptor();
+            mWriteFd = new FileDescriptor();
+            try {
+                Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
+            } catch (ErrnoException e) {
+                fail("Could not create socket pair: " + e);
+            }
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            return mReadFd;
+        }
+
+        private void sendMessage(byte[] msg) {
+            mHandler.post(() -> {
+                try {
+                    NetlinkSocket.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
+                                              TIMEOUT_MS);
+                } catch (ErrnoException | InterruptedIOException e) {
+                    fail("Unable to send netfilter message: " + e);
+                }
+            });
+        }
+
+        private final FileDescriptor mReadFd;
+        private final FileDescriptor mWriteFd;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        // ConntrackMonitor needs to be started from the handler thread.
+        final ConditionVariable initDone = new ConditionVariable();
+        mHandler.post(() -> {
+            TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
+            m.start();
+            mConntrackMonitor = m;
+
+            initDone.open();
+        });
+        if (!initDone.block(TIMEOUT_MS)) {
+            fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quitSafely();
+    }
+
+    public static final String CT_V4NEW_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "8C000000" +      // length = 140
+            "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000019e" +      // nla_value = 0b110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00000078";       // nla_value = 120 (big endian)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @NonNull
+    private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+        final Inet4Address privateIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+        final Inet4Address remoteIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+        final Inet4Address publicIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+        return new ConntrackEvent(
+                (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+                new Tuple(new TupleIpv4(privateIp, remoteIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+                new Tuple(new TupleIpv4(remoteIp, publicIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+                status,
+                timeoutSec);
+    }
+
+    @Test
+    public void testConntrackEventNew() throws Exception {
+        final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+                0x19e /* status */, 120 /* timeoutSec */);
+        mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+    }
+
+    @Test
+    public void testConntrackEventEquals() {
+        final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        assertEquals(event1, event2);
+    }
+
+    @Test
+    public void testConntrackEventNotEquals() {
+        final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+
+        final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, typeNotEqual);
+
+        final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+                null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleOrigNotEqual);
+
+        final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleReplyNotEqual);
+
+        final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+        assertNotEquals(e, statusNotEqual);
+
+        final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+        assertNotEquals(e, timeoutSecNotEqual);
+    }
+
+    @Test
+    public void testToString() {
+        final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+                0x198 /* status */, 120 /* timeoutSec */);
+        final String expected = ""
+                + "ConntrackEvent{"
+                + "msg_type{IPCTNL_MSG_CT_NEW}, "
+                + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+                + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+                + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+                + "timeout_sec{120}}";
+        assertEquals(expected, event.toString());
+    }
+
+    public static final String CT_V4DELETE_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "84000000" +      // length = 132
+            "0201" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+            "0000" +          // flags = 0
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family  = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000039E";       // nla_value = 0b1110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+                              // IPS_DYING (1 << 9)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4DELETE_TCP_BYTES =
+            HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testConntrackEventDelete() throws Exception {
+        final ConntrackEvent expectedEvent =
+                makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+                        0 /* timeoutSec (absent) */);
+        mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+    }
+}
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index e2b8e4f..d86d0bb 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -18,12 +18,15 @@
 
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -48,22 +51,32 @@
 import android.net.MacAddress;
 import android.net.NetworkStackIpMemoryStore;
 import android.net.RouteInfo;
+import android.net.apf.ApfCapabilities;
+import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.ipmemorystore.NetworkAttributes;
 import android.net.metrics.IpConnectivityLog;
 import android.net.shared.InitialConfiguration;
+import android.net.shared.Layer2Information;
 import android.net.shared.ProvisioningConfiguration;
+import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 import android.net.util.InterfaceParams;
+import android.os.Build;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.networkstack.R;
 import com.android.server.NetworkObserver;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService;
 import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService;
-import com.android.testutils.HandlerUtilsKt;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.HandlerUtils;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -73,9 +86,12 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.nio.ByteBuffer;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Random;
 import java.util.Set;
 
 
@@ -85,6 +101,9 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class IpClientTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String VALID = "VALID";
     private static final String INVALID = "INVALID";
     private static final String TEST_IFNAME = "test_wlan0";
@@ -94,6 +113,9 @@
     private static final int TEST_TIMEOUT_MS = 400;
     private static final String TEST_L2KEY = "some l2key";
     private static final String TEST_CLUSTER = "some cluster";
+    private static final String TEST_SSID = "test_ssid";
+    private static final String TEST_BSSID = "00:11:22:33:44:55";
+    private static final String TEST_BSSID2 = "00:1A:11:22:33:44";
 
     private static final String TEST_GLOBAL_ADDRESS = "1234:4321::548d:2db2:4fcf:ef75/64";
     private static final String[] TEST_LOCAL_ADDRESSES = {
@@ -262,7 +284,7 @@
                 lp.getDnsServers().stream().map(InetAddress::getHostAddress)
                         .toArray(String[]::new));
 
-        HandlerUtilsKt.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mCb, never()).onProvisioningFailure(any());
         verify(mIpMemoryStore, never()).storeNetworkAttributes(any(), any(), any());
 
@@ -301,7 +323,7 @@
 
         reset(mCb);
         doIPv6ProvisioningLoss(lp);
-        HandlerUtilsKt.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mCb).onProvisioningFailure(lp);
         verify(mCb).onLinkPropertiesChange(makeEmptyLinkProperties(TEST_IFNAME));
 
@@ -326,11 +348,11 @@
         final IpClient ipc = doProvisioningWithDefaultConfiguration();
         final LinkProperties lp = makeIPv6ProvisionedLinkProperties();
         addIPv4Provisioning(lp);
-        HandlerUtilsKt.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
 
         reset(mCb);
         doIPv6ProvisioningLoss(lp);
-        HandlerUtilsKt.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         if (avoidBadWifi) { // Provisioning failure is expected only when avoidBadWifi is true
             verify(mCb).onProvisioningFailure(lp);
             verify(mCb).onLinkPropertiesChange(makeEmptyLinkProperties(TEST_IFNAME));
@@ -631,6 +653,183 @@
         return out;
     }
 
+    private ApfConfiguration verifyApfFilterCreatedOnStart(IpClient ipc) {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withInitialConfiguration(
+                        conf(links(TEST_LOCAL_ADDRESSES), prefixes(TEST_PREFIXES), ips()))
+                .withApfCapabilities(new ApfCapabilities(
+                        4 /* version */, 4096 /* maxProgramSize */, 4 /* format */))
+                .build();
+
+        ipc.startProvisioning(config);
+        final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
+                ApfConfiguration.class);
+        verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
+                any(), configCaptor.capture(), any(), any());
+
+        return configCaptor.getValue();
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testApfConfiguration_R() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc);
+
+        assertEquals(ApfCapabilities.getApfDrop8023Frames(), config.ieee802_3Filter);
+        assertArrayEquals(ApfCapabilities.getApfEtherTypeBlackList(), config.ethTypeBlackList);
+
+        verify(mResources, never()).getBoolean(R.bool.config_apfDrop802_3Frames);
+        verify(mResources, never()).getIntArray(R.array.config_apfEthTypeDenyList);
+
+        verifyShutdown(ipc);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testApfConfiguration() throws Exception {
+        doReturn(true).when(mResources).getBoolean(R.bool.config_apfDrop802_3Frames);
+        final int[] ethTypeDenyList = new int[] { 0x88A2, 0x88A4 };
+        doReturn(ethTypeDenyList).when(mResources).getIntArray(
+                R.array.config_apfEthTypeDenyList);
+
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc);
+
+        assertTrue(config.ieee802_3Filter);
+        assertArrayEquals(ethTypeDenyList, config.ethTypeBlackList);
+
+        verifyShutdown(ipc);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testApfConfiguration_NoApfDrop8023Frames() throws Exception {
+        doReturn(false).when(mResources).getBoolean(R.bool.config_apfDrop802_3Frames);
+        final int[] ethTypeDenyList = new int[] { 0x88A3, 0x88A5 };
+        doReturn(ethTypeDenyList).when(mResources).getIntArray(
+                R.array.config_apfEthTypeDenyList);
+
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc);
+
+        assertFalse(config.ieee802_3Filter);
+        assertArrayEquals(ethTypeDenyList, config.ethTypeBlackList);
+
+        verifyShutdown(ipc);
+    }
+
+    private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
+        final ByteBuffer payload = ByteBuffer.allocate(14 /* oui + type + data */);
+        final byte[] data = new byte[10];
+        new Random().nextBytes(data);
+        payload.put(new byte[] { 0x00, 0x1A, 0x11 });
+        payload.put((byte) 0x06);
+        payload.put(data);
+
+        final ScanResultInfo.InformationElement ie =
+                new ScanResultInfo.InformationElement(0xdd /* IE id */, payload);
+        return new ScanResultInfo(ssid, bssid, Collections.singletonList(ie));
+    }
+
+    @Test
+    public void testGetInitialBssidOnSOrAbove() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID));
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID2);
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
+                true /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidOnSOrAbove_NullScanReqsultInfo() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID));
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* ScanResultInfo */,
+                true /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidOnSOrAbove_NullBssid() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                null /* bssid */);
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID);
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
+                true /* isAtLeastS */);
+        assertNull(bssid);
+    }
+
+    @Test
+    public void testGetInitialBssidOnSOrAbove_NullLayer2Info() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID);
+        final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
+                true /* isAtLeastS */);
+        assertNull(bssid);
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID2));
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID);
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
+                false /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS_NullLayer2Info() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID);
+        final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
+                false /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS_BrokenInitialBssid() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, "00:11:22:33:44:");
+        final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
+                false /* isAtLeastS */);
+        assertNull(bssid);
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS_BrokenInitialBssidFallback() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID));
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, "00:11:22:33:44:");
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
+                false /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS_NullScanResultInfoFallback() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID));
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* scanResultInfo */,
+                false /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidBeforeS_NullScanResultInfoAndLayer2Info() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */,
+                null /* scanResultInfo */, false /* isAtLeastS */);
+        assertNull(bssid);
+    }
+
     interface Fn<A,B> {
         B call(A a) throws Exception;
     }
diff --git a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
index 5c86757..2e8d184 100644
--- a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
+++ b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
@@ -16,7 +16,14 @@
 
 package android.net.netlink;
 
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static android.net.netlink.NetlinkConstants.NFNL_SUBSYS_CTNETLINK;
+
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import android.system.OsConstants;
@@ -31,13 +38,19 @@
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ConntrackMessageTest {
     private static final boolean USING_LE = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN);
 
+    private short makeCtType(short msgType) {
+        return (short) (NFNL_SUBSYS_CTNETLINK << 8 | (byte) msgType);
+    }
+
     // Example 1: TCP (192.168.43.209, 44333) -> (23.211.13.26, 443)
     public static final String CT_V4UPDATE_TCP_HEX =
             // struct nlmsghdr
@@ -71,6 +84,14 @@
     public static final byte[] CT_V4UPDATE_TCP_BYTES =
             HexEncoding.decode(CT_V4UPDATE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
 
+    private byte[] makeIPv4TimeoutUpdateRequestTcp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                OsConstants.IPPROTO_TCP,
+                (Inet4Address) InetAddress.getByName("192.168.43.209"), 44333,
+                (Inet4Address) InetAddress.getByName("23.211.13.26"), 443,
+                432000);
+    }
+
     // Example 2: UDP (100.96.167.146, 37069) -> (216.58.197.10, 443)
     public static final String CT_V4UPDATE_UDP_HEX =
             // struct nlmsghdr
@@ -104,27 +125,309 @@
     public static final byte[] CT_V4UPDATE_UDP_BYTES =
             HexEncoding.decode(CT_V4UPDATE_UDP_HEX.replaceAll(" ", "").toCharArray(), false);
 
-    @Test
-    public void testConntrackIPv4TcpTimeoutUpdate() throws Exception {
-        assumeTrue(USING_LE);
-
-        final byte[] tcp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
-                OsConstants.IPPROTO_TCP,
-                (Inet4Address) InetAddress.getByName("192.168.43.209"), 44333,
-                (Inet4Address) InetAddress.getByName("23.211.13.26"), 443,
-                432000);
-        assertArrayEquals(CT_V4UPDATE_TCP_BYTES, tcp);
-    }
-
-    @Test
-    public void testConntrackIPv4UdpTimeoutUpdate() throws Exception {
-        assumeTrue(USING_LE);
-
-        final byte[] udp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+    private byte[] makeIPv4TimeoutUpdateRequestUdp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
                 OsConstants.IPPROTO_UDP,
                 (Inet4Address) InetAddress.getByName("100.96.167.146"), 37069,
                 (Inet4Address) InetAddress.getByName("216.58.197.10"), 443,
                 180);
+    }
+
+    @Test
+    public void testConntrackMakeIPv4TcpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
+        assertArrayEquals(CT_V4UPDATE_TCP_BYTES, tcp);
+    }
+
+    @Test
+    public void testConntrackParseIPv4TcpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(tcp);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(80, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_REPLACE | StructNlMsgHdr.NLM_F_REQUEST
+                | StructNlMsgHdr.NLM_F_ACK), hdr.nlmsg_flags);
+        assertEquals(1, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("192.168.43.209"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("23.211.13.26"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 44333, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
+        assertEquals(0 /* absent */, conntrackMessage.status);
+        assertEquals(432000, conntrackMessage.timeoutSec);
+    }
+
+    @Test
+    public void testConntrackMakeIPv4UdpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
         assertArrayEquals(CT_V4UPDATE_UDP_BYTES, udp);
     }
+
+    @Test
+    public void testConntrackParseIPv4UdpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(udp);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(80, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_REPLACE | StructNlMsgHdr.NLM_F_REQUEST
+                | StructNlMsgHdr.NLM_F_ACK), hdr.nlmsg_flags);
+        assertEquals(1, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("100.96.167.146"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("216.58.197.10"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_UDP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 37069, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
+        assertEquals(0 /* absent */, conntrackMessage.status);
+        assertEquals(180, conntrackMessage.timeoutSec);
+    }
+
+    public static final String CT_V4NEW_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "8C000000" +      // length = 140
+            "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "00000198" +      // nla_value = 0b110011000 (big endian)
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00000078";       // nla_value = 120 (big endian)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseCtNew() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_TCP_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        final StructNlMsgHdr hdr = conntrackMessage.getHeader();
+        assertNotNull(hdr);
+        assertEquals(140, hdr.nlmsg_len);
+        assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
+        assertEquals((short) (StructNlMsgHdr.NLM_F_CREATE | StructNlMsgHdr.NLM_F_EXCL),
+                hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
+        assertNotNull(nfmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
+        assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
+        assertEquals((short) 0x1234, nfmsgHdr.res_id);
+
+        assertEquals(InetAddress.parseNumericAddress("192.168.80.12"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 62449, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleReply.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("100.81.179.1"),
+                conntrackMessage.tupleReply.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleReply.protoNum);
+        assertEquals((short) 443, conntrackMessage.tupleReply.srcPort);
+        assertEquals((short) 62449, conntrackMessage.tupleReply.dstPort);
+
+        assertEquals(0x198, conntrackMessage.status);
+        assertEquals(120, conntrackMessage.timeoutSec);
+    }
+
+    @Test
+    public void testParseTruncation() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the truncated message which has been truncated to every
+        // length between 0 and its full length - 1.
+        for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+            final byte[] truncated = Arrays.copyOfRange(CT_V4NEW_TCP_BYTES, 0, len);
+
+            final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+            byteBuffer.order(ByteOrder.nativeOrder());
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+        }
+    }
+
+    @Test
+    public void testParseTruncationWithInvalidByte() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the message which is truncated by invalid bytes. The
+        // message has been truncated to every length between 0 and its full length - 1.
+        for (byte invalid : new byte[]{(byte) 0x00, (byte) 0xff}) {
+            for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+                final byte[] truncated = new byte[CT_V4NEW_TCP_BYTES.length];
+                Arrays.fill(truncated, (byte) invalid);
+                System.arraycopy(CT_V4NEW_TCP_BYTES, 0, truncated, 0, len);
+
+                final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+                byteBuffer.order(ByteOrder.nativeOrder());
+                final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                        OsConstants.NETLINK_NETFILTER);
+            }
+        }
+    }
+
+    // Malformed conntrack messages.
+    public static final String CT_MALFORMED_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG     -->|
+            // CTA_TUPLE_ORIG has no nla_value.
+            "18000000 0001 0006 00000000 00000000   02 00 0000 0400 0180"
+            // nested CTA_TUPLE_IP has no nla_value.
+            + "1C000000 0001 0006 00000000 00000000 02 00 0000 0800 0180 0400 0180"
+            // nested CTA_IP_V4_SRC has no nla_value.
+            + "20000000 0001 0006 00000000 00000000 02 00 0000 0C00 0180 0800 0180 0400 0100"
+            // nested CTA_TUPLE_PROTO has no nla_value.
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG
+            + "30000000 0001 0006 00000000 00000000 02 00 0000 1C00 0180 1400 0180 0800 0100"
+            //                                  -->|
+            + "C0A8500C 0800 0200 8C700874 0400 0280";
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_MALFORMED_BYTES =
+            HexEncoding.decode(CT_MALFORMED_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseMalformation() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_MALFORMED_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        // Expect no crash while parsing the malformed message.
+        int messageCount = 0;
+        while (byteBuffer.remaining() > 0) {
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+            messageCount++;
+        }
+        assertEquals(4, messageCount);
+    }
+
+    @Test
+    public void testToString() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_TCP_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
+        assertNotNull(msg);
+        assertTrue(msg instanceof ConntrackMessage);
+        final ConntrackMessage conntrackMessage = (ConntrackMessage) msg;
+
+        // Bug: "nlmsg_flags{1536(NLM_F_MATCH))" is not correct because StructNlMsgHdr
+        // #stringForNlMsgFlags can't convert all flags (ex: NLM_F_CREATE) and can't distinguish
+        // the flags which have the same value (ex: NLM_F_MATCH <0x200> and NLM_F_EXCL <0x200>).
+        // The flags output string should be "NLM_F_CREATE|NLM_F_EXCL" in this case.
+        // TODO: correct the flag converted string once #stringForNlMsgFlags does.
+        final String expected = ""
+                + "ConntrackMessage{"
+                + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{140}, nlmsg_type{256(IPCTNL_MSG_CT_NEW)}, "
+                + "nlmsg_flags{1536(NLM_F_MATCH))}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "nfgenmsg{NfGenMsg{ nfgen_family{AF_INET}, version{0}, res_id{4660} }}, "
+                + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+                + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+                + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+                + "timeout_sec{120}}";
+        assertEquals(expected, conntrackMessage.toString());
+    }
 }
diff --git a/tests/unit/src/android/net/netlink/InetDiagSocketTest.java b/tests/unit/src/android/net/netlink/InetDiagSocketTest.java
index 3478276..fcc85a2 100644
--- a/tests/unit/src/android/net/netlink/InetDiagSocketTest.java
+++ b/tests/unit/src/android/net/netlink/InetDiagSocketTest.java
@@ -22,6 +22,7 @@
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -249,7 +250,7 @@
     public void testParseInetDiagResponse() throws Exception {
         final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
         assertNotNull(msg);
 
         assertTrue(msg instanceof InetDiagMessage);
diff --git a/tests/unit/src/android/net/netlink/NduseroptMessageTest.java b/tests/unit/src/android/net/netlink/NduseroptMessageTest.java
index 8cde196..b070d61 100644
--- a/tests/unit/src/android/net/netlink/NduseroptMessageTest.java
+++ b/tests/unit/src/android/net/netlink/NduseroptMessageTest.java
@@ -18,6 +18,7 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.NETLINK_ROUTE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -94,7 +95,7 @@
         assertEquals(68, buf.limit());
         buf.order(ByteOrder.nativeOrder());
 
-        NetlinkMessage nlMsg = NetlinkMessage.parse(buf);
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
         assertNotNull(nlMsg);
         assertTrue(nlMsg instanceof NduseroptMessage);
 
@@ -123,7 +124,7 @@
         assertEquals(76, buf.limit());
         buf.order(ByteOrder.nativeOrder());
 
-        NetlinkMessage nlMsg = NetlinkMessage.parse(buf);
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
         assertNotNull(nlMsg);
         assertTrue(nlMsg instanceof NduseroptMessage);
 
@@ -184,13 +185,13 @@
         String hexString = HDR_32BYTE + OPT_PREF64;
         ByteBuffer buf = toBuffer(hexString);
         assertEquals(32, buf.limit());
-        assertNull(NduseroptMessage.parse(toBuffer(hexString)));
+        assertNull(NduseroptMessage.parse(toBuffer(hexString), NETLINK_ROUTE));
 
         // Header claims 32 bytes of options. Buffer ends at end of options with no source address.
         hexString = HDR_32BYTE + OPT_PREF64 + OPT_PREF64;
         buf = toBuffer(hexString);
         assertEquals(48, buf.limit());
-        assertNull(NduseroptMessage.parse(toBuffer(hexString)));
+        assertNull(NduseroptMessage.parse(toBuffer(hexString), NETLINK_ROUTE));
     }
 
     @Test
diff --git a/tests/unit/src/android/net/netlink/NetlinkConstantsTest.java b/tests/unit/src/android/net/netlink/NetlinkConstantsTest.java
new file mode 100644
index 0000000..131feeb
--- /dev/null
+++ b/tests/unit/src/android/net/netlink/NetlinkConstantsTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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 android.net.netlink;
+
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_CTRZERO;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_DYING;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_STATS;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_STATS_CPU;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_GET_UNCONFIRMED;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static android.net.netlink.NetlinkConstants.NFNL_SUBSYS_CTNETLINK;
+import static android.net.netlink.NetlinkConstants.NLMSG_DONE;
+import static android.net.netlink.NetlinkConstants.NLMSG_ERROR;
+import static android.net.netlink.NetlinkConstants.NLMSG_NOOP;
+import static android.net.netlink.NetlinkConstants.NLMSG_OVERRUN;
+import static android.net.netlink.NetlinkConstants.RTM_DELADDR;
+import static android.net.netlink.NetlinkConstants.RTM_DELLINK;
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.RTM_DELROUTE;
+import static android.net.netlink.NetlinkConstants.RTM_DELRULE;
+import static android.net.netlink.NetlinkConstants.RTM_GETADDR;
+import static android.net.netlink.NetlinkConstants.RTM_GETLINK;
+import static android.net.netlink.NetlinkConstants.RTM_GETNEIGH;
+import static android.net.netlink.NetlinkConstants.RTM_GETROUTE;
+import static android.net.netlink.NetlinkConstants.RTM_GETRULE;
+import static android.net.netlink.NetlinkConstants.RTM_NEWADDR;
+import static android.net.netlink.NetlinkConstants.RTM_NEWLINK;
+import static android.net.netlink.NetlinkConstants.RTM_NEWNDUSEROPT;
+import static android.net.netlink.NetlinkConstants.RTM_NEWNEIGH;
+import static android.net.netlink.NetlinkConstants.RTM_NEWROUTE;
+import static android.net.netlink.NetlinkConstants.RTM_NEWRULE;
+import static android.net.netlink.NetlinkConstants.RTM_SETLINK;
+import static android.net.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+import static android.system.OsConstants.NETLINK_NETFILTER;
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetlinkConstantsTest {
+    private static final short UNKNOWN_FAMILY = 1234;
+
+    private short makeCtType(short msgType) {
+        return (short) (NFNL_SUBSYS_CTNETLINK << 8 | (byte) msgType);
+    }
+
+    @Test
+    public void testStringForNlMsgType() {
+        assertEquals("RTM_NEWLINK", stringForNlMsgType(RTM_NEWLINK, NETLINK_ROUTE));
+        assertEquals("RTM_DELLINK", stringForNlMsgType(RTM_DELLINK, NETLINK_ROUTE));
+        assertEquals("RTM_GETLINK", stringForNlMsgType(RTM_GETLINK, NETLINK_ROUTE));
+        assertEquals("RTM_SETLINK", stringForNlMsgType(RTM_SETLINK, NETLINK_ROUTE));
+        assertEquals("RTM_NEWADDR", stringForNlMsgType(RTM_NEWADDR, NETLINK_ROUTE));
+        assertEquals("RTM_DELADDR", stringForNlMsgType(RTM_DELADDR, NETLINK_ROUTE));
+        assertEquals("RTM_GETADDR", stringForNlMsgType(RTM_GETADDR, NETLINK_ROUTE));
+        assertEquals("RTM_NEWROUTE", stringForNlMsgType(RTM_NEWROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_DELROUTE", stringForNlMsgType(RTM_DELROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_GETROUTE", stringForNlMsgType(RTM_GETROUTE, NETLINK_ROUTE));
+        assertEquals("RTM_NEWNEIGH", stringForNlMsgType(RTM_NEWNEIGH, NETLINK_ROUTE));
+        assertEquals("RTM_DELNEIGH", stringForNlMsgType(RTM_DELNEIGH, NETLINK_ROUTE));
+        assertEquals("RTM_GETNEIGH", stringForNlMsgType(RTM_GETNEIGH, NETLINK_ROUTE));
+        assertEquals("RTM_NEWRULE", stringForNlMsgType(RTM_NEWRULE, NETLINK_ROUTE));
+        assertEquals("RTM_DELRULE", stringForNlMsgType(RTM_DELRULE, NETLINK_ROUTE));
+        assertEquals("RTM_GETRULE", stringForNlMsgType(RTM_GETRULE, NETLINK_ROUTE));
+        assertEquals("RTM_NEWNDUSEROPT", stringForNlMsgType(RTM_NEWNDUSEROPT, NETLINK_ROUTE));
+
+        assertEquals("SOCK_DIAG_BY_FAMILY",
+                stringForNlMsgType(SOCK_DIAG_BY_FAMILY, NETLINK_INET_DIAG));
+
+        assertEquals("IPCTNL_MSG_CT_NEW",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_NEW), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_DELETE",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_DELETE), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_CTRZERO",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_CTRZERO), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_STATS_CPU",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_STATS_CPU), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_STATS",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_STATS), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_DYING",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_DYING), NETLINK_NETFILTER));
+        assertEquals("IPCTNL_MSG_CT_GET_UNCONFIRMED",
+                stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_GET_UNCONFIRMED), NETLINK_NETFILTER));
+    }
+
+    @Test
+    public void testStringForNlMsgType_ControlMessage() {
+        for (int family : new int[]{NETLINK_ROUTE, NETLINK_INET_DIAG, NETLINK_NETFILTER}) {
+            assertEquals("NLMSG_NOOP", stringForNlMsgType(NLMSG_NOOP, family));
+            assertEquals("NLMSG_ERROR", stringForNlMsgType(NLMSG_ERROR, family));
+            assertEquals("NLMSG_DONE", stringForNlMsgType(NLMSG_DONE, family));
+            assertEquals("NLMSG_OVERRUN", stringForNlMsgType(NLMSG_OVERRUN, family));
+        }
+    }
+
+    @Test
+    public void testStringForNlMsgType_UnknownFamily() {
+        assertTrue(stringForNlMsgType(RTM_NEWLINK, UNKNOWN_FAMILY).startsWith("unknown"));
+        assertTrue(stringForNlMsgType(SOCK_DIAG_BY_FAMILY, UNKNOWN_FAMILY).startsWith("unknown"));
+        assertTrue(stringForNlMsgType(makeCtType(IPCTNL_MSG_CT_NEW), UNKNOWN_FAMILY)
+                .startsWith("unknown"));
+    }
+}
diff --git a/tests/unit/src/android/net/netlink/NetlinkErrorMessageTest.java b/tests/unit/src/android/net/netlink/NetlinkErrorMessageTest.java
index 44ab605..345622f 100644
--- a/tests/unit/src/android/net/netlink/NetlinkErrorMessageTest.java
+++ b/tests/unit/src/android/net/netlink/NetlinkErrorMessageTest.java
@@ -19,6 +19,7 @@
 import static android.net.netlink.StructNlMsgHdr.NLM_F_ACK;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REPLACE;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.system.OsConstants.NETLINK_ROUTE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -68,7 +69,7 @@
     public void testParseNlmErrorOk() {
         final ByteBuffer byteBuffer = ByteBuffer.wrap(NLM_ERROR_OK);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
         assertNotNull(msg);
         assertTrue(msg instanceof NetlinkErrorMessage);
         final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
diff --git a/tests/unit/src/android/net/netlink/NetlinkSocketTest.java b/tests/unit/src/android/net/netlink/NetlinkSocketTest.java
index 3916578..6a84a85 100644
--- a/tests/unit/src/android/net/netlink/NetlinkSocketTest.java
+++ b/tests/unit/src/android/net/netlink/NetlinkSocketTest.java
@@ -17,21 +17,36 @@
 package android.net.netlink;
 
 import static android.net.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNSPEC;
+import static android.system.OsConstants.EACCES;
 import static android.system.OsConstants.NETLINK_ROUTE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import android.content.Context;
 import android.net.netlink.NetlinkSocket;
 import android.net.netlink.RtNetlinkNeighborMessage;
 import android.net.netlink.StructNlMsgHdr;
+import android.system.ErrnoException;
 import android.system.NetlinkSocketAddress;
 import android.system.Os;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
 import libcore.io.IoUtils;
 
 import org.junit.Test;
@@ -47,7 +62,7 @@
     private final String TAG = "NetlinkSocketTest";
 
     @Test
-    public void testBasicWorkingGetNeighborsQuery() throws Exception {
+    public void testGetNeighborsQuery() throws Exception {
         final FileDescriptor fd = NetlinkSocket.forProto(NETLINK_ROUTE);
         assertNotNull(fd);
 
@@ -63,6 +78,25 @@
         assertNotNull(req);
 
         final long TIMEOUT = 500;
+        final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
+        final int targetSdk =
+                ctx.getPackageManager()
+                        .getApplicationInfo(ctx.getPackageName(), 0)
+                        .targetSdkVersion;
+
+        // Apps targeting an SDK version > S are not allowed to send RTM_GETNEIGH{TBL} messages
+        if (SdkLevel.isAtLeastT() && targetSdk > 31) {
+            try {
+                NetlinkSocket.sendMessage(fd, req, 0, req.length, TIMEOUT);
+                fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms");
+            } catch (ErrnoException e) {
+                // Expected
+                assertEquals(e.errno, EACCES);
+                return;
+            }
+        }
+
+        // Check that apps targeting lower API levels / running on older platforms succeed
         assertEquals(req.length, NetlinkSocket.sendMessage(fd, req, 0, req.length, TIMEOUT));
 
         int neighMessageCount = 0;
@@ -77,7 +111,7 @@
 
             // Verify the messages at least appears minimally reasonable.
             while (response.remaining() > 0) {
-                final NetlinkMessage msg = NetlinkMessage.parse(response);
+                final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
                 assertNotNull(msg);
                 final StructNlMsgHdr hdr = msg.getHeader();
                 assertNotNull(hdr);
@@ -103,4 +137,105 @@
 
         IoUtils.closeQuietly(fd);
     }
+
+    @Test
+    public void testBasicWorkingGetAddrQuery() throws Exception {
+        final FileDescriptor fd = NetlinkSocket.forProto(NETLINK_ROUTE);
+        assertNotNull(fd);
+
+        NetlinkSocket.connectToKernel(fd);
+
+        final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
+        assertNotNull(localAddr);
+        assertEquals(0, localAddr.getGroupsMask());
+        assertTrue(0 != localAddr.getPortId());
+
+        final int testSeqno = 8;
+        final byte[] req = newGetAddrRequest(testSeqno);
+        assertNotNull(req);
+
+        final long timeout = 500;
+        assertEquals(req.length, NetlinkSocket.sendMessage(fd, req, 0, req.length, timeout));
+
+        int addrMessageCount = 0;
+
+        while (true) {
+            ByteBuffer response = NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
+            assertNotNull(response);
+            assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
+            assertEquals(0, response.position());
+            assertEquals(ByteOrder.nativeOrder(), response.order());
+
+            final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(response);
+            assertNotNull(nlmsghdr);
+
+            if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
+                break;
+            }
+
+            assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
+            assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+            assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
+            assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
+            addrMessageCount++;
+
+            final IfaddrMsg ifaMsg = Struct.parse(IfaddrMsg.class, response);
+            assertTrue(
+                    "Non-IP address family: " + ifaMsg.family,
+                    ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
+        }
+
+        assertTrue(addrMessageCount > 0);
+
+        IoUtils.closeQuietly(fd);
+    }
+
+    /** A convenience method to create an RTM_GETADDR request message. */
+    private static byte[] newGetAddrRequest(int seqNo) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + Struct.getSize(RtgenMsg.class);
+        final byte[] bytes = new byte[length];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_len = length;
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETADDR;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+        nlmsghdr.nlmsg_seq = seqNo;
+        nlmsghdr.pack(byteBuffer);
+
+        final RtgenMsg rtgenMsg = new RtgenMsg();
+        rtgenMsg.family = (byte) AF_UNSPEC;
+        rtgenMsg.writeToByteBuffer(byteBuffer);
+
+        return bytes;
+    }
+
+    /** From uapi/linux/rtnetlink.h */
+    private static class RtgenMsg extends Struct {
+        @Field(order = 0, type = Type.U8)
+        public short family;
+    }
+
+    /**
+     * From uapi/linux/ifaddr.h
+     *
+     * Public ensures visibility to Struct class
+     */
+    public static class IfaddrMsg extends Struct {
+        @Field(order = 0, type = Type.U8)
+        public short family;
+
+        @Field(order = 1, type = Type.U8)
+        public short prefixlen;
+
+        @Field(order = 2, type = Type.U8)
+        public short flags;
+
+        @Field(order = 3, type = Type.U8)
+        public short scope;
+
+        @Field(order = 4, type = Type.U32)
+        public long index;
+    }
 }
diff --git a/tests/unit/src/android/net/netlink/NetlinkTestUtils.kt b/tests/unit/src/android/net/netlink/NetlinkTestUtils.kt
index 1bc3ab0..3b485eb 100644
--- a/tests/unit/src/android/net/netlink/NetlinkTestUtils.kt
+++ b/tests/unit/src/android/net/netlink/NetlinkTestUtils.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:JvmName("NetlinkTestUtils")
+
 package android.net.netlink
 
 import android.net.netlink.NetlinkConstants.RTM_DELNEIGH
diff --git a/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java b/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
index 34257b8..a1f1d44 100644
--- a/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
+++ b/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
@@ -16,9 +16,10 @@
 
 package android.net.netlink;
 
-import static android.net.netlink.NetlinkTestUtilsKt.makeDelNeighMessage;
-import static android.net.netlink.NetlinkTestUtilsKt.makeNewNeighMessage;
+import static android.net.netlink.NetlinkTestUtils.makeDelNeighMessage;
+import static android.net.netlink.NetlinkTestUtils.makeNewNeighMessage;
 import static android.net.netlink.StructNdMsg.NUD_STALE;
+import static android.system.OsConstants.NETLINK_ROUTE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -81,7 +82,7 @@
     public void testParseRtmDelNeigh() {
         final ByteBuffer byteBuffer = ByteBuffer.wrap(RTM_DELNEIGH);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
         assertNotNull(msg);
         assertTrue(msg instanceof RtNetlinkNeighborMessage);
         final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
@@ -108,7 +109,7 @@
     public void testParseRtmNewNeigh() {
         final ByteBuffer byteBuffer = ByteBuffer.wrap(RTM_NEWNEIGH);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
         assertNotNull(msg);
         assertTrue(msg instanceof RtNetlinkNeighborMessage);
         final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
@@ -138,7 +139,7 @@
 
         int messageCount = 0;
         while (byteBuffer.remaining() > 0) {
-            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer);
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
             assertNotNull(msg);
             assertTrue(msg instanceof RtNetlinkNeighborMessage);
             final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) msg;
@@ -150,6 +151,13 @@
             assertEquals(0, hdr.nlmsg_seq);
             assertEquals(11070, hdr.nlmsg_pid);
 
+            final int probes = neighMsg.getProbes();
+            assertTrue("Unexpected number of probes. Got " +  probes + ", max=5",
+                    probes < 5);
+            final int ndm_refcnt = neighMsg.getCacheInfo().ndm_refcnt;
+            assertTrue("nda_cacheinfo has unexpectedly high ndm_refcnt: " + ndm_refcnt,
+                    ndm_refcnt < 0x100);
+
             messageCount++;
         }
         // TODO: add more detailed spot checks.
diff --git a/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java b/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java
index a09805e..0f3020d 100644
--- a/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java
+++ b/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java
@@ -20,7 +20,7 @@
 import static android.net.netlink.StructNdOptPref64.plcToPrefixLength;
 import static android.net.netlink.StructNdOptPref64.prefixLengthToPlc;
 
-import static com.android.testutils.MiscAssertsKt.assertThrows;
+import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
diff --git a/tests/unit/src/android/net/netlink/StructNlMsgHdrTest.java b/tests/unit/src/android/net/netlink/StructNlMsgHdrTest.java
new file mode 100644
index 0000000..b44b31d
--- /dev/null
+++ b/tests/unit/src/android/net/netlink/StructNlMsgHdrTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.net.netlink;
+
+import static org.junit.Assert.fail;
+
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNlMsgHdrTest {
+
+    public static final short TEST_NLMSG_LEN = 16;
+    public static final short TEST_NLMSG_FLAGS = StructNlMsgHdr.NLM_F_REQUEST
+            | StructNlMsgHdr.NLM_F_MULTI | StructNlMsgHdr.NLM_F_ACK | StructNlMsgHdr.NLM_F_ECHO;
+    public static final short TEST_NLMSG_SEQ = 1234;
+    public static final short TEST_NLMSG_PID = 5678;
+
+    // Checking the header string nlmsg_{len, ..} of the number can make sure that the checking
+    // number comes from the expected element.
+    // TODO: Verify more flags once StructNlMsgHdr can distinguish the flags which have the same
+    // value. For example, NLM_F_MATCH (0x200) and NLM_F_EXCL (0x200) can't be distinguished.
+    // See StructNlMsgHdrTest#stringForNlMsgFlags.
+    public static final String TEST_NLMSG_LEN_STR = "nlmsg_len{16}";
+    public static final String TEST_NLMSG_FLAGS_STR =
+            "NLM_F_REQUEST|NLM_F_MULTI|NLM_F_ACK|NLM_F_ECHO";
+    public static final String TEST_NLMSG_SEQ_STR = "nlmsg_seq{1234}";
+    public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
+
+    private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+        final StructNlMsgHdr struct = new StructNlMsgHdr();
+        struct.nlmsg_len = TEST_NLMSG_LEN;
+        struct.nlmsg_type = type;
+        struct.nlmsg_flags = TEST_NLMSG_FLAGS;
+        struct.nlmsg_seq = TEST_NLMSG_SEQ;
+        struct.nlmsg_pid = TEST_NLMSG_PID;
+        return struct;
+    }
+
+    private static void assertContains(String actualValue, String expectedSubstring) {
+        if (actualValue.contains(expectedSubstring)) return;
+        fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
+    }
+
+    @Test
+    public void testToString() {
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
+        String s = struct.toString();
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20()}");
+
+        struct = makeStructNlMsgHdr(NetlinkConstants.SOCK_DIAG_BY_FAMILY);
+        s = struct.toString();
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20()}");
+    }
+
+    @Test
+    public void testToStringWithNetlinkFamily() {
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20(RTM_NEWADDR)}");
+
+        struct = makeStructNlMsgHdr(NetlinkConstants.SOCK_DIAG_BY_FAMILY);
+        s = struct.toString(OsConstants.NETLINK_INET_DIAG);
+        assertContains(s, TEST_NLMSG_LEN_STR);
+        assertContains(s, TEST_NLMSG_FLAGS_STR);
+        assertContains(s, TEST_NLMSG_SEQ_STR);
+        assertContains(s, TEST_NLMSG_PID_STR);
+        assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
+    }
+}
diff --git a/tests/unit/src/android/net/shared/InitialConfigurationTest.java b/tests/unit/src/android/net/shared/InitialConfigurationTest.java
index 17f8324..4d5a7df 100644
--- a/tests/unit/src/android/net/shared/InitialConfigurationTest.java
+++ b/tests/unit/src/android/net/shared/InitialConfigurationTest.java
@@ -18,7 +18,7 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 
-import static com.android.testutils.MiscAssertsKt.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
diff --git a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
index 49cea7d..436b81a 100644
--- a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
+++ b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
@@ -19,7 +19,7 @@
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.shared.ProvisioningConfiguration.fromStableParcelable;
 
-import static com.android.testutils.MiscAssertsKt.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -27,8 +27,10 @@
 import android.net.LinkAddress;
 import android.net.MacAddress;
 import android.net.Network;
+import android.net.ProvisioningConfigurationParcelable;
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
+import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 
 import androidx.test.filters.SmallTest;
@@ -39,7 +41,9 @@
 import org.junit.runner.RunWith;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
@@ -62,6 +66,16 @@
                 Collections.singletonList(ie));
     }
 
+    private List<DhcpOption> makeCustomizedDhcpOptions(byte type, final byte[] value) {
+        final DhcpOption option = new DhcpOption();
+        option.type = type;
+        option.value = value;
+
+        final List<DhcpOption> options = new ArrayList<DhcpOption>();
+        options.add(option);
+        return options;
+    }
+
     @Before
     public void setUp() {
         mConfig = new ProvisioningConfiguration();
@@ -86,8 +100,10 @@
         mConfig.mScanResultInfo = makeScanResultInfo("ssid");
         mConfig.mLayer2Info = new Layer2Information("some l2key", "some cluster",
                 MacAddress.fromString("00:01:02:03:04:05"));
+        mConfig.mDhcpOptions = makeCustomizedDhcpOptions((byte) 60,
+                new String("android-dhcp-11").getBytes());
         // Any added field must be included in equals() to be tested properly
-        assertFieldCountEquals(15, ProvisioningConfiguration.class);
+        assertFieldCountEquals(16, ProvisioningConfiguration.class);
     }
 
     @Test
@@ -126,6 +142,12 @@
     }
 
     @Test
+    public void testParcelUnparcel_NullCustomizedDhcpOptions() {
+        mConfig.mDhcpOptions = null;
+        doParcelUnparcelTest();
+    }
+
+    @Test
     public void testParcelUnparcel_WithPreDhcpConnection() {
         mConfig.mEnablePreconnection = true;
         doParcelUnparcelTest();
@@ -170,7 +192,13 @@
         assertNotEqualsAfterChange(c -> c.mLayer2Info = new Layer2Information("some l2key",
                 "some cluster", MacAddress.fromString("01:02:03:04:05:06")));
         assertNotEqualsAfterChange(c -> c.mLayer2Info = null);
-        assertFieldCountEquals(15, ProvisioningConfiguration.class);
+        assertNotEqualsAfterChange(c -> c.mDhcpOptions = new ArrayList<DhcpOption>());
+        assertNotEqualsAfterChange(c -> c.mDhcpOptions = null);
+        assertNotEqualsAfterChange(c -> c.mDhcpOptions = makeCustomizedDhcpOptions((byte) 60,
+                  new String("vendor-class-identifier").getBytes()));
+        assertNotEqualsAfterChange(c -> c.mDhcpOptions = makeCustomizedDhcpOptions((byte) 77,
+                  new String("vendor-class-identifier").getBytes()));
+        assertFieldCountEquals(16, ProvisioningConfiguration.class);
     }
 
     private void assertNotEqualsAfterChange(Consumer<ProvisioningConfiguration> mutator) {
@@ -178,4 +206,33 @@
         mutator.accept(newConfig);
         assertNotEquals(mConfig, newConfig);
     }
+
+    private static final String TEMPLATE = ""
+            + "android.net.ProvisioningConfigurationParcelable{enableIPv4: true, enableIPv6: true,"
+            + " usingMultinetworkPolicyTracker: true,"
+            + " usingIpReachabilityMonitor: true, requestedPreDhcpActionMs: 42,"
+            + " initialConfig: android.net.InitialConfigurationParcelable{ipAddresses:"
+            + " [192.168.42.42/24], directlyConnectedRoutes: [], dnsServers: [], gateway: null},"
+            + " staticIpConfig: IP address 2001:db8::42/90 Gateway  DNS servers: [ ] Domains ,"
+            + " apfCapabilities: ApfCapabilities{version: 1, maxSize: 2, format: 3},"
+            + " provisioningTimeoutMs: 4200, ipv6AddrGenMode: 123, network: 321,"
+            + " displayName: test_config, enablePreconnection: false, scanResultInfo:"
+            + " android.net.ScanResultInfoParcelable{ssid: ssid, bssid: 01:02:03:04:05:06,"
+            + " informationElements: [android.net.InformationElementParcelable{id: 221,"
+            + " payload: [0, 23, -14, 6, 1, 1, 3, 1, 0, 0]}]}, layer2Info:"
+            + " android.net.Layer2InformationParcelable{l2Key: some l2key,"
+            + " cluster: some cluster, bssid: %s},"
+            + " options: [android.net.networkstack.aidl.dhcp.DhcpOption{type: 60,"
+            + " value: [97, 110, 100, 114, 111, 105, 100, 45, 100, 104, 99, 112, 45, 49, 49]}]}";
+
+    @Test
+    public void testParcelableToString() {
+        String expected = String.format(TEMPLATE, "00:01:02:03:04:05");
+        assertEquals(expected, mConfig.toStableParcelable().toString());
+
+        final ProvisioningConfigurationParcelable parcelWithNull = mConfig.toStableParcelable();
+        parcelWithNull.layer2Info.bssid = null;
+        expected = String.format(TEMPLATE, "null");
+        assertEquals(expected, parcelWithNull.toString());
+    }
 }
diff --git a/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt b/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt
index 4e4d25a..6f495e7 100644
--- a/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt
+++ b/tests/unit/src/android/net/testutils/TestableNetworkCallbackTest.kt
@@ -1,12 +1,22 @@
 package android.net.testutils
 
+import android.annotation.SuppressLint
 import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.Network
 import android.net.NetworkCapabilities
-import com.android.testutils.ConcurrentIntepreter
+import com.android.testutils.ConcurrentInterpreter
 import com.android.testutils.InterpretMatcher
 import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.AVAILABLE
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.BLOCKED_STATUS
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LINK_PROPERTIES_CHANGED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOSING
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOST
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
@@ -33,6 +43,7 @@
 const val TEST_INTERFACE_NAME = "testInterfaceName"
 
 @RunWith(JUnit4::class)
+@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
 class TestableNetworkCallbackTest {
     private lateinit var mCallback: TestableNetworkCallback
 
@@ -245,15 +256,51 @@
             onBlockedStatus(199)       | poll(1) = BlockedStatus(199) time 0..3
         """)
     }
+
+    @Test
+    fun testEventuallyExpect() {
+        // TODO: Current test does not verify the inline one. Also verify the behavior after
+        // aligning two eventuallyExpect()
+        val net1 = Network(100)
+        val net2 = Network(101)
+        mCallback.onAvailable(net1)
+        mCallback.onCapabilitiesChanged(net1, NetworkCapabilities())
+        mCallback.onLinkPropertiesChanged(net1, LinkProperties())
+        mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) {
+            net1.equals(it.network)
+        }
+        // No further new callback. Expect no callback.
+        assertFails { mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) }
+
+        // Verify no predicate set.
+        mCallback.onAvailable(net2)
+        mCallback.onLinkPropertiesChanged(net2, LinkProperties())
+        mCallback.onBlockedStatusChanged(net1, false)
+        mCallback.eventuallyExpect(BLOCKED_STATUS) { net1.equals(it.network) }
+        // Verify no callback received if the callback does not happen.
+        assertFails { mCallback.eventuallyExpect(LOSING) }
+    }
+
+    @Test
+    fun testEventuallyExpectOnMultiThreads() {
+        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
+                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
+                onAvailable(100)                   | eventually(CapabilitiesChanged(100), 1) fails
+                sleep ; onCapabilitiesChanged(100) | eventually(CapabilitiesChanged(100), 2)
+                onAvailable(101) ; onBlockedStatus(101) | eventually(BlockedStatus(100), 2) fails
+                onSuspended(100) ; sleep ; onLost(100)  | eventually(Lost(100), 2)
+        """)
+    }
 }
 
-private object TNCInterpreter : ConcurrentIntepreter<TestableNetworkCallback>(interpretTable)
+private object TNCInterpreter : ConcurrentInterpreter<TestableNetworkCallback>(interpretTable)
 
 val EntryList = CallbackEntry::class.sealedSubclasses.map { it.simpleName }.joinToString("|")
 private fun callbackEntryFromString(name: String): KClass<out CallbackEntry> {
     return CallbackEntry::class.sealedSubclasses.first { it.simpleName == name }
 }
 
+@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs.
 private val interpretTable = listOf<InterpretMatcher<TestableNetworkCallback>>(
     // Interpret "Available(xx)" as "call to onAvailable with netId xx", and likewise for
     // all callback types. This is implemented above by enumerating the subclasses of
@@ -289,5 +336,26 @@
     },
     Regex("""poll\((\d+)\)""") to { i, cb, t ->
         cb.pollForNextCallback(t.timeArg(1))
+    },
+    // Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects
+    // CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and
+    // likewise for all callback types.
+    Regex("""eventually\(($EntryList)\((\d+)\),\s+(\d+)\)""") to { i, cb, t ->
+        val net = Network(t.intArg(2))
+        val timeout = t.timeArg(3)
+        when (t.strArg(1)) {
+            "Available" -> cb.eventuallyExpect(AVAILABLE, timeout) { net == it.network }
+            "Suspended" -> cb.eventuallyExpect(SUSPENDED, timeout) { net == it.network }
+            "Resumed" -> cb.eventuallyExpect(RESUMED, timeout) { net == it.network }
+            "Losing" -> cb.eventuallyExpect(LOSING, timeout) { net == it.network }
+            "Lost" -> cb.eventuallyExpect(LOST, timeout) { net == it.network }
+            "Unavailable" -> cb.eventuallyExpect(UNAVAILABLE, timeout) { net == it.network }
+            "BlockedStatus" -> cb.eventuallyExpect(BLOCKED_STATUS, timeout) { net == it.network }
+            "CapabilitiesChanged" ->
+                cb.eventuallyExpect(NETWORK_CAPS_UPDATED, timeout) { net == it.network }
+            "LinkPropertiesChanged" ->
+                cb.eventuallyExpect(LINK_PROPERTIES_CHANGED, timeout) { net == it.network }
+            else -> fail("Unknown callback type")
+        }
     }
 )
diff --git a/tests/unit/src/android/net/testutils/TrackRecordTest.kt b/tests/unit/src/android/net/testutils/TrackRecordTest.kt
deleted file mode 100644
index 995d537..0000000
--- a/tests/unit/src/android/net/testutils/TrackRecordTest.kt
+++ /dev/null
@@ -1,420 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.testutils
-
-import com.android.testutils.ArrayTrackRecord
-import com.android.testutils.ConcurrentIntepreter
-import com.android.testutils.InterpretException
-import com.android.testutils.InterpretMatcher
-import com.android.testutils.SyntaxException
-import com.android.testutils.TrackRecord
-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 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 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 : ConcurrentIntepreter<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/tests/unit/src/android/net/util/InterfaceParamsTest.java b/tests/unit/src/android/net/util/InterfaceParamsTest.java
index 1be4368..dc70bd0 100644
--- a/tests/unit/src/android/net/util/InterfaceParamsTest.java
+++ b/tests/unit/src/android/net/util/InterfaceParamsTest.java
@@ -25,7 +25,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.server.util.NetworkStackConstants;
+import com.android.net.module.util.NetworkStackConstants;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tests/unit/src/android/net/util/NetworkStackUtilsTest.java b/tests/unit/src/android/net/util/NetworkStackUtilsTest.java
deleted file mode 100644
index 80b4272..0000000
--- a/tests/unit/src/android/net/util/NetworkStackUtilsTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.util;
-
-import static android.net.InetAddresses.parseNumericAddress;
-import static android.net.util.NetworkStackUtils.isIPv6ULA;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.eq;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.provider.DeviceConfig;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-
-
-/**
- * Tests for NetworkStackUtils.
- *
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class NetworkStackUtilsTest {
-    private static final String TEST_NAME_SPACE = "connectivity";
-    private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
-    private static final int TEST_FLAG_VALUE = 28;
-    private static final String TEST_FLAG_VALUE_STRING = "28";
-    private static final int TEST_DEFAULT_FLAG_VALUE = 0;
-    private static final int TEST_MAX_FLAG_VALUE = 1000;
-    private static final int TEST_MIN_FLAG_VALUE = 100;
-    private static final long TEST_PACKAGE_VERSION = 290000000;
-    private static final String TEST_PACKAGE_NAME = "NetworkStackUtilsTest";
-    private MockitoSession mSession;
-
-    @Mock private Context mContext;
-    @Mock private PackageManager mPm;
-    @Mock private PackageInfo mPi;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
-
-        final PackageInfo pi = new PackageInfo();
-        pi.setLongVersionCode(TEST_PACKAGE_VERSION);
-
-        doReturn(mPm).when(mContext).getPackageManager();
-        doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
-        doReturn(pi).when(mPm).getPackageInfo(anyString(), anyInt());
-    }
-
-    @After
-    public void tearDown() {
-        mSession.finishMocking();
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_Null() {
-        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_DEFAULT_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_NotNull() {
-        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_NormalValue() {
-        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
-                TEST_MAX_FLAG_VALUE /* maximum value */,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_NullValue() {
-        doReturn(null).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_DEFAULT_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, 0 /* minimum value */,
-                TEST_MAX_FLAG_VALUE /* maximum value */,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_OverMaximumValue() {
-        doReturn(Integer.toString(TEST_MAX_FLAG_VALUE + 10)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_DEFAULT_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
-                TEST_MAX_FLAG_VALUE /* maximum value */,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyInt_BelowMinimumValue() {
-        doReturn(Integer.toString(TEST_MIN_FLAG_VALUE - 10)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
-        assertEquals(TEST_DEFAULT_FLAG_VALUE, NetworkStackUtils.getDeviceConfigPropertyInt(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_MIN_FLAG_VALUE /* minimum value */,
-                TEST_MAX_FLAG_VALUE /* maximum value */,
-                TEST_DEFAULT_FLAG_VALUE /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyBoolean_Null() {
-        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertFalse(NetworkStackUtils.getDeviceConfigPropertyBoolean(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
-                false /* default value */));
-    }
-
-    @Test
-    public void testGetDeviceConfigPropertyBoolean_NotNull() {
-        doReturn("true").when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(NetworkStackUtils.getDeviceConfigPropertyBoolean(
-                TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG,
-                false /* default value */));
-    }
-
-    @Test
-    public void testFeatureIsEnabledWithExceptionsEnabled() {
-        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(NetworkStackUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG));
-    }
-
-    @Test
-    public void testFeatureIsNotEnabled() {
-        doReturn(null).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        assertFalse(NetworkStackUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG));
-    }
-
-    @Test
-    public void testFeatureIsEnabledWithException() throws Exception {
-        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
-                eq(TEST_EXPERIMENT_FLAG)));
-        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
-        assertFalse(NetworkStackUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG));
-    }
-
-    @Test
-    public void testIsIPv6ULA() {
-        assertTrue(isIPv6ULA(parseNumericAddress("fc00::")));
-        assertTrue(isIPv6ULA(parseNumericAddress("fc00::1")));
-        assertTrue(isIPv6ULA(parseNumericAddress("fc00:1234::5678")));
-        assertTrue(isIPv6ULA(parseNumericAddress("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
-
-        assertFalse(isIPv6ULA(parseNumericAddress("fe00::")));
-        assertFalse(isIPv6ULA(parseNumericAddress("2480:1248::123:456")));
-    }
-}
\ No newline at end of file
diff --git a/tests/unit/src/android/net/util/PacketReaderTest.java b/tests/unit/src/android/net/util/PacketReaderTest.java
deleted file mode 100644
index 3947d15..0000000
--- a/tests/unit/src/android/net/util/PacketReaderTest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.util;
-
-import static android.net.util.PacketReader.DEFAULT_RECV_BUF_SIZE;
-import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.IPPROTO_UDP;
-import static android.system.OsConstants.SOCK_DGRAM;
-import static android.system.OsConstants.SOCK_NONBLOCK;
-import static android.system.OsConstants.SOL_SOCKET;
-import static android.system.OsConstants.SO_SNDTIMEO;
-
-import static com.android.testutils.MiscAssertsKt.assertThrows;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.StructTimeval;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.FileDescriptor;
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketException;
-import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tests for PacketReader.
- *
- * @hide
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class PacketReaderTest {
-    static final InetAddress LOOPBACK6 = Inet6Address.getLoopbackAddress();
-    static final StructTimeval TIMEO = StructTimeval.fromMillis(500);
-
-    protected CountDownLatch mLatch;
-    protected FileDescriptor mLocalSocket;
-    protected InetSocketAddress mLocalSockName;
-    protected byte[] mLastRecvBuf;
-    protected boolean mStopped;
-    protected HandlerThread mHandlerThread;
-    protected PacketReader mReceiver;
-
-    class UdpLoopbackReader extends PacketReader {
-        public UdpLoopbackReader(Handler h) {
-            super(h);
-        }
-
-        @Override
-        protected FileDescriptor createFd() {
-            FileDescriptor s = null;
-            try {
-                s = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
-                Os.bind(s, LOOPBACK6, 0);
-                mLocalSockName = (InetSocketAddress) Os.getsockname(s);
-                Os.setsockoptTimeval(s, SOL_SOCKET, SO_SNDTIMEO, TIMEO);
-            } catch (ErrnoException|SocketException e) {
-                closeFd(s);
-                fail();
-                return null;
-            }
-
-            mLocalSocket = s;
-            return s;
-        }
-
-        @Override
-        protected void handlePacket(byte[] recvbuf, int length) {
-            mLastRecvBuf = Arrays.copyOf(recvbuf, length);
-            mLatch.countDown();
-        }
-
-        @Override
-        protected void onStart() {
-            mStopped = false;
-            mLatch.countDown();
-        }
-
-        @Override
-        protected void onStop() {
-            mStopped = true;
-            mLatch.countDown();
-        }
-    };
-
-    @Before
-    public void setUp() {
-        resetLatch();
-        mLocalSocket = null;
-        mLocalSockName = null;
-        mLastRecvBuf = null;
-        mStopped = false;
-
-        mHandlerThread = new HandlerThread(PacketReaderTest.class.getSimpleName());
-        mHandlerThread.start();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (mReceiver != null) {
-            mHandlerThread.getThreadHandler().post(() -> { mReceiver.stop(); });
-            waitForActivity();
-        }
-        mReceiver = null;
-        mHandlerThread.quit();
-        mHandlerThread = null;
-    }
-
-    void resetLatch() { mLatch = new CountDownLatch(1); }
-
-    void waitForActivity() throws Exception {
-        try {
-            mLatch.await(1000, TimeUnit.MILLISECONDS);
-        } finally {
-            resetLatch();
-        }
-    }
-
-    void sendPacket(byte[] contents) throws Exception {
-        final DatagramSocket sender = new DatagramSocket();
-        sender.connect(mLocalSockName);
-        sender.send(new DatagramPacket(contents, contents.length));
-        sender.close();
-    }
-
-    @Test
-    public void testBasicWorking() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-        mReceiver = new UdpLoopbackReader(h);
-
-        h.post(() -> { mReceiver.start(); });
-        waitForActivity();
-        assertTrue(mLocalSockName != null);
-        assertEquals(LOOPBACK6, mLocalSockName.getAddress());
-        assertTrue(0 < mLocalSockName.getPort());
-        assertTrue(mLocalSocket != null);
-        assertFalse(mStopped);
-
-        final byte[] one = "one 1".getBytes("UTF-8");
-        sendPacket(one);
-        waitForActivity();
-        assertEquals(1, mReceiver.numPacketsReceived());
-        assertTrue(Arrays.equals(one, mLastRecvBuf));
-        assertFalse(mStopped);
-
-        final byte[] two = "two 2".getBytes("UTF-8");
-        sendPacket(two);
-        waitForActivity();
-        assertEquals(2, mReceiver.numPacketsReceived());
-        assertTrue(Arrays.equals(two, mLastRecvBuf));
-        assertFalse(mStopped);
-
-        h.post(() -> mReceiver.stop());
-        waitForActivity();
-        assertEquals(2, mReceiver.numPacketsReceived());
-        assertTrue(Arrays.equals(two, mLastRecvBuf));
-        assertTrue(mStopped);
-        mReceiver = null;
-    }
-
-    class NullPacketReader extends PacketReader {
-        public NullPacketReader(Handler h, int recvbufsize) {
-            super(h, recvbufsize);
-        }
-
-        @Override
-        public FileDescriptor createFd() { return null; }
-    }
-
-    @Test
-    public void testMinimalRecvBufSize() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-
-        for (int i : new int[]{-1, 0, 1, DEFAULT_RECV_BUF_SIZE-1}) {
-            final PacketReader b = new NullPacketReader(h, i);
-            assertEquals(DEFAULT_RECV_BUF_SIZE, b.recvBufSize());
-        }
-    }
-
-    @Test
-    public void testStartingFromWrongThread() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
-        assertThrows(IllegalStateException.class, () -> b.start());
-    }
-
-    @Test
-    public void testStoppingFromWrongThread() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
-        assertThrows(IllegalStateException.class, () -> b.stop());
-    }
-
-    @Test
-    public void testSuccessToCreateSocket() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-        final PacketReader b = new UdpLoopbackReader(h);
-        h.post(() -> assertTrue(b.start()));
-    }
-
-    @Test
-    public void testFailToCreateSocket() throws Exception {
-        final Handler h = mHandlerThread.getThreadHandler();
-        final PacketReader b = new NullPacketReader(h, DEFAULT_RECV_BUF_SIZE);
-        h.post(() -> assertFalse(b.start()));
-    }
-}
diff --git a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
index 348392d..3b44597 100644
--- a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
+++ b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
@@ -112,6 +112,22 @@
         }
     }
 
+    private val mTestCapportVenueUrlWithFriendlyNameLp by lazy {
+        LinkProperties().apply {
+            captivePortalData = CaptivePortalData.Builder()
+                    .setCaptive(false)
+                    .setVenueInfoUrl(Uri.parse(TEST_VENUE_INFO_URL))
+                    .build()
+            val networkShim = NetworkInformationShimImpl.newInstance()
+            val captivePortalDataShim = networkShim.getCaptivePortalData(this)
+
+            if (captivePortalDataShim != null) {
+                networkShim.setCaptivePortalData(this, captivePortalDataShim
+                        .withVenueFriendlyName(TEST_NETWORK_FRIENDLY_NAME))
+            }
+        }
+    }
+
     private val TEST_NETWORK = Network(42)
     private val TEST_NETWORK_TAG = TEST_NETWORK.networkHandle.toString()
     private val TEST_SSID = "TestSsid"
@@ -125,6 +141,7 @@
 
     private val TEST_VENUE_INFO_URL = "https://testvenue.example.com/info"
     private val EMPTY_CAPPORT_LP = LinkProperties()
+    private val TEST_NETWORK_FRIENDLY_NAME = "Network Friendly Name"
 
     @Before
     fun setUp() {
@@ -333,6 +350,28 @@
         verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any())
     }
 
+    @Test
+    fun testConnectedVenueInfoWithFriendlyNameNotification() {
+        // Venue info (CaptivePortalData) with friendly name is not available for API <= R
+        assumeTrue(NetworkInformationShimImpl.useApiAboveR())
+        mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK)
+        onLinkPropertiesChanged(mTestCapportVenueUrlWithFriendlyNameLp)
+        onDefaultNetworkAvailable(TEST_NETWORK)
+        val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID)
+        onCapabilitiesChanged(capabilities)
+
+        mLooper.processAllMessages()
+
+        verifyConnectedNotification(timeout = 0)
+        verifyVenueInfoIntent(mIntentCaptor.value)
+        verify(mResources).getString(R.string.tap_for_info)
+        verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture())
+        val note = mNoteCaptor.value
+        assertEquals(TEST_NETWORK_FRIENDLY_NAME, note.extras
+                .getCharSequence(Notification.EXTRA_TITLE))
+        verifyCanceledNotificationAfterDefaultNetworkLost()
+    }
+
     private fun verifyVenueInfoIntent(intent: Intent) {
         assertEquals(Intent.ACTION_VIEW, intent.action)
         assertEquals(Uri.parse(TEST_VENUE_INFO_URL), intent.data)
diff --git a/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java b/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java
index 18f4e04..dd7ba6a 100644
--- a/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java
+++ b/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java
@@ -16,9 +16,9 @@
 
 package com.android.networkstack.arp;
 
-import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
-import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.testutils.MiscAssertsKt.assertThrows;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
index a884f7e..8fbe0c4 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
@@ -207,7 +207,6 @@
     private static final int TEST_NETID2_FWMARK = 0x1A85;
     private static final int NETID_MASK = 0xffff;
     @Mock private TcpSocketTracker.Dependencies mDependencies;
-    @Mock private FileDescriptor mMockFd;
     @Mock private INetd mNetd;
     private final Network mNetwork = new Network(TEST_NETID1);
     private final Network mOtherNetwork = new Network(TEST_NETID2);
@@ -226,7 +225,7 @@
                 Log.setWtfHandler((tag, what, system) -> Log.e(tag, what.getMessage(), what));
         when(mDependencies.getNetd()).thenReturn(mNetd);
         when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true);
-        when(mDependencies.connectToKernel()).thenReturn(mMockFd);
+        when(mDependencies.connectToKernel()).thenReturn(new FileDescriptor());
         when(mDependencies.getDeviceConfigPropertyInt(
                 eq(NAMESPACE_CONNECTIVITY),
                 eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
diff --git a/tests/unit/src/com/android/networkstack/packets/NeighborAdvertisementTest.java b/tests/unit/src/com/android/networkstack/packets/NeighborAdvertisementTest.java
new file mode 100644
index 0000000..c689d7b
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/packets/NeighborAdvertisementTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.packets;
+
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+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.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class NeighborAdvertisementTest {
+    private static final Inet6Address TEST_SRC_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::dfd9:50a0:cc7b:7d6d");
+    private static final Inet6Address TEST_TARGET_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8:1:0:c928:250d:b90c:3178");
+    private static final byte[] TEST_SOURCE_MAC_ADDR = new byte[] {
+            (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25
+    };
+    private static final byte[] TEST_DST_MAC_ADDR = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+    };
+    private static final byte[] TEST_GRATUITOUS_NA = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xdf, (byte) 0xd9, (byte) 0x50, (byte) 0xa0,
+        (byte) 0xcc, (byte) 0x7b, (byte) 0x7d, (byte) 0x6d,
+        // destination address
+        (byte) 0xff, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+        // ICMP type, code, checksum
+        (byte) 0x88, (byte) 0x00, (byte) 0x3a, (byte) 0x3c,
+        // flags
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+        (byte) 0xc9, (byte) 0x28, (byte) 0x25, (byte) 0x0d,
+        (byte) 0xb9, (byte) 0x0c, (byte) 0x31, (byte) 0x78,
+        // TLLA option
+        (byte) 0x02, (byte) 0x01,
+        // Link-Layer address
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25,
+    };
+    private static final byte[] TEST_GRATUITOUS_NA_WITHOUT_TLLA = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xdf, (byte) 0xd9, (byte) 0x50, (byte) 0xa0,
+        (byte) 0xcc, (byte) 0x7b, (byte) 0x7d, (byte) 0x6d,
+        // destination address
+        (byte) 0xff, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+        // ICMP type, code, checksum
+        (byte) 0x88, (byte) 0x00, (byte) 0x3a, (byte) 0x3c,
+        // flags
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+        (byte) 0xc9, (byte) 0x28, (byte) 0x25, (byte) 0x0d,
+        (byte) 0xb9, (byte) 0x0c, (byte) 0x31, (byte) 0x78,
+    };
+    private static final byte[] TEST_GRATUITOUS_NA_LESS_LENGTH = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xdf, (byte) 0xd9, (byte) 0x50, (byte) 0xa0,
+        (byte) 0xcc, (byte) 0x7b, (byte) 0x7d, (byte) 0x6d,
+        // destination address
+        (byte) 0xff, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+        // ICMP type, code, checksum
+        (byte) 0x88, (byte) 0x00, (byte) 0x3a, (byte) 0x3c,
+        // flags
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+    };
+    private static final byte[] TEST_GRATUITOUS_NA_TRUNCATED = new byte[] {
+        // dst mac address
+        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+        // src mac address
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25, (byte) 0xc1, (byte) 0x25,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xdf, (byte) 0xd9, (byte) 0x50, (byte) 0xa0,
+        (byte) 0xcc, (byte) 0x7b, (byte) 0x7d, (byte) 0x6d,
+        // destination address
+        (byte) 0xff, (byte) 0x02, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+        // ICMP type, code, checksum
+        (byte) 0x88, (byte) 0x00, (byte) 0x3a, (byte) 0x3c,
+        // flags
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+        (byte) 0xc9, (byte) 0x28, (byte) 0x25, (byte) 0x0d,
+        (byte) 0xb9, (byte) 0x0c, (byte) 0x31, (byte) 0x78,
+        // TLLA option
+        (byte) 0x02, (byte) 0x01,
+        // truncatd Link-Layer address: 4bytes
+        (byte) 0xea, (byte) 0xbe, (byte) 0x11, (byte) 0x25,
+    };
+
+    @Test
+    public void testGratuitousNa_build() throws Exception {
+        final ByteBuffer na = NeighborAdvertisement.build(
+                MacAddress.fromBytes(TEST_SOURCE_MAC_ADDR),
+                MacAddress.fromBytes(TEST_DST_MAC_ADDR),
+                TEST_SRC_ADDR, IPV6_ADDR_ALL_ROUTERS_MULTICAST, 0 /* flags */, TEST_TARGET_ADDR);
+        assertArrayEquals(na.array(), TEST_GRATUITOUS_NA);
+    }
+
+    private void assertNeighborAdvertisement(final NeighborAdvertisement na,
+            boolean hasTllaOption) {
+        assertArrayEquals(TEST_SOURCE_MAC_ADDR, na.ethHdr.srcMac.toByteArray());
+        assertArrayEquals(TEST_DST_MAC_ADDR, na.ethHdr.dstMac.toByteArray());
+        assertEquals(ETH_P_IPV6, na.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, na.ipv6Hdr.nextHeader);
+        assertEquals(0xff, na.ipv6Hdr.hopLimit);
+        assertEquals(IPV6_ADDR_ALL_ROUTERS_MULTICAST, na.ipv6Hdr.dstIp);
+        assertEquals(TEST_SRC_ADDR, na.ipv6Hdr.srcIp);
+        assertEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, na.icmpv6Hdr.type);
+        assertEquals(0, na.icmpv6Hdr.code);
+        assertEquals(0, na.naHdr.flags);
+        assertEquals(TEST_TARGET_ADDR, na.naHdr.target);
+        if (hasTllaOption) {
+            assertEquals(ICMPV6_ND_OPTION_TLLA, na.tlla.type);
+            assertEquals(1, na.tlla.length);
+            assertArrayEquals(TEST_SOURCE_MAC_ADDR, na.tlla.linkLayerAddress.toByteArray());
+        }
+    }
+
+    @Test
+    public void testGratuitousNa_parse() throws Exception {
+        final NeighborAdvertisement na = NeighborAdvertisement.parse(TEST_GRATUITOUS_NA,
+                TEST_GRATUITOUS_NA.length);
+
+        assertNeighborAdvertisement(na, true /* hasTllaOption */);
+        assertArrayEquals(TEST_GRATUITOUS_NA, na.toByteBuffer().array());
+    }
+
+    @Test
+    public void testGratuitousNa_parseWithoutTllaOption() throws Exception {
+        final NeighborAdvertisement na =
+                NeighborAdvertisement.parse(TEST_GRATUITOUS_NA_WITHOUT_TLLA,
+                        TEST_GRATUITOUS_NA_WITHOUT_TLLA.length);
+
+        assertNeighborAdvertisement(na, false /* hasTllaOption */);
+        assertArrayEquals(TEST_GRATUITOUS_NA_WITHOUT_TLLA, na.toByteBuffer().array());
+    }
+
+    @Test
+    public void testGratuitousNa_zeroPacketLength() throws Exception {
+        assertThrows(NeighborAdvertisement.ParseException.class,
+                () -> NeighborAdvertisement.parse(TEST_GRATUITOUS_NA, 0));
+    }
+
+    @Test
+    public void testGratuitousNa_invalidByteBufferLength() throws Exception {
+        assertThrows(NeighborAdvertisement.ParseException.class,
+                () -> NeighborAdvertisement.parse(TEST_GRATUITOUS_NA_TRUNCATED,
+                                                  TEST_GRATUITOUS_NA.length));
+    }
+
+    @Test
+    public void testGratuitousNa_lessPacketLength() throws Exception {
+        assertThrows(NeighborAdvertisement.ParseException.class,
+                () -> NeighborAdvertisement.parse(TEST_GRATUITOUS_NA_LESS_LENGTH,
+                                                  TEST_GRATUITOUS_NA_LESS_LENGTH.length));
+    }
+
+    @Test
+    public void testGratuitousNa_truncatedPacket() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> NeighborAdvertisement.parse(TEST_GRATUITOUS_NA_TRUNCATED,
+                                                  TEST_GRATUITOUS_NA_TRUNCATED.length));
+    }
+}
diff --git a/tests/unit/src/com/android/networkstack/packets/NeighborSolicitationTest.java b/tests/unit/src/com/android/networkstack/packets/NeighborSolicitationTest.java
new file mode 100644
index 0000000..18a3ef3
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/packets/NeighborSolicitationTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.packets;
+
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+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.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class NeighborSolicitationTest {
+    private static final Inet6Address TEST_SRC_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::d419:d664:df38:2f65");
+    private static final Inet6Address TEST_DST_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::200:1a:1122:3344");
+    private static final Inet6Address TEST_TARGET_ADDR =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::200:1a:1122:3344");
+    private static final byte[] TEST_SOURCE_MAC_ADDR = new byte[] {
+            (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02, (byte) 0x61, (byte) 0x11,
+    };
+    private static final byte[] TEST_DST_MAC_ADDR = new byte[] {
+            (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+    };
+    private static final byte[] TEST_NEIGHBOR_SOLICITATION = new byte[] {
+        // dst mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // src mac address
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02, (byte) 0x61, (byte) 0x11,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xd4, (byte) 0x19, (byte) 0xd6, (byte) 0x64,
+        (byte) 0xdf, (byte) 0x38, (byte) 0x2f, (byte) 0x65,
+        // destination address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // ICMP type, code, checksum
+        (byte) 0x87, (byte) 0x00, (byte) 0x22, (byte) 0x96,
+        // reserved
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // slla option
+        (byte) 0x01, (byte) 0x01,
+        // link-layer address
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02,
+        (byte) 0x61, (byte) 0x11,
+    };
+    private static final byte[] TEST_NEIGHBOR_SOLICITATION_WITHOUT_SLLA = new byte[] {
+        // dst mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // src mac address
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02, (byte) 0x61, (byte) 0x11,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xd4, (byte) 0x19, (byte) 0xd6, (byte) 0x64,
+        (byte) 0xdf, (byte) 0x38, (byte) 0x2f, (byte) 0x65,
+        // destination address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // ICMP type, code, checksum
+        (byte) 0x87, (byte) 0x00, (byte) 0x22, (byte) 0x96,
+        // reserved
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+    };
+    private static final byte[] TEST_NEIGHBOR_SOLICITATION_LESS_LENGTH = new byte[] {
+        // dst mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // src mac address
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02, (byte) 0x61, (byte) 0x11,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xd4, (byte) 0x19, (byte) 0xd6, (byte) 0x64,
+        (byte) 0xdf, (byte) 0x38, (byte) 0x2f, (byte) 0x65,
+        // destination address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // ICMP type, code, checksum
+        (byte) 0x87, (byte) 0x00, (byte) 0x22, (byte) 0x96,
+        // reserved
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+    };
+    private static final byte[] TEST_NEIGHBOR_SOLICITATION_TRUNCATED = new byte[] {
+        // dst mac address
+        (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // src mac address
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02, (byte) 0x61, (byte) 0x11,
+        // ether type
+        (byte) 0x86, (byte) 0xdd,
+        // version, priority and flow label
+        (byte) 0x60, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // length
+        (byte) 0x00, (byte) 0x20,
+        // next header
+        (byte) 0x3a,
+        // hop limit
+        (byte) 0xff,
+        // source address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0xd4, (byte) 0x19, (byte) 0xd6, (byte) 0x64,
+        (byte) 0xdf, (byte) 0x38, (byte) 0x2f, (byte) 0x65,
+        // destination address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // ICMP type, code, checksum
+        (byte) 0x87, (byte) 0x00, (byte) 0x22, (byte) 0x96,
+        // reserved
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        // target address
+        (byte) 0xfe, (byte) 0x80, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x1a,
+        (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44,
+        // slla option
+        (byte) 0x01, (byte) 0x01,
+        // truncatd link-layer address: 4bytes
+        (byte) 0x06, (byte) 0x5a, (byte) 0xac, (byte) 0x02,
+    };
+
+    @Test
+    public void testNeighborSolicitation_build() throws Exception {
+        final ByteBuffer ns = NeighborSolicitation.build(
+                MacAddress.fromBytes(TEST_SOURCE_MAC_ADDR),
+                MacAddress.fromBytes(TEST_DST_MAC_ADDR),
+                TEST_SRC_ADDR, TEST_DST_ADDR, TEST_TARGET_ADDR);
+        assertArrayEquals(ns.array(), TEST_NEIGHBOR_SOLICITATION);
+    }
+
+    private void assertNeighborSolicitation(final NeighborSolicitation ns, boolean hasSllaOption) {
+        assertArrayEquals(TEST_SOURCE_MAC_ADDR, ns.ethHdr.srcMac.toByteArray());
+        assertArrayEquals(TEST_DST_MAC_ADDR, ns.ethHdr.dstMac.toByteArray());
+        assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
+        assertEquals(0xff, ns.ipv6Hdr.hopLimit);
+        assertEquals(TEST_DST_ADDR, ns.ipv6Hdr.dstIp);
+        assertEquals(TEST_SRC_ADDR, ns.ipv6Hdr.srcIp);
+        assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
+        assertEquals(0, ns.icmpv6Hdr.code);
+        assertEquals(TEST_TARGET_ADDR, ns.nsHdr.target);
+        if (hasSllaOption) {
+            assertEquals(ICMPV6_ND_OPTION_SLLA, ns.slla.type);
+            assertEquals(1, ns.slla.length);
+            assertEquals(MacAddress.fromBytes(TEST_SOURCE_MAC_ADDR), ns.slla.linkLayerAddress);
+        }
+    }
+
+    @Test
+    public void testNeighborSolicitation_parse() throws Exception {
+        final NeighborSolicitation ns = NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION,
+                TEST_NEIGHBOR_SOLICITATION.length);
+
+        assertNeighborSolicitation(ns, true /* hasSllaOption */);
+        assertArrayEquals(TEST_NEIGHBOR_SOLICITATION, ns.toByteBuffer().array());
+    }
+
+    @Test
+    public void testNeighborSolicitation_parseWithoutSllaOption() throws Exception {
+        final NeighborSolicitation ns =
+                NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION_WITHOUT_SLLA,
+                        TEST_NEIGHBOR_SOLICITATION_WITHOUT_SLLA.length);
+
+        assertNeighborSolicitation(ns, false /* hasSllaOption */);
+        assertArrayEquals(TEST_NEIGHBOR_SOLICITATION_WITHOUT_SLLA, ns.toByteBuffer().array());
+    }
+
+    @Test
+    public void testNeighborSolicitation_invalidPacketLength() throws Exception {
+        assertThrows(NeighborSolicitation.ParseException.class,
+                () -> NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION, 0));
+    }
+
+    @Test
+    public void testNeighborSolicitation_invalidByteBufferLength() throws Exception {
+        assertThrows(NeighborSolicitation.ParseException.class,
+                () -> NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION_TRUNCATED,
+                                                  TEST_NEIGHBOR_SOLICITATION.length));
+    }
+
+    @Test
+    public void testNeighborSolicitation_lessPacketLength() throws Exception {
+        assertThrows(NeighborSolicitation.ParseException.class,
+                () -> NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION_LESS_LENGTH,
+                                                  TEST_NEIGHBOR_SOLICITATION_LESS_LENGTH.length));
+    }
+
+    @Test
+    public void testNeighborSolicitation_truncatedPacket() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> NeighborSolicitation.parse(TEST_NEIGHBOR_SOLICITATION_TRUNCATED,
+                                                  TEST_NEIGHBOR_SOLICITATION_TRUNCATED.length));
+    }
+}
diff --git a/tests/unit/src/com/android/server/NetworkStackServiceTest.kt b/tests/unit/src/com/android/server/NetworkStackServiceTest.kt
index c054b3a..b32a419 100644
--- a/tests/unit/src/com/android/server/NetworkStackServiceTest.kt
+++ b/tests/unit/src/com/android/server/NetworkStackServiceTest.kt
@@ -31,6 +31,7 @@
 import android.net.ip.IpClient
 import android.os.Build
 import android.os.IBinder
+import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH
@@ -42,6 +43,8 @@
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.ExceptionUtils
+import com.android.testutils.assertThrows
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -172,6 +175,7 @@
                 mock(IBinder::class.java)))
         doReturn(9990003).`when`(mockNetworkMonitorCb).interfaceVersion
         doReturn("networkmonitor_hash").`when`(mockNetworkMonitorCb).interfaceHash
+        INetworkMonitorCallbacks.Stub.setDefaultImpl(INetworkMonitorCallbacks.Default())
 
         connector.makeNetworkMonitor(Network(123), "test_nm", mockNetworkMonitorCb)
 
@@ -202,9 +206,15 @@
         connector.makeDhcpServer(TEST_IFACE, testParams, mockDhcpCb)
         verify(mockDhcpCb, times(2)).onDhcpServerCreated(eq(IDhcpServer.STATUS_SUCCESS), any())
 
-        // Verify all methods were covered by the test (4 methods + getVersion + getHash)
-        assertEquals(6, INetworkStackConnector::class.declaredMemberFunctions.count {
+        // allowTestUid does not need to record the caller's version
+        assertThrows(SecurityException::class.java, ExceptionUtils.ThrowingRunnable {
+            // Should throw because the test does not run as root
+            connector.allowTestUid(Process.myUid(), null)
+        })
+
+        // Verify all methods were covered by the test (5 methods + getVersion + getHash)
+        assertEquals(7, INetworkStackConnector::class.declaredMemberFunctions.count {
             it.visibility == KVisibility.PUBLIC
         })
     }
-}
\ No newline at end of file
+}
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index a3ef532..66d1c71 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -25,9 +25,14 @@
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -91,6 +96,7 @@
 import static java.util.stream.Collectors.toList;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -142,7 +148,11 @@
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
 import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.metrics.DataStallDetectionStats;
 import com.android.networkstack.metrics.DataStallStatsUtils;
 import com.android.networkstack.netlink.TcpSocketTracker;
@@ -151,8 +161,9 @@
 import com.android.server.connectivity.nano.DnsEvent;
 import com.android.server.connectivity.nano.WifiData;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.HandlerUtilsKt;
+import com.android.testutils.HandlerUtils;
 
 import com.google.protobuf.nano.MessageNano;
 
@@ -197,6 +208,7 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
+@SuppressLint("NewApi")  // Uses hidden APIs, which the linter would identify as missing APIs.
 public class NetworkMonitorTest {
     private static final String LOCATION_HEADER = "location";
     private static final String CONTENT_TYPE_HEADER = "Content-Type";
@@ -261,6 +273,7 @@
     private static final String TEST_SPEED_TEST_URL = "https://speedtest.example.com";
     private static final String TEST_RELATIVE_URL = "/test/relative/gen_204";
     private static final String TEST_MCCMNC = "123456";
+    private static final String TEST_FRIENDLY_NAME = "Friendly Name";
     private static final String[] TEST_HTTP_URLS = {TEST_HTTP_OTHER_URL1, TEST_HTTP_OTHER_URL2};
     private static final String[] TEST_HTTPS_URLS = {TEST_HTTPS_OTHER_URL1, TEST_HTTPS_OTHER_URL2};
     private static final int TEST_TCP_FAIL_RATE = 99;
@@ -278,7 +291,9 @@
     private static final int DEFAULT_DNS_TIMEOUT_THRESHOLD = 5;
 
     private static final int HANDLER_TIMEOUT_MS = 1000;
-
+    private static final int TEST_MIN_STALL_EVALUATE_INTERVAL_MS = 500;
+    private static final int STALL_EXPECTED_LAST_PROBE_TIME_MS =
+            TEST_MIN_STALL_EVALUATE_INTERVAL_MS + HANDLER_TIMEOUT_MS;
     private static final LinkProperties TEST_LINK_PROPERTIES = new LinkProperties();
 
     // Cannot have a static member for the LinkProperties with captive portal API information, as
@@ -308,6 +323,14 @@
     private static final NetworkCapabilities CELL_NO_INTERNET_CAPABILITIES =
             new NetworkCapabilities().addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
 
+    private static final NetworkCapabilities WIFI_OEM_PAID_CAPABILITIES =
+            new NetworkCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .addCapability(NET_CAPABILITY_OEM_PAID)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+
     /**
      * Fakes DNS responses.
      *
@@ -538,7 +561,7 @@
 
         resetCallbacks();
 
-        setMinDataStallEvaluateInterval(500);
+        setMinDataStallEvaluateInterval(TEST_MIN_STALL_EVALUATE_INTERVAL_MS);
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS);
         setValidDataStallDnsTimeThreshold(500);
         setConsecutiveDnsTimeoutThreshold(5);
@@ -631,7 +654,7 @@
         final WrappedNetworkMonitor nm = new WrappedNetworkMonitor();
         nm.start();
         setNetworkCapabilities(nm, nc);
-        HandlerUtilsKt.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
         mCreatedNetworkMonitors.add(nm);
         when(mTstDependencies.isTcpInfoParsingSupported()).thenReturn(false);
 
@@ -655,12 +678,13 @@
 
     private void setNetworkCapabilities(NetworkMonitor nm, NetworkCapabilities nc) {
         nm.notifyNetworkCapabilitiesChanged(nc);
-        HandlerUtilsKt.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
     }
 
     @Test
     public void testOnlyWifiTransport() {
-        final WrappedNetworkMonitor wnm = makeCellMeteredNetworkMonitor();
+        final WrappedNetworkMonitor wnm = makeMonitor(CELL_METERED_CAPABILITIES);
+        assertFalse(wnm.onlyWifiTransport());
         final NetworkCapabilities nc = new NetworkCapabilities()
                 .addTransportType(TRANSPORT_WIFI)
                 .addTransportType(TRANSPORT_VPN);
@@ -926,6 +950,18 @@
         return info;
     }
 
+    private void setupNoSimCardNeighborMcc() throws Exception {
+        // Enable using neighbor resource by camping mcc feature.
+        doReturn(true).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
+        final List<CellInfo> cellList = new ArrayList<CellInfo>();
+        final int testMcc = 460;
+        cellList.add(makeTestCellInfoGsm(Integer.toString(testMcc)));
+        doReturn(cellList).when(mTelephony).getAllCellInfo();
+        final Configuration config = mResources.getConfiguration();
+        config.mcc = testMcc;
+        doReturn(mMccContext).when(mContext).createConfigurationContext(eq(config));
+    }
+
     @Test
     public void testMakeFallbackUrls() throws Exception {
         final WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
@@ -949,21 +985,28 @@
         assertEquals("http://testUrl1.com", urls[0].toString());
         assertEquals("http://testUrl2.com", urls[1].toString());
 
-        // Value is expected to be replaced by location resource.
-        doReturn(true).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
-
-        final List<CellInfo> cellList = new ArrayList<CellInfo>();
-        final int testMcc = 460;
-        cellList.add(makeTestCellInfoGsm(Integer.toString(testMcc)));
-        doReturn(cellList).when(mTelephony).getAllCellInfo();
-        final Configuration config = mResources.getConfiguration();
-        config.mcc = testMcc;
-        doReturn(mMccContext).when(mContext).createConfigurationContext(eq(config));
+        // Even though the using neighbor resource by camping mcc feature is enabled, the
+        // customized context has been assigned and won't change. So calling
+        // makeCaptivePortalFallbackUrls() still gets the original value.
+        setupNoSimCardNeighborMcc();
         doReturn(new String[] {"http://testUrl3.com"}).when(mMccResource)
                 .getStringArray(R.array.config_captive_portal_fallback_urls);
         urls = wnm.makeCaptivePortalFallbackUrls();
+        assertEquals(urls.length, 2);
+        assertEquals("http://testUrl1.com", urls[0].toString());
+        assertEquals("http://testUrl2.com", urls[1].toString());
+    }
+
+    @Test
+    public void testMakeFallbackUrlsWithCustomizedContext() throws Exception {
+        // Value is expected to be replaced by location resource.
+        setupNoSimCardNeighborMcc();
+        doReturn(new String[] {"http://testUrl.com"}).when(mMccResource)
+                .getStringArray(R.array.config_captive_portal_fallback_urls);
+        final WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        final URL[] urls = wnm.makeCaptivePortalFallbackUrls();
         assertEquals(urls.length, 1);
-        assertEquals("http://testUrl3.com", urls[0].toString());
+        assertEquals("http://testUrl.com", urls[0].toString());
     }
 
     private static CellIdentityGsm makeCellIdentityGsm(int lac, int cid, int arfcn, int bsic,
@@ -1041,6 +1084,34 @@
         runPortalNetworkTest();
     }
 
+    @Test
+    public void testIsCaptivePortal_Http200EmptyResponse() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 200);
+        // Invalid if there is no content (can't login to an empty page)
+        runNetworkTest(VALIDATION_RESULT_INVALID, 0 /* probesSucceeded */, null);
+        verify(mCallbacks, never()).showProvisioningNotification(any(), any());
+    }
+
+    private void doCaptivePortal200ResponseTest(String expectedRedirectUrl) throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 200);
+        doReturn(100L).when(mHttpConnection).getContentLengthLong();
+        // Redirect URL was null before S
+        runNetworkTest(VALIDATION_RESULT_PORTAL, 0 /* probesSucceeded */, expectedRedirectUrl);
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS)).showProvisioningNotification(any(), any());
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testIsCaptivePortal_HttpProbeIs200Portal_R() throws Exception {
+        doCaptivePortal200ResponseTest(null);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testIsCaptivePortal_HttpProbeIs200Portal() throws Exception {
+        doCaptivePortal200ResponseTest(TEST_HTTP_URL);
+    }
+
     private void setupPrivateIpResponse(String privateAddr) throws Exception {
         setSslException(mHttpsConnection);
         setPortal302(mHttpConnection);
@@ -1090,6 +1161,14 @@
     }
 
     @Test
+    public void testIsCaptivePortal_HttpSucceedFallbackProbeIsPortal() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 204);
+        setPortal302(mFallbackConnection);
+        runPortalNetworkTest();
+    }
+
+    @Test
     public void testIsCaptivePortal_FallbackProbeIsNotPortal() throws Exception {
         setSslException(mHttpsConnection);
         setStatus(mHttpConnection, 500);
@@ -1120,7 +1199,8 @@
 
         // Second check should be triggered automatically after the reevaluate delay, and uses the
         // URL chosen by mRandom
-        // This test is appropriate to cover reevaluate behavior as long as the timeout is short
+        // Ensure that the reevaluate delay is not changed to a large value, otherwise this test
+        // would block for too long and a different test strategy should be used.
         assertTrue(INITIAL_REEVALUATE_DELAY_MS < 2000);
         verify(mOtherFallbackConnection, timeout(INITIAL_REEVALUATE_DELAY_MS + HANDLER_TIMEOUT_MS))
                 .getResponseCode();
@@ -1414,13 +1494,13 @@
     }
 
     @Test
-    public void testIsCaptivePortal_FallbackSpecIsPartial() throws Exception {
+    public void testIsCaptivePortal_FallbackSpecIsFail() throws Exception {
         setupFallbackSpec();
         set302(mOtherFallbackConnection, "https://www.google.com/test?q=3");
 
-        // HTTPS failed, fallback spec went through -> partial connectivity
-        runPartialConnectivityNetworkTest(
-                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK);
+        runNetworkTest(VALIDATION_RESULT_INVALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK,
+                null /* redirectUrl */);
         verify(mOtherFallbackConnection, times(1)).getResponseCode();
         verify(mFallbackConnection, never()).getResponseCode();
     }
@@ -1520,6 +1600,32 @@
     }
 
     @Test
+    public void testIsCaptivePortal_TestUrlsWithUrlOverlays() throws Exception {
+        setupResourceForMultipleProbes();
+        doReturn(TEST_HTTPS_URL).when(mResources)
+                .getString(R.string.config_captive_portal_https_url);
+        doReturn(TEST_HTTP_URL).when(mResources)
+                .getString(R.string.config_captive_portal_http_url);
+
+        setDeviceConfig(TEST_URL_EXPIRATION_TIME,
+                String.valueOf(currentTimeMillis() + TimeUnit.MINUTES.toMillis(9)));
+        setDeviceConfig(TEST_CAPTIVE_PORTAL_HTTPS_URL, TEST_OVERRIDE_URL);
+        setDeviceConfig(TEST_CAPTIVE_PORTAL_HTTP_URL, TEST_OVERRIDE_URL);
+        setStatus(mTestOverriddenUrlConnection, 204);
+
+        runValidatedNetworkTest();
+        verify(mHttpsConnection, never()).getResponseCode();
+        verify(mHttpConnection, never()).getResponseCode();
+        verify(mOtherHttpsConnection1, never()).getResponseCode();
+        verify(mOtherHttpsConnection2, never()).getResponseCode();
+        verify(mOtherHttpConnection1, never()).getResponseCode();
+        verify(mOtherHttpConnection2, never()).getResponseCode();
+
+        // Used for both HTTP and HTTPS: can be called once (if HTTPS validates first) or twice
+        verify(mTestOverriddenUrlConnection, atLeastOnce()).getResponseCode();
+    }
+
+    @Test
     public void testIsDataStall_EvaluationDisabled() {
         setDataStallEvaluationType(0);
         WrappedNetworkMonitor wrappedMonitor = makeCellMeteredNetworkMonitor();
@@ -1543,7 +1649,8 @@
         wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
         assertFalse(wrappedMonitor.isDataStall());
 
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        wrappedMonitor.setLastProbeTime(
+                SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         makeDnsTimeoutEvent(wrappedMonitor, DEFAULT_DNS_TIMEOUT_THRESHOLD);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(
@@ -1553,7 +1660,8 @@
     @Test
     public void testIsDataStall_EvaluationDnsWithDnsTimeoutCount() throws Exception {
         WrappedNetworkMonitor wrappedMonitor = makeCellMeteredNetworkMonitor();
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        wrappedMonitor.setLastProbeTime(
+                SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         makeDnsTimeoutEvent(wrappedMonitor, 3);
         assertFalse(wrappedMonitor.isDataStall());
         // Reset consecutive timeout counts.
@@ -1571,7 +1679,8 @@
         // Set the value to larger than the default dns log size.
         setConsecutiveDnsTimeoutThreshold(51);
         wrappedMonitor = makeCellMeteredNetworkMonitor();
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        wrappedMonitor.setLastProbeTime(
+                SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         makeDnsTimeoutEvent(wrappedMonitor, 50);
         assertFalse(wrappedMonitor.isDataStall());
 
@@ -1603,7 +1712,8 @@
         wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
         makeDnsTimeoutEvent(wrappedMonitor, DEFAULT_DNS_TIMEOUT_THRESHOLD);
         assertFalse(wrappedMonitor.isDataStall());
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        wrappedMonitor.setLastProbeTime(
+                SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(
                 matchDnsDataStallParcelable(DEFAULT_DNS_TIMEOUT_THRESHOLD));
@@ -1614,7 +1724,8 @@
         wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
         makeDnsTimeoutEvent(wrappedMonitor, DEFAULT_DNS_TIMEOUT_THRESHOLD);
         assertFalse(wrappedMonitor.isDataStall());
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        wrappedMonitor.setLastProbeTime(
+                SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         assertFalse(wrappedMonitor.isDataStall());
     }
 
@@ -1630,7 +1741,7 @@
         // Trigger a tcp event immediately.
         setTcpPollingInterval(0);
         wrappedMonitor.sendTcpPollingEvent();
-        HandlerUtilsKt.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
         assertFalse(wrappedMonitor.isDataStall());
 
         when(mTst.getLatestReceivedCount()).thenReturn(0);
@@ -1638,19 +1749,38 @@
         // Trigger a tcp event immediately.
         setTcpPollingInterval(0);
         wrappedMonitor.sendTcpPollingEvent();
-        HandlerUtilsKt.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(matchTcpDataStallParcelable());
     }
 
     @Test
+    public void testIsDataStall_EvaluationDnsAndTcp() throws Exception {
+        setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS | DATA_STALL_EVALUATION_TYPE_TCP);
+        setupTcpDataStall();
+        final WrappedNetworkMonitor nm = makeMonitor(CELL_METERED_CAPABILITIES);
+        nm.setLastProbeTime(SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
+        makeDnsTimeoutEvent(nm, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+        assertTrue(nm.isDataStall());
+        verify(mCallbacks).notifyDataStallSuspected(
+                matchDnsAndTcpDataStallParcelable(DEFAULT_DNS_TIMEOUT_THRESHOLD));
+
+        when(mTst.getLatestReceivedCount()).thenReturn(5);
+        // Trigger a tcp event immediately.
+        setTcpPollingInterval(0);
+        nm.sendTcpPollingEvent();
+        HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
+        assertFalse(nm.isDataStall());
+    }
+
+    @Test
     public void testIsDataStall_DisableTcp() {
         // Disable tcp detection with only DNS detect. keep the tcp signal but set to no DNS signal.
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS);
         WrappedNetworkMonitor wrappedMonitor = makeMonitor(CELL_METERED_CAPABILITIES);
         makeDnsSuccessEvent(wrappedMonitor, 1);
         wrappedMonitor.sendTcpPollingEvent();
-        HandlerUtilsKt.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
         assertFalse(wrappedMonitor.isDataStall());
         verify(mTst, never()).isDataStallSuspected();
         verify(mTst, never()).pollSocketsInfo();
@@ -1665,11 +1795,85 @@
         runFailedNetworkTest();
     }
 
+    private void doValidationSkippedTest(NetworkCapabilities nc) throws Exception {
+        // For S+, the RESULT_SKIPPED bit will be included on networks that both do not require
+        // validation and for which validation is not performed.
+        final int validationResult = ShimUtils.isAtLeastS()
+                ? NETWORK_VALIDATION_RESULT_VALID | NETWORK_VALIDATION_RESULT_SKIPPED
+                : NETWORK_VALIDATION_RESULT_VALID;
+        runNetworkTest(TEST_LINK_PROPERTIES, nc, validationResult,
+                0 /* probesSucceeded */, null /* redirectUrl */);
+        verify(mCleartextDnsNetwork, never()).openConnection(any());
+    }
+
     @Test
     public void testNoInternetCapabilityValidated() throws Exception {
-        runNetworkTest(TEST_LINK_PROPERTIES, CELL_NO_INTERNET_CAPABILITIES,
-                NETWORK_VALIDATION_RESULT_VALID, 0 /* probesSucceeded */, null /* redirectUrl */);
-        verify(mCleartextDnsNetwork, never()).openConnection(any());
+        doValidationSkippedTest(CELL_NO_INTERNET_CAPABILITIES);
+    }
+
+    @Test
+    public void testNoTrustedCapabilityValidated() throws Exception {
+        final NetworkCapabilities.Builder nc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_CELLULAR);
+        if (ShimUtils.isAtLeastS()) {
+            nc.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        }
+        doValidationSkippedTest(nc.build());
+    }
+
+    @Test
+    public void testRestrictedCapabilityValidated() throws Exception {
+        final NetworkCapabilities.Builder nc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .addTransportType(TRANSPORT_CELLULAR);
+        if (ShimUtils.isAtLeastS()) {
+            nc.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        }
+        doValidationSkippedTest(nc.build());
+    }
+
+    private NetworkCapabilities getVcnUnderlyingCarrierWifiCaps() {
+        // Must be called from within the test because NOT_VCN_MANAGED is an invalid capability
+        // value up to Android R. Thus, this must be guarded by an SDK check in tests that use this.
+        return new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+    }
+
+    @Test
+    public void testVcnUnderlyingNetwork() throws Exception {
+        assumeTrue(ShimUtils.isAtLeastS());
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        final NetworkMonitor nm = runNetworkTest(
+                TEST_LINK_PROPERTIES, getVcnUnderlyingCarrierWifiCaps(),
+                NETWORK_VALIDATION_RESULT_VALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS,
+                null /* redirectUrl */);
+        assertEquals(NETWORK_VALIDATION_RESULT_VALID,
+                nm.getEvaluationState().getEvaluationResult());
+    }
+
+    @Test
+    public void testVcnUnderlyingNetworkBadNetwork() throws Exception {
+        assumeTrue(ShimUtils.isAtLeastS());
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 500);
+        setStatus(mFallbackConnection, 404);
+
+        final NetworkMonitor nm = runNetworkTest(
+                TEST_LINK_PROPERTIES, getVcnUnderlyingCarrierWifiCaps(),
+                VALIDATION_RESULT_INVALID, 0 /* probesSucceeded */, null /* redirectUrl */);
+        assertEquals(VALIDATION_RESULT_INVALID,
+                nm.getEvaluationState().getEvaluationResult());
     }
 
     @Test
@@ -1773,7 +1977,7 @@
 
         wnm.forceReevaluation(Process.myUid());
         // ProbeCompleted should be reset to 0
-        HandlerUtilsKt.waitForIdle(wnm.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(wnm.getHandler(), HANDLER_TIMEOUT_MS);
         assertEquals(wnm.getEvaluationState().getProbeCompletedResult(), 0);
         verifyNetworkTested(NETWORK_VALIDATION_RESULT_VALID, PROBES_PRIVDNS_VALID);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS)).notifyProbeStatusChanged(
@@ -1879,28 +2083,28 @@
 
     @Test
     public void testDataStall_StallTcpSuspectedAndSendMetricsOnCell() throws Exception {
-        testDataStall_StallTcpSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_CELLULAR,
-                CELL_METERED_CAPABILITIES);
+        testDataStall_StallTcpSuspectedAndSendMetrics(CELL_METERED_CAPABILITIES);
     }
 
     @Test
     public void testDataStall_StallTcpSuspectedAndSendMetricsOnWifi() throws Exception {
-        testDataStall_StallTcpSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_WIFI,
-                WIFI_NOT_METERED_CAPABILITIES);
+        testDataStall_StallTcpSuspectedAndSendMetrics(WIFI_NOT_METERED_CAPABILITIES);
     }
 
-    private void testDataStall_StallTcpSuspectedAndSendMetrics(int transport,
-            NetworkCapabilities nc) throws Exception {
+    private void testDataStall_StallTcpSuspectedAndSendMetrics(NetworkCapabilities nc)
+            throws Exception {
         assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
         setupTcpDataStall();
+        setTcpPollingInterval(0);
         // NM suspects data stall from TCP signal and sends data stall metrics.
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
         final WrappedNetworkMonitor nm = prepareNetworkMonitorForVerifyDataStall(nc);
-
         // Trigger a tcp event immediately.
-        setTcpPollingInterval(0);
         nm.sendTcpPollingEvent();
-        verifySendDataStallDetectionStats(nm, DATA_STALL_EVALUATION_TYPE_TCP, transport);
+        // Allow only one transport type in the context of this test for simplification.
+        final int[] transports = nc.getTransportTypes();
+        assertEquals(1, transports.length);
+        verifySendDataStallDetectionStats(nm, DATA_STALL_EVALUATION_TYPE_TCP, transports[0]);
     }
 
     private WrappedNetworkMonitor prepareNetworkMonitorForVerifyDataStall(NetworkCapabilities nc)
@@ -1909,25 +2113,20 @@
         // evaluation will only start from validated state.
         setStatus(mHttpsConnection, 204);
         final WrappedNetworkMonitor nm;
-        final int[] transports = nc.getTransportTypes();
-        // Though multiple transport types are allowed, use the first transport type for
-        // simplification.
-        switch (transports[0]) {
-            case NetworkCapabilities.TRANSPORT_CELLULAR:
-                nm = makeCellMeteredNetworkMonitor();
-                break;
-            case NetworkCapabilities.TRANSPORT_WIFI:
-                nm = makeWifiNotMeteredNetworkMonitor();
-                setupTestWifiInfo();
-                break;
-            default:
-                nm = null;
-                fail("Undefined transport type");
+        // Allow only one transport type in the context of this test for simplification.
+        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            nm = makeCellMeteredNetworkMonitor();
+        } else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+            nm = makeWifiNotMeteredNetworkMonitor();
+            setupTestWifiInfo();
+        } else {
+            nm = null;
+            fail("Undefined transport type");
         }
         nm.notifyNetworkConnected(TEST_LINK_PROPERTIES, nc);
         verifyNetworkTested(NETWORK_VALIDATION_RESULT_VALID,
                 NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
-        nm.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        nm.setLastProbeTime(SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         return nm;
     }
 
@@ -1949,6 +2148,8 @@
                 ArgumentCaptor.forClass(DataStallDetectionStats.class);
         verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(1))
                 .writeDataStallDetectionStats(statsCaptor.capture(), probeResultCaptor.capture());
+        // Ensure probe will not stop due to rate-limiting mechanism.
+        nm.setLastProbeTime(SystemClock.elapsedRealtime() - STALL_EXPECTED_LAST_PROBE_TIME_MS);
         assertTrue(nm.isDataStall());
         assertTrue(probeResultCaptor.getValue().isSuccessful());
         verifyTestDataStallDetectionStats(evalType, transport, statsCaptor.getValue());
@@ -2147,13 +2348,16 @@
         setStatus(mFallbackConnection, 500);
         runPartialConnectivityNetworkTest(
                 NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP);
+    }
 
-        resetCallbacks();
+    @Test
+    public void testIsCaptivePortal_OnlyFallbackSucceed() throws Exception {
         setStatus(mHttpsConnection, 500);
         setStatus(mHttpConnection, 500);
         setStatus(mFallbackConnection, 204);
-        runPartialConnectivityNetworkTest(
-                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK);
+        runNetworkTest(VALIDATION_RESULT_INVALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK,
+                null /* redirectUrl */);
     }
 
     private void assertIpAddressArrayEquals(String[] expected, InetAddress[] actual) {
@@ -2213,8 +2417,10 @@
         setStatus(mFallbackConnection, 204);
         nm.forceReevaluation(Process.myUid());
         // Expect to send HTTP, HTTPs, FALLBACK and evaluation results.
-        verifyNetworkTested(NETWORK_VALIDATION_RESULT_PARTIAL,
-                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK);
+        verifyNetworkTested(VALIDATION_RESULT_INVALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK,
+                null /* redirectUrl */);
+        HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
     }
 
     @Test
@@ -2463,6 +2669,118 @@
         verify(mCleartextDnsNetwork, times(4)).openConnection(any());
     }
 
+    @Test
+    public void testIsCaptivePortal_FromExternalSource() throws Exception {
+        assumeTrue(CaptivePortalDataShimImpl.isSupported());
+        assumeTrue(ShimUtils.isAtLeastS());
+        when(mDependencies.isFeatureEnabled(any(), eq(NAMESPACE_CONNECTIVITY),
+                eq(DISMISS_PORTAL_IN_VALIDATED_NETWORK), anyBoolean())).thenReturn(true);
+        final NetworkMonitor monitor = makeMonitor(WIFI_NOT_METERED_CAPABILITIES);
+
+        NetworkInformationShim networkShim = NetworkInformationShimImpl.newInstance();
+        CaptivePortalDataShim captivePortalData = new CaptivePortalDataShimImpl(
+                new CaptivePortalData.Builder().setCaptive(true).build());
+        final LinkProperties linkProperties = new LinkProperties(TEST_LINK_PROPERTIES);
+        networkShim.setCaptivePortalData(linkProperties, captivePortalData);
+        CaptivePortalDataShim captivePortalDataShim =
+                networkShim.getCaptivePortalData(linkProperties);
+
+        try {
+            // Set up T&C captive portal info from Passpoint
+            captivePortalData = captivePortalDataShim.withPasspointInfo(TEST_FRIENDLY_NAME,
+                    Uri.parse(TEST_VENUE_INFO_URL), Uri.parse(TEST_LOGIN_URL));
+        } catch (UnsupportedApiLevelException e) {
+            // Minimum API level for this test is 31
+            return;
+        }
+
+        networkShim.setCaptivePortalData(linkProperties, captivePortalData);
+        monitor.notifyLinkPropertiesChanged(linkProperties);
+        final NetworkCapabilities networkCapabilities =
+                new NetworkCapabilities(WIFI_NOT_METERED_CAPABILITIES);
+        monitor.notifyNetworkConnected(linkProperties, networkCapabilities);
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .showProvisioningNotification(any(), any());
+        assertEquals(1, mRegisteredReceivers.size());
+        verifyNetworkTested(VALIDATION_RESULT_PORTAL, 0 /* probesSucceeded */, TEST_LOGIN_URL);
+
+        // Force reevaluation and confirm that the network is still captive
+        HandlerUtils.waitForIdle(monitor.getHandler(), HANDLER_TIMEOUT_MS);
+        resetCallbacks();
+        monitor.forceReevaluation(Process.myUid());
+        assertEquals(monitor.getEvaluationState().getProbeCompletedResult(), 0);
+        verifyNetworkTested(VALIDATION_RESULT_PORTAL, 0 /* probesSucceeded */, TEST_LOGIN_URL);
+
+        // Check that startCaptivePortalApp sends the expected intent.
+        monitor.launchCaptivePortalApp();
+
+        verify(mCm, timeout(HANDLER_TIMEOUT_MS).times(1)).startCaptivePortalApp(
+                argThat(network -> TEST_NETID == network.netId),
+                argThat(bundle -> bundle.getString(
+                        ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL).equals(TEST_LOGIN_URL)
+                        && TEST_NETID == ((Network) bundle.getParcelable(
+                        ConnectivityManager.EXTRA_NETWORK)).netId));
+    }
+
+    @Test
+    public void testOemPaidNetworkValidated() throws Exception {
+        setValidProbes();
+
+        final NetworkMonitor nm = runNetworkTest(TEST_LINK_PROPERTIES,
+                WIFI_OEM_PAID_CAPABILITIES,
+                NETWORK_VALIDATION_RESULT_VALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS,
+                null /* redirectUrl */);
+        assertEquals(NETWORK_VALIDATION_RESULT_VALID,
+                nm.getEvaluationState().getEvaluationResult());
+    }
+
+    @Test
+    public void testOemPaidNetwork_AllProbesFailed() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 500);
+        setStatus(mFallbackConnection, 404);
+
+        runNetworkTest(TEST_LINK_PROPERTIES,
+                WIFI_OEM_PAID_CAPABILITIES,
+                VALIDATION_RESULT_INVALID, 0 /* probesSucceeded */, null /* redirectUrl */);
+    }
+
+    @Test
+    public void testOemPaidNetworkNoInternetCapabilityValidated() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 500);
+        setStatus(mFallbackConnection, 404);
+
+        final NetworkCapabilities networkCapabilities =
+                new NetworkCapabilities(WIFI_OEM_PAID_CAPABILITIES);
+        networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
+
+        final int validationResult = ShimUtils.isAtLeastS()
+                ? NETWORK_VALIDATION_RESULT_VALID | NETWORK_VALIDATION_RESULT_SKIPPED
+                : NETWORK_VALIDATION_RESULT_VALID;
+        runNetworkTest(TEST_LINK_PROPERTIES, networkCapabilities,
+                validationResult, 0 /* probesSucceeded */, null /* redirectUrl */);
+
+        verify(mCleartextDnsNetwork, never()).openConnection(any());
+        verify(mHttpsConnection, never()).getResponseCode();
+        verify(mHttpConnection, never()).getResponseCode();
+        verify(mFallbackConnection, never()).getResponseCode();
+    }
+
+    @Test
+    public void testOemPaidNetwork_CaptivePortalNotLaunched() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mFallbackConnection, 404);
+        setPortal302(mHttpConnection);
+
+        runNetworkTest(TEST_LINK_PROPERTIES, WIFI_OEM_PAID_CAPABILITIES,
+                VALIDATION_RESULT_PORTAL, 0 /* probesSucceeded */,
+                TEST_LOGIN_URL);
+
+        verify(mCallbacks, never()).showProvisioningNotification(any(), any());
+    }
+
     private void setupResourceForMultipleProbes() {
         // Configure the resource to send multiple probe.
         when(mResources.getStringArray(R.array.config_captive_portal_https_urls))
@@ -2510,8 +2828,8 @@
     }
 
     private void setTcpPollingInterval(int time) {
-        when(mDependencies.getDeviceConfigPropertyInt(any(),
-                eq(CONFIG_DATA_STALL_TCP_POLLING_INTERVAL), anyInt())).thenReturn(time);
+        doReturn(time).when(mDependencies).getDeviceConfigPropertyInt(any(),
+                eq(CONFIG_DATA_STALL_TCP_POLLING_INTERVAL), anyInt());
     }
 
     private void setFallbackUrl(String url) {
@@ -2591,7 +2909,7 @@
         final NetworkMonitor monitor = makeMonitor(nc);
         monitor.notifyNetworkConnected(lp, nc);
         verifyNetworkTested(testResult, probesSucceeded, redirectUrl);
-        HandlerUtilsKt.waitForIdle(monitor.getHandler(), HANDLER_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(monitor.getHandler(), HANDLER_TIMEOUT_MS);
 
         return monitor;
     }
@@ -2686,13 +3004,20 @@
                 && Objects.equals(p.redirectUrl, redirectUrl));
     }
 
+    private DataStallReportParcelable matchDnsAndTcpDataStallParcelable(final int timeoutCount) {
+        return argThat(p ->
+                (p.detectionMethod & ConstantsShim.DETECTION_METHOD_DNS_EVENTS) != 0
+                && (p.detectionMethod & ConstantsShim.DETECTION_METHOD_TCP_METRICS) != 0
+                && p.dnsConsecutiveTimeouts == timeoutCount);
+    }
+
     private DataStallReportParcelable matchDnsDataStallParcelable(final int timeoutCount) {
-        return argThat(p -> p.detectionMethod == ConstantsShim.DETECTION_METHOD_DNS_EVENTS
+        return argThat(p -> (p.detectionMethod & ConstantsShim.DETECTION_METHOD_DNS_EVENTS) != 0
                 && p.dnsConsecutiveTimeouts == timeoutCount);
     }
 
     private DataStallReportParcelable matchTcpDataStallParcelable() {
-        return argThat(p -> p.detectionMethod == ConstantsShim.DETECTION_METHOD_TCP_METRICS);
+        return argThat(p -> (p.detectionMethod & ConstantsShim.DETECTION_METHOD_TCP_METRICS) != 0);
     }
 }
 
diff --git a/tests/unit/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/unit/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreServiceTest.java
index c0bdc4c..73edcc7 100644
--- a/tests/unit/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreServiceTest.java
+++ b/tests/unit/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -506,7 +506,7 @@
         // Verify that this test does not miss any new field added later.
         // If any field is added to NetworkAttributes it must be tested here for storing
         // and retrieving.
-        assertEquals(5, Arrays.stream(NetworkAttributes.class.getDeclaredFields())
+        assertEquals(6, Arrays.stream(NetworkAttributes.class.getDeclaredFields())
                 .filter(f -> !Modifier.isStatic(f.getModifiers())).count());
     }