Snap for 11273583 from 499764925927c69d361dc754cedd2def1bafc26c to mainline-wifi-release

Change-Id: If91b6adb34094709d454a6c110815ac58613da5d
diff --git a/Android.bp b/Android.bp
index 8ee5d6f..a7f57ae 100644
--- a/Android.bp
+++ b/Android.bp
@@ -58,6 +58,7 @@
     name: "NetworkStackNextEnableDefaults",
     enabled: true,
 }
+
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may have different "enabled" values
 // depending on the branch
@@ -72,7 +73,7 @@
         "framework-connectivity-t",
         "framework-statsd",
         "framework-wifi",
-    ]
+    ],
 }
 
 // Common defaults for NetworkStack integration tests, root tests and coverage tests
@@ -85,7 +86,7 @@
 
 java_defaults {
     name: "NetworkStackReleaseApiLevel",
-    defaults:["NetworkStackReleaseTargetSdk"],
+    defaults: ["NetworkStackReleaseTargetSdk"],
     sdk_version: module_34_version,
     libs: [
         "framework-configinfrastructure",
@@ -93,7 +94,7 @@
         "framework-connectivity-t",
         "framework-statsd",
         "framework-wifi",
-    ]
+    ],
 }
 
 // Libraries for the API shims
@@ -103,12 +104,12 @@
         "androidx.annotation_annotation",
         "networkstack-aidl-latest",
     ],
-    static_libs : [
-        "modules-utils-build_system"
+    static_libs: [
+        "modules-utils-build_system",
     ],
     apex_available: [
         "com.android.tethering",
-        "//apex_available:platform",  // For InProcessNetworkStack
+        "//apex_available:platform", // For InProcessNetworkStack
     ],
     min_sdk_version: "30",
 }
@@ -124,6 +125,9 @@
     srcs: ["apishim/common/**/*.java"],
     sdk_version: "system_current",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Each level of the shims (29, 30, ...) is its own java_library compiled against the corresponding
@@ -137,6 +141,9 @@
     ],
     sdk_version: "system_29",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -153,6 +160,7 @@
     visibility: ["//visibility:private"],
     lint: {
         strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
     },
 }
 
@@ -176,6 +184,9 @@
     ],
     sdk_version: "module_31",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -196,6 +207,9 @@
     ],
     sdk_version: "module_33",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -217,6 +231,9 @@
     ],
     sdk_version: module_34_version,
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Shims for APIs being added to the current development version of Android. These APIs are not
@@ -228,7 +245,10 @@
 // are part of the stable shims and scanned when generating jarjar rules.
 java_library {
     name: "NetworkStackApi35Shims",
-    defaults: ["NetworkStackShimsDefaults", "ConnectivityNextEnableDefaults"],
+    defaults: [
+        "NetworkStackShimsDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
     srcs: [
         "apishim/35/**/*.java",
     ],
@@ -243,10 +263,13 @@
         "framework-connectivity",
         "framework-connectivity-t.stubs.module_lib",
         "framework-tethering",
-        "android.net.ipsec.ike.stubs.module_lib"
+        "android.net.ipsec.ike.stubs.module_lib",
     ],
     sdk_version: "module_current",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // API current uses the API current shims directly.
@@ -274,6 +297,9 @@
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // API stable uses jarjar to rename the latest stable apishim package from
@@ -281,7 +307,10 @@
 // the networkstack code.
 java_library {
     name: "NetworkStackApiStableShims",
-    defaults: ["NetworkStackShimsDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackShimsDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: [
         "NetworkStackShimsCommon",
         "NetworkStackApi29Shims",
@@ -297,6 +326,9 @@
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Common defaults for android libraries containing network stack code, used to compile variants of
@@ -320,9 +352,9 @@
         "datastallprotosnano",
         "statsprotos",
         "captiveportal-lib",
-        "net-utils-device-common",
         "net-utils-device-common-ip",
         "net-utils-device-common-netlink",
+        "net-utils-device-common-struct",
     ],
 }
 
@@ -336,11 +368,10 @@
     ],
     srcs: [
         "src/**/*.java",
-        ":statslog-networkstack-java-gen-current"
+        ":statslog-networkstack-java-gen-current",
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
-        "net-utils-device-common-struct",
     ],
     manifest: "AndroidManifestBase.xml",
     visibility: [
@@ -349,19 +380,24 @@
         "//packages/modules/NetworkStack/tests/unit",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
     name: "NetworkStackApiStableLib",
-    defaults: ["NetworkStackReleaseApiLevel", "NetworkStackAndroidLibraryDefaults"],
+    defaults: [
+        "NetworkStackReleaseApiLevel",
+        "NetworkStackAndroidLibraryDefaults",
+    ],
     srcs: [
         "src/**/*.java",
         ":statslog-networkstack-java-gen-stable",
     ],
     static_libs: [
         "NetworkStackApiStableShims",
-        "net-utils-device-common-struct",
     ],
     manifest: "AndroidManifestBase.xml",
     visibility: [
@@ -372,7 +408,10 @@
         "//packages/modules/NetworkStack/tests/unit",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -390,6 +429,7 @@
         "modules-utils-build",
         "net-utils-framework-common",
         "networkstack-client",
+        "net-utils-device-common",
     ],
     // If this library is ever used outside of tests, it should not use "Dhcp*Packet", and specify
     // its contents explicitly.
@@ -397,6 +437,9 @@
         "//packages/modules/Connectivity/Tethering/tests/integration",
         "//packages/modules/Connectivity/tests/cts/net",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_genrule {
@@ -456,12 +499,18 @@
     certificate: "platform",
     manifest: "AndroidManifest_InProcess.xml",
     // InProcessNetworkStack is a replacement for NetworkStack
-    overrides: ["NetworkStack", "NetworkStackNext"],
+    overrides: [
+        "NetworkStack",
+        "NetworkStackNext",
+    ],
     // The InProcessNetworkStack goes together with the PlatformCaptivePortalLogin, which replaces
     // the default CaptivePortalLogin.
     required: [
         "PlatformCaptivePortalLogin",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Pre-merge the AndroidManifest for NetworkStackNext, so that its manifest can be merged on top
@@ -473,7 +522,10 @@
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: ["NetworkStackApiCurrentLib"],
-    manifest: "AndroidManifest.xml"
+    manifest: "AndroidManifest.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // NetworkStack build targeting the current API release, for testing on in-development SDK
@@ -491,13 +543,19 @@
         "privapp_whitelist_com.android.networkstack",
     ],
     updatable: true,
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Updatable network stack for finalized API
 android_app {
     name: "NetworkStack",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackAppDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: ["NetworkStackApiStableLib"],
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
@@ -505,13 +563,16 @@
         "privapp_whitelist_com.android.networkstack",
     ],
     updatable: true,
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 cc_library_shared {
     name: "libnetworkstackutilsjni",
     srcs: [
-        "jni/network_stack_utils_jni.cpp"
+        "jni/network_stack_utils_jni.cpp",
     ],
     header_libs: [
         "bpf_headers",
@@ -549,8 +610,8 @@
     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" +
-         " --minApiLevel 30",
+        " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
+        " --minApiLevel 30",
     out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
 }
 
@@ -558,12 +619,11 @@
     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 30 --compileApiLevel 30",
+        " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
+        " --minApiLevel 30 --compileApiLevel 30",
     out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
 }
 
-
 version_code_networkstack_next = "300000000"
 version_code_networkstack_test = "999999999"
 
@@ -571,21 +631,27 @@
     name: "NetworkStackTestAndroidManifest",
     srcs: ["AndroidManifest.xml"],
     out: ["TestAndroidManifest.xml"],
-    cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\""
-        + version_code_networkstack_test
-        + "\"/' $(in) > $(out)",
+    cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\"" +
+        version_code_networkstack_test +
+        "\"/' $(in) > $(out)",
     visibility: ["//visibility:private"],
 }
 
 android_app {
     name: "TestNetworkStack",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackAppDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: ["NetworkStackApiStableLib"],
     certificate: "networkstack",
     manifest: ":NetworkStackTestAndroidManifest",
     required: [
         "privapp_whitelist_com.android.networkstack",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // When adding or modifying protos, the jarjar rules and possibly proguard rules need
@@ -602,4 +668,7 @@
         "networkstackprotos",
     ],
     defaults: ["NetworkStackReleaseApiLevel"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 259a403..a65d0d5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -45,6 +45,10 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <!-- Signature permission defined in NetworkStackStub -->
     <uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK" />
+    <!-- Crash while reading deviceownerName security exception  -->
+    <uses-permission android:name="android.permission.MANAGE_USERS"/>
+    <!-- ends here -->
+
     <application
         android:extractNativeLibs="false"
         android:persistent="true"
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index e056e3b..060f0da 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -48,21 +48,54 @@
             enabled: false,
         },
     },
-    versions: [
-        "1",
-        "2",
-        "3",
-        "4",
-        "5",
-        "6",
-        "7",
-        "8",
-        "9",
-        "10",
-    ],
+
     visibility: [
         "//system/tools/aidl/build",
     ],
+    versions_with_info: [
+        {
+            version: "1",
+            imports: [],
+        },
+        {
+            version: "2",
+            imports: [],
+        },
+        {
+            version: "3",
+            imports: [],
+        },
+        {
+            version: "4",
+            imports: [],
+        },
+        {
+            version: "5",
+            imports: [],
+        },
+        {
+            version: "6",
+            imports: [],
+        },
+        {
+            version: "7",
+            imports: [],
+        },
+        {
+            version: "8",
+            imports: [],
+        },
+        {
+            version: "9",
+            imports: [],
+        },
+        {
+            version: "10",
+            imports: [],
+        },
+
+    ],
+
 }
 
 aidl_interface {
@@ -164,12 +197,13 @@
             version: "19",
             imports: ["ipmemorystore-aidl-interfaces-V10"],
         },
+        {
+            version: "20",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
 
     ],
-
-    // "frozen: true" is removed manually after each freeze, this property cannot be unrecognized
-    // in some downstream branches and TH will get the build error, see b/262507066 for details.
-
+    frozen: true,
 }
 
 java_library {
@@ -178,7 +212,7 @@
     min_sdk_version: "30",
     static_libs: [
         "ipmemorystore-aidl-interfaces-V10-java",
-        "networkstack-aidl-interfaces-V19-java",
+        "networkstack-aidl-interfaces-V20-java",
     ],
     visibility: ["//packages/modules/NetworkStack:__subpackages__"],
     apex_available: [
@@ -187,6 +221,9 @@
         "com.android.tethering",
         "com.android.wifi",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -231,4 +268,7 @@
         "com.android.tethering",
         "com.android.wifi",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/.hash
new file mode 100644
index 0000000..e447bdd
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/.hash
@@ -0,0 +1 @@
+fdb37ac2749e155d9d5aa51541ec254dc36dc353
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..771deda
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..31f2194
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..fb13c0c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkMonitor.aidl
@@ -0,0 +1,60 @@
+/**
+ * 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);
+  oneway void notifyNetworkConnectedParcel(in android.net.networkstack.aidl.NetworkMonitorParameters params);
+  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 = 0x01;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 0x04;
+  const int NETWORK_VALIDATION_PROBE_DNS = 0x04;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 0x08;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 0x10;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 0x20;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 0x40;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..36eda8e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..8120ffc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..0b6b778
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..6103774
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6a597e6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..83796ee
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..4b3fff5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..18cf954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..4d6d5a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..ab62fe7
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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(equals=true, toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+  int privateDnsMode = (-1) /* -1 */;
+  String dohName = "";
+  String[] dohIps = {};
+  String dohPath = "";
+  int dohPort = (-1) /* -1 */;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..7061f1e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,64 @@
+/*
+**
+** 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 {
+  /**
+   * @deprecated use ipv4ProvisioningMode instead.
+   */
+  boolean enableIPv4;
+  /**
+   * @deprecated use ipv6ProvisioningMode instead.
+   */
+  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;
+  int ipv4ProvisioningMode;
+  int ipv6ProvisioningMode;
+  boolean uniqueEui64AddressesOnly;
+  int creatorUid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..94fc27f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..0e1c21c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3cd8860
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..7997936
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,49 @@
+/**
+ *
+ * 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;
+  int leasesSubnetPrefixLength = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..9312f47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..1109f35
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..ab8577c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..b81ec20
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ip/IIpClient.aidl
@@ -0,0 +1,59 @@
+/**
+ * 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);
+  oneway void updateApfCapabilities(in android.net.apf.ApfCapabilities apfCapabilities);
+  const int PROV_IPV4_DISABLED = 0x00;
+  const int PROV_IPV4_STATIC = 0x01;
+  const int PROV_IPV4_DHCP = 0x02;
+  const int PROV_IPV6_DISABLED = 0x00;
+  const int PROV_IPV6_SLAAC = 0x01;
+  const int PROV_IPV6_LINKLOCAL = 0x02;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9d36419
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/ip/IIpClientCallbacks.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 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);
+  oneway void onReachabilityFailure(in android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable lossInfo);
+  oneway void setMaxDtimMultiplier(int multiplier);
+  const int DTIM_MULTIPLIER_RESET = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/NetworkMonitorParameters.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
new file mode 100644
index 0000000..2ab9db0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
@@ -0,0 +1,41 @@
+/**
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+@JavaDerive(equals=true, toString=true)
+parcelable NetworkMonitorParameters {
+  android.net.NetworkAgentConfig networkAgentConfig;
+  android.net.NetworkCapabilities networkCapabilities;
+  android.net.LinkProperties linkProperties;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..eea3e0d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/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/20/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
new file mode 100644
index 0000000..bb88434
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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.ip;
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ReachabilityLossInfoParcelable {
+  String message;
+  android.net.networkstack.aidl.ip.ReachabilityLossReason reason;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
new file mode 100644
index 0000000..f9bb3c4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/20/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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.ip;
+@Backing(type="int")
+enum ReachabilityLossReason {
+  ROAM,
+  CONFIRM,
+  ORGANIC,
+}
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 fba524b..7061f1e 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
@@ -60,4 +60,5 @@
   int ipv4ProvisioningMode;
   int ipv6ProvisioningMode;
   boolean uniqueEui64AddressesOnly;
+  int creatorUid;
 }
diff --git a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
index c4d7866..7ab612f 100644
--- a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
@@ -50,4 +50,5 @@
     int ipv4ProvisioningMode;
     int ipv6ProvisioningMode;
     boolean uniqueEui64AddressesOnly;
+    int creatorUid;
 }
diff --git a/common/networkstackclient/src/android/net/shared/Layer2Information.java b/common/networkstackclient/src/android/net/shared/Layer2Information.java
index 8cdd018..f384509 100644
--- a/common/networkstackclient/src/android/net/shared/Layer2Information.java
+++ b/common/networkstackclient/src/android/net/shared/Layer2Information.java
@@ -30,7 +30,6 @@
     public final String mCluster;
     @Nullable
     public final MacAddress mBssid;
-
     /**
      * Create a Layer2Information with the specified configuration.
      */
diff --git a/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java b/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
index f3631f2..16ad201 100644
--- a/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
+++ b/common/networkstackclient/src/android/net/shared/ProvisioningConfiguration.java
@@ -228,6 +228,14 @@
         }
 
         /**
+         * Specify the UID of the remote entity that created this Network.
+         */
+        public Builder withCreatorUid(int creatoruid) {
+            mConfig.mCreatorUid = creatoruid;
+            return this;
+        }
+
+        /**
          * Specify the information elements included in wifi scan result that was obtained
          * prior to connecting to the access point, if this is a WiFi network.
          *
@@ -492,6 +500,7 @@
     public List<DhcpOption> mDhcpOptions;
     public int mIPv4ProvisioningMode = PROV_IPV4_DHCP;
     public int mIPv6ProvisioningMode = PROV_IPV6_SLAAC;
+    public int mCreatorUid;
 
     public ProvisioningConfiguration() {} // used by Builder
 
@@ -510,6 +519,7 @@
         mIPv6AddrGenMode = other.mIPv6AddrGenMode;
         mNetwork = other.mNetwork;
         mDisplayName = other.mDisplayName;
+        mCreatorUid = other.mCreatorUid;
         mScanResultInfo = other.mScanResultInfo;
         mLayer2Info = other.mLayer2Info;
         mDhcpOptions = other.mDhcpOptions;
@@ -540,6 +550,7 @@
         p.ipv6AddrGenMode = mIPv6AddrGenMode;
         p.network = mNetwork;
         p.displayName = mDisplayName;
+        p.creatorUid = mCreatorUid;
         p.scanResultInfo = (mScanResultInfo == null) ? null : mScanResultInfo.toStableParcelable();
         p.layer2Info = (mLayer2Info == null) ? null : mLayer2Info.toStableParcelable();
         p.options = (mDhcpOptions == null) ? null : new ArrayList<>(mDhcpOptions);
@@ -572,6 +583,7 @@
         config.mIPv6AddrGenMode = p.ipv6AddrGenMode;
         config.mNetwork = p.network;
         config.mDisplayName = p.displayName;
+        config.mCreatorUid = p.creatorUid;
         config.mScanResultInfo = ScanResultInfo.fromStableParcelable(p.scanResultInfo);
         config.mLayer2Info = Layer2Information.fromStableParcelable(p.layer2Info);
         config.mDhcpOptions = (p.options == null) ? null : new ArrayList<>(p.options);
@@ -630,6 +642,7 @@
                 .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
                 .add("mNetwork: " + mNetwork)
                 .add("mDisplayName: " + mDisplayName)
+                .add("mCreatorUid:" + mCreatorUid)
                 .add("mScanResultInfo: " + mScanResultInfo)
                 .add("mLayer2Info: " + mLayer2Info)
                 .add("mDhcpOptions: " + mDhcpOptions)
@@ -680,7 +693,8 @@
                 && Objects.equals(mLayer2Info, other.mLayer2Info)
                 && dhcpOptionListEquals(mDhcpOptions, other.mDhcpOptions)
                 && mIPv4ProvisioningMode == other.mIPv4ProvisioningMode
-                && mIPv6ProvisioningMode == other.mIPv6ProvisioningMode;
+                && mIPv6ProvisioningMode == other.mIPv6ProvisioningMode
+                && mCreatorUid == other.mCreatorUid;
     }
 
     public boolean isValid() {
diff --git a/res/values/config.xml b/res/values/config.xml
index 15152e3..aed375c 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -125,4 +125,7 @@
 
     <!-- Whether to validate DUN networks. This is unused and always true on U+. -->
     <bool name="config_validate_dun_networks">false</bool>
+
+    <!-- Configuration for including DHCP domain search list option -->
+    <bool name="config_dhcp_client_domain_search_list">false</bool>
 </resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 20ee593..9c2172a 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -90,6 +90,8 @@
             <item type="drawable" name="icon_wifi"/>
             <!-- Whether to validate DUN networks. This is unused and always true on U+. -->
             <item type="bool" name="config_validate_dun_networks"/>
+            <!-- Configuration for including DHCP client domain search list option. -->
+            <item type="bool" name="config_dhcp_client_domain_search_list"/>
         </policy>
     </overlayable>
 </resources>
diff --git a/src/android/net/DhcpResults.java b/src/android/net/DhcpResults.java
index 7c271c3..d590c07 100644
--- a/src/android/net/DhcpResults.java
+++ b/src/android/net/DhcpResults.java
@@ -63,6 +63,8 @@
     @Nullable
     public String captivePortalApiUrl;
 
+    public ArrayList<String> dmnsrchList = new ArrayList<>();
+
     public DhcpResults() {
         super();
     }
@@ -100,6 +102,7 @@
             mtu = source.mtu;
             serverHostName = source.serverHostName;
             captivePortalApiUrl = source.captivePortalApiUrl;
+            dmnsrchList = source.dmnsrchList;
         }
     }
 
@@ -135,6 +138,7 @@
         mtu = 0;
         serverHostName = null;
         captivePortalApiUrl = null;
+        dmnsrchList.clear();
     }
 
     @Override
@@ -167,13 +171,14 @@
                 && Objects.equals(serverHostName, target.serverHostName)
                 && leaseDuration == target.leaseDuration
                 && mtu == target.mtu
-                && Objects.equals(captivePortalApiUrl, target.captivePortalApiUrl);
+                && Objects.equals(captivePortalApiUrl, target.captivePortalApiUrl)
+                && dmnsrchList.equals(target.dmnsrchList);
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(ipAddress, gateway, dnsServers, domains, serverAddress, vendorInfo,
-            serverHostName, captivePortalApiUrl) + 43 *  leaseDuration + 67 * mtu;
+            serverHostName, captivePortalApiUrl, dmnsrchList) + 43 *  leaseDuration + 67 * mtu;
     }
 
     /**
@@ -302,6 +307,15 @@
         return domains;
     }
 
+    /**
+     * Append the domain search list strings separated by space to domain string.
+     */
+    public String appendDomainsSearchList() {
+        final String domainsPrefix = domains == null ? "" : domains;
+        final String separator = domains != null && dmnsrchList.size() > 0 ? " " : "";
+        return domainsPrefix + separator + TextUtils.join(" ", dmnsrchList);
+    }
+
     public void setDomains(String domains) {
         this.domains = domains;
     }
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
new file mode 100644
index 0000000..0527666
--- /dev/null
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2023 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.apf;
+
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Common counter class for {@code ApfFilter} and {@code LegacyApfFilter}.
+ *
+ * @hide
+ */
+public class ApfCounterTracker {
+    /**
+     * APF packet counters.
+     *
+     * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
+     * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
+     * the last writable 32bit word.
+     */
+    @VisibleForTesting
+    public enum Counter {
+        RESERVED_OOB,  // Points to offset 0 from the end of the buffer (out-of-bounds)
+        TOTAL_PACKETS,
+        PASSED_ARP,
+        PASSED_DHCP,
+        PASSED_IPV4,
+        PASSED_IPV6_NON_ICMP,
+        PASSED_IPV4_UNICAST,
+        PASSED_IPV6_ICMP,
+        PASSED_IPV6_UNICAST_NON_ICMP,
+        PASSED_ARP_NON_IPV4,
+        PASSED_ARP_UNKNOWN,
+        PASSED_ARP_UNICAST_REPLY,
+        PASSED_NON_IP_UNICAST,
+        PASSED_MDNS,
+        DROPPED_ETH_BROADCAST,
+        DROPPED_RA,
+        DROPPED_GARP_REPLY,
+        DROPPED_ARP_OTHER_HOST,
+        DROPPED_IPV4_L2_BROADCAST,
+        DROPPED_IPV4_BROADCAST_ADDR,
+        DROPPED_IPV4_BROADCAST_NET,
+        DROPPED_IPV4_MULTICAST,
+        DROPPED_IPV6_ROUTER_SOLICITATION,
+        DROPPED_IPV6_MULTICAST_NA,
+        DROPPED_IPV6_MULTICAST,
+        DROPPED_IPV6_MULTICAST_PING,
+        DROPPED_IPV6_NON_ICMP_MULTICAST,
+        DROPPED_802_3_FRAME,
+        DROPPED_ETHERTYPE_DENYLISTED,
+        DROPPED_ARP_REPLY_SPA_NO_HOST,
+        DROPPED_IPV4_KEEPALIVE_ACK,
+        DROPPED_IPV6_KEEPALIVE_ACK,
+        DROPPED_IPV4_NATT_KEEPALIVE,
+        DROPPED_MDNS,
+        DROPPED_IPV4_TCP_PORT7_UNICAST,
+        DROPPED_ARP_NON_IPV4,
+        DROPPED_ARP_UNKNOWN;
+
+        /**
+         * Returns the negative byte offset from the end of the APF data segment for
+         * a given counter.
+         */
+        public int offset() {
+            return -this.ordinal() * 4;  // Currently, all counters are 32bit long.
+        }
+
+        /**
+         * Returns the total size of the data segment in bytes.
+         */
+        public static int totalSize() {
+            return (Counter.class.getEnumConstants().length - 1) * 4;
+        }
+    }
+
+    private final List<Counter> mCounterList;
+    // Store the counters' value
+    private final Map<Counter, Long> mCounters = new ArrayMap<>();
+
+    public ApfCounterTracker() {
+        Counter[] counters = Counter.class.getEnumConstants();
+        mCounterList = Arrays.asList(counters).subList(1, counters.length);
+    }
+
+    /**
+     * Get the value of a counter from APF data.
+     */
+    public static long getCounterValue(byte[] data, Counter counter)
+            throws ArrayIndexOutOfBoundsException {
+        // Follow the same wrap-around addressing scheme of the interpreter.
+        int offset = counter.offset();
+        if (offset < 0) {
+            offset = data.length + offset;
+        }
+
+        // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
+        long value = 0;
+        for (int i = 0; i < 4; i++) {
+            value = value << 8 | (data[offset] & 0xFF);
+            offset++;
+        }
+        return value;
+    }
+
+    /**
+     * Update counters from APF data.
+     */
+    public void updateCountersFromData(byte[] data) {
+        if (data == null) return;
+        for (Counter counter : mCounterList) {
+            long value;
+            try {
+                value = getCounterValue(data, counter);
+            } catch (ArrayIndexOutOfBoundsException e) {
+                value = 0;
+            }
+            long oldValue = mCounters.getOrDefault(counter, 0L);
+            // All counters are increamental
+            if (value > oldValue) {
+                mCounters.put(counter, value);
+            }
+        }
+    }
+
+    /**
+     * Get counters map.
+     */
+    public Map<Counter, Long> getCounters() {
+        return mCounters;
+    }
+}
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 3ab5e0e..ee2990b 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -45,11 +45,13 @@
 import android.net.LinkProperties;
 import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
 import android.os.PowerManager;
 import android.os.SystemClock;
+import android.stats.connectivity.NetworkQuirkEvent;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.format.DateUtils;
@@ -69,6 +71,9 @@
 import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SocketUtils;
+import com.android.networkstack.metrics.ApfSessionInfoMetrics;
+import com.android.networkstack.metrics.IpClientRaInfoMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
 import com.android.networkstack.util.NetworkStackUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -87,6 +92,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * For networks that support packet filtering via APF programs, {@code ApfFilter}
@@ -117,61 +123,16 @@
         public int minRdnssLifetimeSec;
         public int acceptRaMinLft;
         public boolean shouldHandleLightDoze;
+        public long minMetricsSessionDurationMs;
     }
 
-    /**
-     * APF packet counters.
-     *
-     * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
-     * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
-     * the last writable 32bit word.
-     */
-    @VisibleForTesting
-    public static enum Counter {
-        RESERVED_OOB,  // Points to offset 0 from the end of the buffer (out-of-bounds)
-        TOTAL_PACKETS,
-        PASSED_ARP,
-        PASSED_DHCP,
-        PASSED_IPV4,
-        PASSED_IPV6_NON_ICMP,
-        PASSED_IPV4_UNICAST,
-        PASSED_IPV6_ICMP,
-        PASSED_IPV6_UNICAST_NON_ICMP,
-        PASSED_ARP_UNICAST_REPLY,
-        PASSED_NON_IP_UNICAST,
-        PASSED_MDNS,
-        DROPPED_ETH_BROADCAST,
-        DROPPED_RA,
-        DROPPED_GARP_REPLY,
-        DROPPED_ARP_OTHER_HOST,
-        DROPPED_IPV4_L2_BROADCAST,
-        DROPPED_IPV4_BROADCAST_ADDR,
-        DROPPED_IPV4_BROADCAST_NET,
-        DROPPED_IPV4_MULTICAST,
-        DROPPED_IPV6_ROUTER_SOLICITATION,
-        DROPPED_IPV6_MULTICAST_NA,
-        DROPPED_IPV6_MULTICAST,
-        DROPPED_IPV6_MULTICAST_PING,
-        DROPPED_IPV6_NON_ICMP_MULTICAST,
-        DROPPED_802_3_FRAME,
-        DROPPED_ETHERTYPE_BLACKLISTED,
-        DROPPED_ARP_REPLY_SPA_NO_HOST,
-        DROPPED_IPV4_KEEPALIVE_ACK,
-        DROPPED_IPV6_KEEPALIVE_ACK,
-        DROPPED_IPV4_NATT_KEEPALIVE,
-        DROPPED_MDNS,
-        DROPPED_ARP_NON_IPV4,
-        DROPPED_ARP_UNKNOWN;
-
-        // Returns the negative byte offset from the end of the APF data segment for
-        // a given counter.
-        public int offset() {
-            return - this.ordinal() * 4;  // Currently, all counters are 32bit long.
-        }
-
-        // Returns the total size of the data segment in bytes.
-        public static int totalSize() {
-            return (Counter.class.getEnumConstants().length - 1) * 4;
+    /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
+    public static class Clock {
+        /**
+         * @see SystemClock#elapsedRealtime
+         */
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
         }
     }
 
@@ -238,6 +199,7 @@
     // Endianness is not an issue for this constant because the APF interpreter always operates in
     // network byte order.
     private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
+    private static final int IPV4_FRAGMENT_MORE_FRAGS_MASK = 0x2000;
     private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
     private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
     private static final int IPV4_ANY_HOST_ADDRESS = 0;
@@ -262,7 +224,7 @@
     private static final int IPPROTO_HOPOPTS = 0;
 
     // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
-    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
+    private static final int TCP_UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
     private static final int UDP_HEADER_LEN = 8;
 
     private static final int TCP_HEADER_SIZE_OFFSET = 12;
@@ -293,6 +255,8 @@
     private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
             {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
     private static final int MDNS_PORT = 5353;
+
+    private static final int ECHO_PORT = 7;
     private static final int DNS_HEADER_LEN = 12;
     private static final int DNS_QDCOUNT_OFFSET = 4;
     // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT, or the
@@ -303,7 +267,6 @@
             ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
 
 
-
     private final ApfCapabilities mApfCapabilities;
     private final IpClientCallbacksWrapper mIpClientCallback;
     private final InterfaceParams mInterfaceParams;
@@ -322,14 +285,38 @@
     private final boolean mDrop802_3Frames;
     private final int[] mEthTypeBlackList;
 
+    private final Clock mClock;
+    private final ApfCounterTracker mApfCounterTracker = new ApfCounterTracker();
+    @GuardedBy("this")
+    private long mSessionStartMs = 0;
+    @GuardedBy("this")
+    private int mNumParseErrorRas = 0;
+    @GuardedBy("this")
+    private int mNumZeroLifetimeRas = 0;
+    @GuardedBy("this")
+    private int mLowestRouterLifetimeSeconds = Integer.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestPioValidLifetimeSeconds = Long.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestRioRouteLifetimeSeconds = Long.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestRdnssLifetimeSeconds = Long.MAX_VALUE;
+
     // Ignore non-zero RDNSS lifetimes below this value.
     private final int mMinRdnssLifetimeSec;
 
+    // Minimum session time for metrics, duration less than this time will not be logged.
+    private final long mMinMetricsSessionDurationMs;
+
     // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO,
     // and PIO valid lifetimes.
     private final int mAcceptRaMinLft;
     private final boolean mShouldHandleLightDoze;
 
+    private final NetworkQuirkMetrics mNetworkQuirkMetrics;
+    private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
+    private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
+
     private static boolean isDeviceIdleModeChangedAction(Intent intent) {
         return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction());
     }
@@ -386,13 +373,23 @@
     private final Dependencies mDependencies;
 
     public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback) {
-        this(context, config, ifParams, ipClientCallback, new Dependencies(context));
+            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics) {
+        this(context, config, ifParams, ipClientCallback, networkQuirkMetrics,
+                new Dependencies(context), new Clock());
     }
 
     @VisibleForTesting
     public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback, Dependencies dependencies) {
+            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+            Dependencies dependencies) {
+        this(context, config, ifParams, ipClientCallback, networkQuirkMetrics, dependencies,
+                new Clock());
+    }
+
+    @VisibleForTesting
+    public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
+            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+            Dependencies dependencies, Clock clock) {
         mApfCapabilities = config.apfCapabilities;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
@@ -403,6 +400,12 @@
         mContext = context;
         mShouldHandleLightDoze = config.shouldHandleLightDoze;
         mDependencies = dependencies;
+        mNetworkQuirkMetrics = networkQuirkMetrics;
+        mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
+        mApfSessionInfoMetrics = dependencies.getApfSessionInfoMetrics();
+        mClock = clock;
+        mSessionStartMs = mClock.elapsedRealtime();
+        mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
 
         if (mApfCapabilities.hasDataAccess()) {
             mCountAndPassLabel = "countAndPass";
@@ -457,10 +460,25 @@
         public void removeBroadcastReceiver(@NonNull final BroadcastReceiver receiver) {
             mContext.unregisterReceiver(receiver);
         }
+
+        /**
+         * Get a ApfSessionInfoMetrics instance.
+         */
+        public ApfSessionInfoMetrics getApfSessionInfoMetrics() {
+            return new ApfSessionInfoMetrics();
+        }
+
+        /**
+         * Get a IpClientRaInfoMetrics instance.
+         */
+        public IpClientRaInfoMetrics getIpClientRaInfoMetrics() {
+            return new IpClientRaInfoMetrics();
+        }
     }
 
     public synchronized void setDataSnapshot(byte[] data) {
         mDataSnapshot = data;
+        mApfCounterTracker.updateCountersFromData(data);
     }
 
     private void log(String s) {
@@ -516,7 +534,9 @@
                 // a crash on some older devices (b/78905546).
                 if (mApfCapabilities.hasDataAccess()) {
                     byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
-                    mIpClientCallback.installPacketFilter(zeroes);
+                    if (!mIpClientCallback.installPacketFilter(zeroes)) {
+                        sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+                    }
                 }
 
                 // Install basic filters
@@ -537,7 +557,7 @@
     // Returns seconds since device boot.
     @VisibleForTesting
     protected int secondsSinceBoot() {
-        return (int) (SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS);
+        return (int) (mClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS);
     }
 
     public static class InvalidRaException extends Exception {
@@ -642,6 +662,14 @@
         // List of sections in the packet.
         private final ArrayList<PacketSection> mPacketSections = new ArrayList<>();
 
+        // Router lifetime in packet
+        private final int mRouterLifetime;
+        // Minimum valid lifetime of PIOs in packet, Long.MAX_VALUE means not seen.
+        private long mMinPioValidLifetime = Long.MAX_VALUE;
+        // Minimum route lifetime of RIOs in packet, Long.MAX_VALUE means not seen.
+        private long mMinRioRouteLifetime = Long.MAX_VALUE;
+        // Minimum lifetime of RDNSSs in packet, Long.MAX_VALUE means not seen.
+        private long mMinRdnssLifetime = Long.MAX_VALUE;
         // Minimum lifetime in packet
         private final int mMinLifetime;
         // When the packet was last captured, in seconds since Unix Epoch
@@ -825,6 +853,34 @@
             return lifetime;
         }
 
+        /**
+         * Return the router lifetime of the RA
+         */
+        public int routerLifetime() {
+            return mRouterLifetime;
+        }
+
+        /**
+         * Return the minimum valid lifetime in PIOs
+         */
+        public long minPioValidLifetime() {
+            return mMinPioValidLifetime;
+        }
+
+        /**
+         * Return the minimum route lifetime in RIOs
+         */
+        public long minRioRouteLifetime() {
+            return mMinRioRouteLifetime;
+        }
+
+        /**
+         * Return the minimum lifetime in RDNSSs
+         */
+        public long minRdnssLifetime() {
+            return mMinRdnssLifetime;
+        }
+
         // Note that this parses RA and may throw InvalidRaException (from
         // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
         // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
@@ -860,8 +916,9 @@
 
             // Parse router lifetime
             addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, routerLifetime, mAcceptRaMinLft);
+            mRouterLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
+            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, mRouterLifetime, mAcceptRaMinLft);
+            if (mRouterLifetime == 0) mNumZeroLifetimeRas++;
 
             // Add remaining fields (reachable time and retransmission timer) to match section.
             addMatchUntil(ICMP6_RA_OPTION_OFFSET);
@@ -885,6 +942,9 @@
                         lifetime = getUint32(mPacket, mPacket.position());
                         addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
                                 lifetime, mAcceptRaMinLft);
+                        mMinPioValidLifetime = getMinForPositiveValue(
+                                mMinPioValidLifetime, lifetime);
+                        if (lifetime == 0) mNumZeroLifetimeRas++;
 
                         // Parse preferred lifetime
                         lifetime = getUint32(mPacket, mPacket.position());
@@ -901,10 +961,15 @@
                     case ICMP6_RDNSS_OPTION_TYPE:
                         mRdnssOptionOffsets.add(position);
                         lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec);
+                        mMinRdnssLifetime = getMinForPositiveValue(mMinRdnssLifetime, lifetime);
+                        if (lifetime == 0) mNumZeroLifetimeRas++;
                         break;
                     case ICMP6_ROUTE_INFO_OPTION_TYPE:
                         mRioOptionOffsets.add(position);
                         lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft);
+                        mMinRioRouteLifetime = getMinForPositiveValue(
+                                mMinRioRouteLifetime, lifetime);
+                        if (lifetime == 0) mNumZeroLifetimeRas++;
                         break;
                     case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE:
                     case ICMP6_MTU_OPTION_TYPE:
@@ -1083,7 +1148,7 @@
                 // Generate code to match the packet bytes.
                 if (section.type == PacketSection.Type.MATCH) {
                     gen.addLoadImmediate(Register.R0, section.start);
-                    gen.addJumpIfBytesNotEqual(Register.R0,
+                    gen.addJumpIfBytesAtR0NotEqual(
                             Arrays.copyOfRange(mPacket.array(), section.start,
                                     section.start + section.length),
                             nextFilterLabel);
@@ -1218,7 +1283,7 @@
             final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
             // Check payload length is 1
@@ -1233,11 +1298,11 @@
             // Check that the ports match
             gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addAdd(ETH_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel);
 
             // Payload offset = R0 + UDP header length
             gen.addAdd(UDP_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
             gen.addJump(mCountAndDropLabel);
@@ -1333,7 +1398,7 @@
             final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // Skip to the next filter if it's not zero-sized :
             // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
@@ -1355,7 +1420,7 @@
             gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN);
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
             gen.addJump(mCountAndDropLabel);
@@ -1416,6 +1481,12 @@
     // How many times the program was updated since we started.
     @GuardedBy("this")
     private int mNumProgramUpdates = 0;
+    // The maximum program size that updated since we started.
+    @GuardedBy("this")
+    private int mMaxProgramSize = 0;
+    // The maximum number of distinct RAs
+    @GuardedBy("this")
+    private int mMaxDistinctRas = 0;
     // How many times the program was updated since we started for allowing multicast traffic.
     @GuardedBy("this")
     private int mNumProgramUpdatesAllowingMulticast = 0;
@@ -1451,7 +1522,7 @@
         // Drop if not ARP IPv4.
         gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
         maybeSetupCounter(gen, Counter.DROPPED_ARP_NON_IPV4);
-        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndDropLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndDropLabel);
 
         // Drop if unknown ARP opcode.
         gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
@@ -1467,7 +1538,7 @@
         // Pass if non-broadcast reply.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
 
         // Either a request, or a broadcast reply.
         gen.defineLabel(checkTargetIPv4);
@@ -1481,7 +1552,7 @@
             // and broadcast replies with a different target IPv4 address.
             gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
             maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
-            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel);
         }
 
         maybeSetupCounter(gen, Counter.PASSED_ARP);
@@ -1518,18 +1589,18 @@
             // Check it's UDP.
             gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
             gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
-            // Check it's not a fragment. This matches the BPF filter installed by the DHCP client.
+            // Check it's not a fragment or is the initial fragment.
             gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
             gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter);
             // Check it's addressed to DHCP client port.
             gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
-            gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+            gen.addLoad16Indexed(Register.R0, TCP_UDP_DESTINATION_PORT_OFFSET);
             gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
             // Check it's DHCP to our MAC address.
             gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
             // NOTE: Relies on R1 containing IPv4 header offset.
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
             maybeSetupCounter(gen, Counter.PASSED_DHCP);
             gen.addJump(mCountAndPassLabel);
 
@@ -1551,24 +1622,26 @@
                 int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
                 gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
             }
+        }
 
-            // If any TCP keepalive filter matches, drop
-            generateV4KeepaliveFilters(gen);
+        // If any TCP keepalive filter matches, drop
+        generateV4KeepaliveFilters(gen);
 
-            // If any NAT-T keepalive filter matches, drop
-            generateV4NattKeepaliveFilters(gen);
+        // If any NAT-T keepalive filter matches, drop
+        generateV4NattKeepaliveFilters(gen);
 
+        // If TCP unicast on port 7, drop
+        generateV4TcpPort7FilterLocked(gen);
+
+        if (mMulticastFilter) {
             // Otherwise, this is an IPv4 unicast, pass
             // If L2 broadcast packet, drop.
             // 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, ETHER_BROADCAST, mCountAndPassLabel);
+            gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
             gen.addJump(mCountAndDropLabel);
-        } else {
-            generateV4KeepaliveFilters(gen);
-            generateV4NattKeepaliveFilters(gen);
         }
 
         // Otherwise, pass
@@ -1690,8 +1763,7 @@
         // 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, unsolicitedNaDropPrefix,
-                skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
         maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
         gen.addJump(mCountAndDropLabel);
@@ -1748,16 +1820,21 @@
 
         // Check it's L2 mDNS multicast address.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
-                skipMdnsv4Filter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter);
 
         // Checks it's IPv4.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
         gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter);
 
+        // Check it's not a fragment.
+        gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+        gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK,
+                skipMdnsFilter);
+
         // Checks it's UDP.
         gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
         gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+
         // Set R1 to IPv4 header.
         gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
         gen.addJump(checkMdnsUdpPort);
@@ -1766,8 +1843,7 @@
 
         // Checks it's L2 mDNS multicast address.
         // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
-                skipMdnsFilter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
 
         // Checks it's IPv6.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
@@ -1782,7 +1858,7 @@
 
         // Checks it's mDNS UDP port
         gen.defineLabel(checkMdnsUdpPort);
-        gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+        gen.addLoad16Indexed(Register.R0, TCP_UDP_DESTINATION_PORT_OFFSET);
         gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
 
         gen.addLoad16Indexed(Register.R0, MDNS_QDCOUNT_OFFSET);
@@ -1798,7 +1874,7 @@
         for (int i = 0; i < mMdnsAllowList.size(); ++i) {
             final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
             final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
-            gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck);
+            gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck);
             // QNAME matched
             gen.addJump(mDnsAcceptPacket);
             // QNAME not matched
@@ -1817,6 +1893,37 @@
         gen.defineLabel(skipMdnsFilter);
     }
 
+    /**
+     * Generate filter code to drop IPv4 TCP packets on port 7.
+     *
+     * On entry we know it is IPv4 ethertype, but don't know anything else.
+     * R0/R1 have nothing useful in them, and can be clobbered.
+     */
+    @GuardedBy("this")
+    private void generateV4TcpPort7FilterLocked(ApfGenerator gen)
+            throws IllegalInstructionException {
+        final String skipPort7V4Filter = "skip_port7_v4_filter";
+
+        // Check it's TCP.
+        gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
+        gen.addJumpIfR0NotEquals(IPPROTO_TCP, skipPort7V4Filter);
+
+        // Check it's not a fragment or is the initial fragment.
+        gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+        gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipPort7V4Filter);
+
+        // Check it's destination port 7.
+        gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+        gen.addLoad16Indexed(Register.R0, TCP_UDP_DESTINATION_PORT_OFFSET);
+        gen.addJumpIfR0NotEquals(ECHO_PORT, skipPort7V4Filter);
+
+        // Drop it.
+        maybeSetupCounter(gen, Counter.DROPPED_IPV4_TCP_PORT7_UNICAST);
+        gen.addJump(mCountAndDropLabel);
+
+        // Skip label.
+        gen.defineLabel(skipPort7V4Filter);
+    }
 
     private void generateV6KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
         generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET,
@@ -1844,7 +1951,8 @@
      * </ul>
      */
     @GuardedBy("this")
-    private ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+    @VisibleForTesting
+    protected ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
         // This is guaranteed to succeed because of the check in maybeCreate.
         ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported);
 
@@ -1881,7 +1989,7 @@
         }
 
         // Handle ether-type black list
-        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED);
+        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_DENYLISTED);
         for (int p : mEthTypeBlackList) {
             gen.addJumpIfR0Equals(p, mCountAndDropLabel);
         }
@@ -1912,7 +2020,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, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
         maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
         gen.addJump(mCountAndDropLabel);
 
@@ -1983,6 +2091,7 @@
             // Can't fit the program even without any RA filters?
             if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
                 Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                 return;
             }
 
@@ -1993,6 +2102,7 @@
                 // Stop if we get too big.
                 if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
                     if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
+                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                     break;
                 }
 
@@ -2009,13 +2119,19 @@
             program = gen.generate();
         } catch (IllegalInstructionException|IllegalStateException e) {
             Log.e(TAG, "Failed to generate APF program.", e);
+            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
             return;
         }
-        mIpClientCallback.installPacketFilter(program);
+        // Update data snapshot every time we install a new program
+        mIpClientCallback.startReadPacketFilter();
+        if (!mIpClientCallback.installPacketFilter(program)) {
+            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        }
         mLastTimeInstalledProgram = timeSeconds;
         mLastInstalledProgramMinLifetime = programMinLft;
         mLastInstalledProgram = program;
         mNumProgramUpdates++;
+        mMaxProgramSize = Math.max(mMaxProgramSize, program.length);
 
         if (VDBG) {
             hexDump("Installing filter: ", program, program.length);
@@ -2026,6 +2142,20 @@
         log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
     }
 
+    // Get the minimum value excludes zero. This is used for calculating the lowest lifetime values
+    // in RA packets. Zero lifetimes are excluded because we want to detect whether there is any
+    // unusually small lifetimes but zero lifetime is actually valid (cease to be a default router
+    // or the option is no longer be used). Number of zero lifetime RAs is collected in a different
+    // Metrics.
+    private long getMinForPositiveValue(long oldMinValue, long value) {
+        if (value < 1) return oldMinValue;
+        return Math.min(oldMinValue, value);
+    }
+
+    private int getMinForPositiveValue(int oldMinValue, int value) {
+        return (int) getMinForPositiveValue((long) oldMinValue, (long) value);
+    }
+
     /**
      * Process an RA packet, updating the list of known RAs and installing a new APF program
      * if the current APF program should be updated.
@@ -2039,9 +2169,20 @@
             ra = new Ra(packet, length);
         } catch (Exception e) {
             Log.e(TAG, "Error parsing RA", e);
+            mNumParseErrorRas++;
             return;
         }
 
+        // Update info for Metrics
+        mLowestRouterLifetimeSeconds = getMinForPositiveValue(
+                mLowestRouterLifetimeSeconds, ra.routerLifetime());
+        mLowestPioValidLifetimeSeconds = getMinForPositiveValue(
+                mLowestPioValidLifetimeSeconds, ra.minPioValidLifetime());
+        mLowestRioRouteLifetimeSeconds = getMinForPositiveValue(
+                mLowestRioRouteLifetimeSeconds, ra.minRioRouteLifetime());
+        mLowestRdnssLifetimeSeconds = getMinForPositiveValue(
+                mLowestRdnssLifetimeSeconds, ra.minRdnssLifetime());
+
         // Remove all expired RA filters before trying to match the new RA.
         // TODO: matches() still checks that the old RA filter has not expired. Consider removing
         // that check.
@@ -2077,6 +2218,7 @@
                 return;
             }
         }
+        mMaxDistinctRas = Math.max(mMaxDistinctRas, mRas.size() + 1);
         if (mRas.size() >= MAX_RAS) {
             // Remove the last (i.e. oldest) RA.
             mRas.remove(mRas.size() - 1);
@@ -2096,7 +2238,8 @@
      * filtering using APF programs.
      */
     public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) {
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
         ApfCapabilities apfCapabilities =  config.apfCapabilities;
         if (apfCapabilities == null) return null;
@@ -2113,10 +2256,41 @@
             return null;
         }
 
-        return new ApfFilter(context, config, ifParams, ipClientCallback);
+        return new ApfFilter(context, config, ifParams, ipClientCallback, networkQuirkMetrics);
+    }
+
+    private synchronized void collectAndSendMetrics() {
+        if (mIpClientRaInfoMetrics == null || mApfSessionInfoMetrics == null) return;
+        final long sessionDurationMs = mClock.elapsedRealtime() - mSessionStartMs;
+        if (sessionDurationMs < mMinMetricsSessionDurationMs) return;
+
+        // Collect and send IpClientRaInfoMetrics.
+        mIpClientRaInfoMetrics.setMaxNumberOfDistinctRas(mMaxDistinctRas);
+        mIpClientRaInfoMetrics.setNumberOfZeroLifetimeRas(mNumZeroLifetimeRas);
+        mIpClientRaInfoMetrics.setNumberOfParsingErrorRas(mNumParseErrorRas);
+        mIpClientRaInfoMetrics.setLowestRouterLifetimeSeconds(mLowestRouterLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestPioValidLifetimeSeconds(mLowestPioValidLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestRioRouteLifetimeSeconds(mLowestRioRouteLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestRdnssLifetimeSeconds(mLowestRdnssLifetimeSeconds);
+        mIpClientRaInfoMetrics.statsWrite();
+
+        // Collect and send ApfSessionInfoMetrics.
+        mApfSessionInfoMetrics.setVersion(mApfCapabilities.apfVersionSupported);
+        mApfSessionInfoMetrics.setMemorySize(mApfCapabilities.maximumApfProgramSize);
+        mApfSessionInfoMetrics.setApfSessionDurationSeconds(
+                (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS));
+        mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates);
+        mApfSessionInfoMetrics.setMaxProgramSize(mMaxProgramSize);
+        for (Map.Entry<Counter, Long> entry : mApfCounterTracker.getCounters().entrySet()) {
+            if (entry.getValue() > 0) {
+                mApfSessionInfoMetrics.addApfCounter(entry.getKey(), entry.getValue());
+            }
+        }
+        mApfSessionInfoMetrics.statsWrite();
     }
 
     public synchronized void shutdown() {
+        collectAndSendMetrics();
         if (mReceiveThread != null) {
             log("shutting down");
             mReceiveThread.halt();  // Also closes socket.
@@ -2246,23 +2420,6 @@
         installNewProgramLocked();
     }
 
-    static public long counterValue(byte[] data, Counter counter)
-            throws ArrayIndexOutOfBoundsException {
-        // Follow the same wrap-around addressing scheme of the interpreter.
-        int offset = counter.offset();
-        if (offset < 0) {
-            offset = data.length + offset;
-        }
-
-        // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
-        long value = 0;
-        for (int i = 0; i < 4; i++) {
-            value = value << 8 | (data[offset] & 0xFF);
-            offset++;
-        }
-        return value;
-    }
-
     public synchronized void dump(IndentingPrintWriter pw) {
         pw.println("Capabilities: " + mApfCapabilities);
         pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
@@ -2347,11 +2504,17 @@
             try {
                 Counter[] counters = Counter.class.getEnumConstants();
                 for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
-                    long value = counterValue(mDataSnapshot, c);
+                    long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c);
                     // Only print non-zero counters
                     if (value != 0) {
                         pw.println(c.toString() + ": " + value);
                     }
+
+                    // If the counter's value decreases, it may have been cleaned up or there may be
+                    // a bug.
+                    if (value < mApfCounterTracker.getCounters().getOrDefault(c, 0L)) {
+                        Log.e(TAG, "Error: Counter value unexpectedly decreased.");
+                    }
                 }
             } catch (ArrayIndexOutOfBoundsException e) {
                 pw.println("Uh-oh: " + e);
@@ -2406,4 +2569,10 @@
         }
         return result;
     }
+
+    private void sendNetworkQuirkMetrics(final NetworkQuirkEvent event) {
+        if (mNetworkQuirkMetrics == null) return;
+        mNetworkQuirkMetrics.setEvent(event);
+        mNetworkQuirkMetrics.statsWrite();
+    }
 }
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
index e14364e..c6ff441 100644
--- a/src/android/net/apf/ApfGenerator.java
+++ b/src/android/net/apf/ApfGenerator.java
@@ -16,6 +16,11 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfGenerator.Register.R0;
+import static android.net.apf.ApfGenerator.Register.R1;
+
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -26,7 +31,7 @@
  * APF assembler/generator.  A tool for generating an APF program.
  *
  * Call add*() functions to add instructions to the program, then call
- * {@link generate} to get the APF bytecode for the program.
+ * {@link ApfGenerator#generate} to get the APF bytecode for the program.
  *
  * @hide
  */
@@ -41,6 +46,15 @@
     }
     private enum Opcodes {
         LABEL(-1),
+        // Unconditionally pass (if R=0) or drop (if R=1) packet.
+        // An optional unsigned immediate value can be provided to encode the counter number.
+        // If the value is non-zero, the instruction increments the counter.
+        // The counter is located (-4 * counter number) bytes from the end of the data region.
+        // It is a U32 big-endian value and is always incremented by 1.
+        // This is more or less equivalent to: lddw R0, -N4; add R0,1; stdw R0, -N4; {pass,drop}
+        // e.g. "pass", "pass 1", "drop", "drop 1"
+        PASS(0),
+        DROP(0),
         LDB(1),    // Load 1 byte from immediate offset, e.g. "ldb R0, [5]"
         LDH(2),    // Load 2 bytes from immediate offset, e.g. "ldh R0, [5]"
         LDW(3),    // Load 4 bytes from immediate offset, e.g. "ldw R0, [5]"
@@ -54,7 +68,12 @@
         OR(11),    // Or, e.g. "or R0,5"
         SH(12),    // Left shift, e.g, "sh R0, 5" or "sh R0, -5" (shifts right)
         LI(13),    // Load immediate, e.g. "li R0,5" (immediate encoded as signed value)
-        JMP(14),   // Jump, e.g. "jmp label"
+        // Jump, e.g. "jmp label"
+        // In APFv6, we use JMP(R=1) to encode the DATA instruction. DATA is executed as a jump.
+        // It tells how many bytes of the program regions are used to store the data and followed
+        // by the actual data bytes.
+        // "e.g. data 5, abcde"
+        JMP(14),
         JEQ(15),   // Compare equal and branch, e.g. "jeq R0,5,label"
         JNE(16),   // Compare not equal and branch, e.g. "jne R0,5,label"
         JGT(17),   // Compare greater than and branch, e.g. "jgt R0,5,label"
@@ -64,12 +83,17 @@
         EXT(21),   // Followed by immediate indicating ExtendedOpcodes.
         LDDW(22),  // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1"
         STDW(23),  // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
-        WRITE(24),  // Write 1, 2 or 4 bytes imm to the output buffer, e.g. "WRITE 5"
-        // Copy the data from input packet or APF data region to output buffer. Register bit is
-        // used to specify the source of data copy: R=0 means copy from packet, R=1 means copy
-        // from APF data region. The source offset is encoded in the first imm and the copy length
-        // is encoded in the second imm. "e.g. MEMCOPY(R=0), 5, 5"
-        MEMCOPY(25);
+        // Write 1, 2 or 4 bytes immediate to the output buffer and auto-increment the pointer to
+        // write. e.g. "write 5"
+        WRITE(24),
+        // Copy bytes from input packet/APF program/data region to output buffer and
+        // auto-increment the output buffer pointer.
+        // Register bit is used to specify the source of data copy.
+        // R=0 means copy from packet.
+        // R=1 means copy from APF program/data region.
+        // The copy length is stored in (u8)imm2.
+        // e.g. "pktcopy 5, 5" "datacopy 5, 5"
+        PKTDATACOPY(25);
 
         final int value;
 
@@ -86,16 +110,30 @@
         NEG(33),  // Negate, e.g. "neg R0"
         SWAP(34), // Swap, e.g. "swap R0,R1"
         MOVE(35),  // Move, e.g. "move R0,R1"
-        ALLOC(36), // Allocate buffer, "e.g. ALLOC R0"
-        TRANS(37), // Transmit buffer, "e.g. TRANS R0"
-        EWRITE1(38), // Write 1 byte from register to the output buffer, e.g. "EWRITE1 R0"
-        EWRITE2(39), // Write 2 bytes from register to the output buffer, e.g. "EWRITE2 R0"
-        EWRITE4(40), // Write 4 bytes from register to the output buffer, e.g. "EWRITE4 R0"
-        // Copy the data from input packet to output buffer. The source offset is encoded as [Rx
-        // + second imm]. The copy length is encoded in the third imm. "e.g. EPKTCOPY [R0 + 5], 5"
+        // Allocate writable output buffer.
+        // R=0, use register R0 to store the length. R=1, encode the length in the u16 int imm2.
+        // "e.g. allocate R0"
+        // "e.g. allocate 123"
+        ALLOCATE(36),
+        //  Transmit and deallocate the buffer (transmission can be delayed until the program
+        //  terminates). R=0 means discard the buffer, R=1 means transmit the buffer.
+        // "e.g. trans"
+        // "e.g. discard"
+        TRANSMIT(37),
+        DISCARD(37),
+        // Write 1, 2 or 4 byte value from register to the output buffer and auto-increment the
+        // output buffer pointer.
+        // e.g. "ewrite1 r0"
+        EWRITE1(38),
+        EWRITE2(39),
+        EWRITE4(40),
+        // Copy bytes from input packet/APF program/data region to output buffer and
+        // auto-increment the output buffer pointer.
+        // The copy src offset is stored in R0.
+        // when R=0, the copy length is stored in (u8)imm2.
+        // when R=1, the copy length is stored in R1.
+        // e.g. "pktcopy r0, 5", "pktcopy r0, r1", "datacopy r0, 5", "datacopy r0, r1"
         EPKTCOPY(41),
-        // Copy the data from APF data region to output buffer. The source offset is encoded as [Rx
-        // + second imm]. The copy length is encoded in the third imm. "e.g. EDATACOPY [R0 + 5], 5"
         EDATACOPY(42);
 
         final int value;
@@ -115,79 +153,223 @@
         }
     }
 
-    private static class Immediate {
-        public final boolean mSigned;
-        public final byte mImmSize;
+    private enum IntImmediateType {
+        INDETERMINATE_SIZE_SIGNED,
+        INDETERMINATE_SIZE_UNSIGNED,
+        SIGNED_8,
+        UNSIGNED_8,
+        SIGNED_BE16,
+        UNSIGNED_BE16,
+        SIGNED_BE32,
+        UNSIGNED_BE32;
+    }
+
+    private static class IntImmediate {
+        public final IntImmediateType mImmediateType;
         public final int mValue;
 
-        Immediate(int value, boolean signed) {
-            this(value, signed, calculateImmSize(value, signed));
+        IntImmediate(int value, IntImmediateType type) {
+            mImmediateType = type;
+            mValue = value;
         }
 
-        Immediate(int value, boolean signed, byte size) {
-            mValue = value;
-            mSigned = signed;
-            mImmSize = size;
+        private int calculateIndeterminateSize() {
+            switch (mImmediateType) {
+                case INDETERMINATE_SIZE_SIGNED:
+                    return calculateImmSize(mValue, true /* signed */);
+                case INDETERMINATE_SIZE_UNSIGNED:
+                    return calculateImmSize(mValue, false /* signed */);
+                default:
+                    // For IMM with determinate size, return 0 to allow Math.max() calculation in
+                    // caller function.
+                    return 0;
+            }
+        }
+
+        private int getEncodingSize(int immFieldSize) {
+            switch (mImmediateType) {
+                case SIGNED_8:
+                case UNSIGNED_8:
+                    return 1;
+                case SIGNED_BE16:
+                case UNSIGNED_BE16:
+                    return 2;
+                case SIGNED_BE32:
+                case UNSIGNED_BE32:
+                    return 4;
+                case INDETERMINATE_SIZE_SIGNED:
+                case INDETERMINATE_SIZE_UNSIGNED: {
+                    int minSizeRequired = calculateIndeterminateSize();
+                    if (minSizeRequired > immFieldSize) {
+                        throw new IllegalStateException(
+                                String.format("immFieldSize: %d is too small to encode value %d",
+                                        immFieldSize, mValue));
+                    }
+                    return immFieldSize;
+                }
+            }
+            throw new IllegalStateException("UnhandledInvalid IntImmediateType: " + mImmediateType);
+        }
+
+        private int writeValue(byte[] bytecode, Integer writingOffset, int immFieldSize) {
+            return Instruction.writeValue(mValue, bytecode, writingOffset,
+                    getEncodingSize(immFieldSize));
+        }
+
+        public static IntImmediate newSigned(int imm) {
+            return new IntImmediate(imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED);
+        }
+
+        public static IntImmediate newUnsigned(long imm) {
+            // upperBound is 2^32 - 1
+            checkRange("Unsigned IMM", imm, 0 /* lowerBound */,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED);
+        }
+
+        public static IntImmediate newTwosComplementUnsigned(long imm) {
+            checkRange("Unsigned TwosComplement IMM", imm, Integer.MIN_VALUE,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED);
+        }
+
+        public static IntImmediate newTwosComplementSigned(long imm) {
+            checkRange("Signed TwosComplement IMM", imm, Integer.MIN_VALUE,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED);
+        }
+
+        public static IntImmediate newS8(byte imm) {
+            checkRange("S8 IMM", imm, Byte.MIN_VALUE, Byte.MAX_VALUE);
+            return new IntImmediate(imm, IntImmediateType.SIGNED_8);
+        }
+
+        public static IntImmediate newU8(int imm) {
+            checkRange("U8 IMM", imm, 0, 255);
+            return new IntImmediate(imm, IntImmediateType.UNSIGNED_8);
+        }
+
+        public static IntImmediate newS16(short imm) {
+            return new IntImmediate(imm, IntImmediateType.SIGNED_BE16);
+        }
+
+        public static IntImmediate newU16(int imm) {
+            checkRange("U16 IMM", imm, 0, 65535);
+            return new IntImmediate(imm, IntImmediateType.UNSIGNED_BE16);
+        }
+
+        public static IntImmediate newS32(int imm) {
+            return new IntImmediate(imm, IntImmediateType.SIGNED_BE32);
+        }
+
+        public static IntImmediate newU32(long imm) {
+            // upperBound is 2^32 - 1
+            checkRange("U32 IMM", imm, 0 /* lowerBound */,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.UNSIGNED_BE32);
         }
 
         @Override
         public String toString() {
-            return "Immediate{" + "mSigned=" + mSigned + ", mImmSize=" + mImmSize + ", mValue="
-                    + mValue + '}';
+            return "IntImmediate{" + "mImmediateType=" + mImmediateType + ", mValue=" + mValue
+                    + '}';
         }
     }
 
     private class Instruction {
         private final byte mOpcode;   // A "Opcode" value.
         private final byte mRegister; // A "Register" value.
-        private final int mMaxSupportedImms;
-        public final List<Immediate> mImms = new ArrayList<>();
+        public final List<IntImmediate> mIntImms = new ArrayList<>();
         // When mOpcode is a jump:
-        private byte mTargetLabelSize;
+        private int mTargetLabelSize;
+        private int mLenFieldOverride = -1;
         private String mTargetLabel;
         // When mOpcode == Opcodes.LABEL:
         private String mLabel;
-        // When mOpcode == Opcodes.JNEBS:
-        private byte[] mCompareBytes;
+        private byte[] mBytesImm;
         // Offset in bytes from the beginning of this program. Set by {@link ApfGenerator#generate}.
         int offset;
 
         Instruction(Opcodes opcode, Register register) {
-            this(opcode, register, 1 /* maxSupportedImm */);
-        }
-
-        Instruction(Opcodes opcode, Register register, int maxSupportedImms) {
             mOpcode = (byte) opcode.value;
             mRegister = (byte) register.value;
-            mMaxSupportedImms = maxSupportedImms;
+        }
+
+        Instruction(ExtendedOpcodes extendedOpcodes, Register register) {
+            this(Opcodes.EXT, register);
+            addUnsigned(extendedOpcodes.value);
+        }
+
+        Instruction(ExtendedOpcodes extendedOpcodes, int slot, Register register)
+                throws IllegalInstructionException {
+            this(Opcodes.EXT, register);
+            if (slot < 0 || slot >= MEMORY_SLOTS) {
+                throw new IllegalInstructionException("illegal memory slot number: " + slot);
+            }
+            addUnsigned(extendedOpcodes.value + slot);
         }
 
         Instruction(Opcodes opcode) {
-            this(opcode, Register.R0);
+            this(opcode, R0);
         }
 
-        void addUnsignedImm(int imm) {
-            addImm(new Immediate(imm, false));
+        Instruction(ExtendedOpcodes extendedOpcodes) {
+            this(extendedOpcodes, R0);
         }
 
-        void addUnsignedImm(int imm, byte size) {
-            addImm(new Immediate(imm, false, size));
+        Instruction addSigned(int imm) {
+            mIntImms.add(IntImmediate.newSigned(imm));
+            return this;
         }
 
-        void addSignedImm(int imm) {
-            addImm(new Immediate(imm, true));
+        Instruction addUnsigned(int imm) {
+            mIntImms.add(IntImmediate.newUnsigned(imm));
+            return this;
         }
 
-        void addImm(Immediate imm) {
-            if (mImms.size() == mMaxSupportedImms) {
-                throw new IllegalArgumentException(
-                        String.format("Opcode: %d only support at max: %d imms", mOpcode,
-                                mMaxSupportedImms));
-            }
-            mImms.add(imm);
+
+        Instruction addTwosCompSigned(int imm) {
+            mIntImms.add(IntImmediate.newTwosComplementSigned(imm));
+            return this;
         }
 
-        void setLabel(String label) throws IllegalInstructionException {
+
+        Instruction addTwosCompUnsigned(int imm) {
+            mIntImms.add(IntImmediate.newTwosComplementUnsigned(imm));
+            return this;
+        }
+
+        Instruction addS8(byte imm) {
+            mIntImms.add(IntImmediate.newS8(imm));
+            return this;
+        }
+
+        Instruction addU8(int imm) {
+            mIntImms.add(IntImmediate.newU8(imm));
+            return this;
+        }
+
+        Instruction addS16(short imm) {
+            mIntImms.add(IntImmediate.newS16(imm));
+            return this;
+        }
+
+        Instruction addU16(int imm) {
+            mIntImms.add(IntImmediate.newU16(imm));
+            return this;
+        }
+
+        Instruction addS32(int imm) {
+            mIntImms.add(IntImmediate.newS32(imm));
+            return this;
+        }
+
+        Instruction addU32(long imm) {
+            mIntImms.add(IntImmediate.newU32(imm));
+            return this;
+        }
+
+        Instruction setLabel(String label) throws IllegalInstructionException {
             if (mLabels.containsKey(label)) {
                 throw new IllegalInstructionException("duplicate label " + label);
             }
@@ -196,18 +378,23 @@
             }
             mLabel = label;
             mLabels.put(label, this);
+            return this;
         }
 
-        void setTargetLabel(String label) {
+        Instruction setTargetLabel(String label) {
             mTargetLabel = label;
             mTargetLabelSize = 4; // May shrink later on in generate().
+            return this;
         }
 
-        void setCompareBytes(byte[] bytes) {
-            if (mOpcode != Opcodes.JNEBS.value) {
-                throw new IllegalStateException("adding compare bytes to non-JNEBS instruction");
-            }
-            mCompareBytes = bytes;
+        Instruction overrideLenField(int size) {
+            mLenFieldOverride = size;
+            return this;
+        }
+
+        Instruction setBytesImm(byte[] bytes) {
+            mBytesImm = bytes;
+            return this;
         }
 
         /**
@@ -218,23 +405,15 @@
                 return 0;
             }
             int size = 1;
-            byte maxImmSize = getMaxImmSize();
-            // For the copy opcode, the last imm is the length field is always 1 byte
-            if (isCopyOpCode()) {
-                if (mMaxSupportedImms != mImms.size()) {
-                    throw new IllegalStateException(
-                            "mImm size: " + mImms.size() + " doesn't match the mMaxSupportedImms: "
-                                    + mMaxSupportedImms);
-                }
-                size += (mImms.size() - 1) * maxImmSize + mImms.get(mImms.size() - 1).mImmSize;
-            } else {
-                size += mImms.size() * maxImmSize;
+            int indeterminateSize = calculateRequiredIndeterminateSize();
+            for (IntImmediate imm : mIntImms) {
+                size += imm.getEncodingSize(indeterminateSize);
             }
             if (mTargetLabel != null) {
-                size += maxImmSize;
+                size += indeterminateSize;
             }
-            if (mCompareBytes != null) {
-                size += mCompareBytes.length;
+            if (mBytesImm != null) {
+                size += mBytesImm.length;
             }
             return size;
         }
@@ -248,20 +427,34 @@
             if (mTargetLabel == null) {
                 return false;
             }
-            int oldSize = size();
             int oldTargetLabelSize = mTargetLabelSize;
             mTargetLabelSize = calculateImmSize(calculateTargetLabelOffset(), false);
             if (mTargetLabelSize > oldTargetLabelSize) {
                 throw new IllegalStateException("instruction grew");
             }
-            return size() < oldSize;
+            return mTargetLabelSize < oldTargetLabelSize;
         }
 
         /**
          * Assemble value for instruction size field.
          */
-        private byte generateImmSizeField() {
-            byte immSize = getMaxImmSize();
+        private int generateImmSizeField() {
+            // If we already know the size the length field, just use it
+            switch (mLenFieldOverride) {
+                case -1:
+                    break;
+                case 1:
+                    return 1;
+                case 2:
+                    return 2;
+                case 4:
+                    return 3;
+                default:
+                    throw new IllegalStateException(
+                            "mLenFieldOverride has invalid value: " + mLenFieldOverride);
+            }
+            // Otherwise, calculate
+            int immSize = calculateRequiredIndeterminateSize();
             // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4.
             return immSize == 4 ? 3 : immSize;
         }
@@ -270,7 +463,7 @@
          * Assemble first byte of generated instruction.
          */
         private byte generateInstructionByte() {
-            byte sizeField = generateImmSizeField();
+            int sizeField = generateImmSizeField();
             return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister);
         }
 
@@ -283,7 +476,7 @@
          * be sign extended and the truncation should simply throw away their signed
          * upper bits.
          */
-        private int writeValue(int value, byte[] bytecode, int writingOffset, byte immSize) {
+        private static int writeValue(int value, byte[] bytecode, int writingOffset, int immSize) {
             for (int i = immSize - 1; i >= 0; i--) {
                 bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255);
             }
@@ -291,7 +484,7 @@
         }
 
         /**
-         * Generate bytecode for this instruction at offset {@link offset}.
+         * Generate bytecode for this instruction at offset {@link Instruction#offset}.
          */
         void generate(byte[] bytecode) throws IllegalInstructionException {
             if (mOpcode == Opcodes.LABEL.value) {
@@ -299,33 +492,17 @@
             }
             int writingOffset = offset;
             bytecode[writingOffset++] = generateInstructionByte();
-            byte maxImmSize = getMaxImmSize();
+            int indeterminateSize = calculateRequiredIndeterminateSize();
             if (mTargetLabel != null) {
                 writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset,
-                        maxImmSize);
+                        indeterminateSize);
             }
-            // For the copy opcode, the last imm is the length field is always 1 byte
-            if (isCopyOpCode()) {
-                if (mMaxSupportedImms != mImms.size()) {
-                    throw new IllegalStateException(
-                            "mImm size: " + mImms.size() + " doesn't match the mMaxSupportedImms: "
-                                    + mMaxSupportedImms);
-                }
-                int i;
-                for (i = 0; i < mImms.size() - 1; ++i) {
-                    writingOffset = writeValue(mImms.get(i).mValue, bytecode, writingOffset,
-                            maxImmSize);
-                }
-                writingOffset = writeValue(mImms.get(i).mValue, bytecode, writingOffset,
-                        mImms.get(i).mImmSize);
-            } else {
-                for (Immediate imm : mImms) {
-                    writingOffset = writeValue(imm.mValue, bytecode, writingOffset, maxImmSize);
-                }
+            for (IntImmediate imm : mIntImms) {
+                writingOffset = imm.writeValue(bytecode, writingOffset, indeterminateSize);
             }
-            if (mCompareBytes != null) {
-                System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length);
-                writingOffset += mCompareBytes.length;
+            if (mBytesImm != null) {
+                System.arraycopy(mBytesImm, 0, bytecode, writingOffset, mBytesImm.length);
+                writingOffset += mBytesImm.length;
             }
             if ((writingOffset - offset) != size()) {
                 throw new IllegalStateException("wrote " + (writingOffset - offset) +
@@ -333,32 +510,16 @@
             }
         }
 
-        private boolean isCopyOpCode() {
-            if (mOpcode == Opcodes.MEMCOPY.value) {
-                return true;
-            }
-            if (mOpcode == Opcodes.EXT.value) {
-                int realOpcode = mImms.get(0).mValue;
-                if (realOpcode == ExtendedOpcodes.EPKTCOPY.value
-                        || realOpcode == ExtendedOpcodes.EDATACOPY.value) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
         /**
-         * Calculate the size of either the immediate fields or the target label field, if either is
-         * present. Most instructions have either immediates or a target label field, but for the
-         * instructions that have both, the size of the target label field must be the same as the
-         * size of the immediate fields, because there is only one length field in the instruction
-         * byte, hence why this function simply takes the maximum of those sizes, so neither is
-         * truncated.
+         * Calculates the maximum indeterminate size of all IMMs in this instruction.
+         * <p>
+         * This method finds the largest size needed to encode any indeterminate-sized IMMs in
+         * the instruction. This size will be stored in the immLen field.
          */
-        private byte getMaxImmSize() {
-            byte maxSize = mTargetLabelSize;
-            for (int i = 0; i < mImms.size(); ++i) {
-                maxSize = (byte) Math.max(maxSize, mImms.get(i).mImmSize);
+        private int calculateRequiredIndeterminateSize() {
+            int maxSize = mTargetLabelSize;
+            for (IntImmediate imm : mIntImms) {
+                maxSize = Math.max(maxSize, imm.calculateIndeterminateSize());
             }
             return maxSize;
         }
@@ -436,7 +597,10 @@
     public static final int LAST_PREFILLED_MEMORY_SLOT = FILTER_AGE_MEMORY_SLOT;
 
     // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h
-    private static final int MIN_APF_VERSION = 2;
+    public static final int MIN_APF_VERSION = 2;
+    public static final int MIN_APF_VERSION_IN_DEV = 5;
+    public static final int APF_VERSION_4 = 4;
+
 
     private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
     private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>();
@@ -469,11 +633,12 @@
         }
     }
 
-    private void addInstruction(Instruction instruction) {
+    private ApfGenerator append(Instruction instruction) {
         if (mGenerated) {
             throw new IllegalStateException("Program already generated");
         }
         mInstructions.add(instruction);
+        return this;
     }
 
     /**
@@ -492,53 +657,38 @@
      * In this case "next_filter" may not have any generated code associated with it.
      */
     public ApfGenerator defineLabel(String name) throws IllegalInstructionException {
-        Instruction instruction = new Instruction(Opcodes.LABEL);
-        instruction.setLabel(name);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.LABEL).setLabel(name));
     }
 
     /**
      * Add an unconditional jump instruction to the end of the program.
      */
     public ApfGenerator addJump(String target) {
-        Instruction instruction = new Instruction(Opcodes.JMP);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.JMP).setTargetLabel(target));
     }
 
     /**
      * Add an instruction to the end of the program to load the byte at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad8(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDB, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad8(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDB, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 16-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad16(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDH, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad16(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDH, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 32-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad32(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDW, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad32(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDW, r).addUnsigned(ofs));
     }
 
     /**
@@ -546,11 +696,8 @@
      * {@code register}. The offset of the loaded byte from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad8Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDBX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad8Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDBX, r).addUnsigned(ofs));
     }
 
     /**
@@ -558,11 +705,8 @@
      * {@code register}. The offset of the loaded 16-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad16Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDHX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad16Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDHX, r).addUnsigned(ofs));
     }
 
     /**
@@ -570,109 +714,81 @@
      * {@code register}. The offset of the loaded 32-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad32Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDWX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad32Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDWX, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to add {@code value} to register R0.
      */
-    public ApfGenerator addAdd(int value) {
-        Instruction instruction = new Instruction(Opcodes.ADD);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAdd(int val) {
+        return append(new Instruction(Opcodes.ADD).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to multiply register R0 by {@code value}.
      */
-    public ApfGenerator addMul(int value) {
-        Instruction instruction = new Instruction(Opcodes.MUL);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addMul(int val) {
+        return append(new Instruction(Opcodes.MUL).addUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to divide register R0 by {@code value}.
      */
-    public ApfGenerator addDiv(int value) {
-        Instruction instruction = new Instruction(Opcodes.DIV);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addDiv(int val) {
+        return append(new Instruction(Opcodes.DIV).addUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to logically and register R0 with {@code value}.
      */
-    public ApfGenerator addAnd(int value) {
-        Instruction instruction = new Instruction(Opcodes.AND);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAnd(int val) {
+        return append(new Instruction(Opcodes.AND).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to logically or register R0 with {@code value}.
      */
-    public ApfGenerator addOr(int value) {
-        Instruction instruction = new Instruction(Opcodes.OR);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addOr(int val) {
+        return append(new Instruction(Opcodes.OR).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to shift left register R0 by {@code value} bits.
      */
-    public ApfGenerator addLeftShift(int value) {
-        Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.addSignedImm(value);
-        addInstruction(instruction);
-        return this;
+    // TODO: consider whether should change the argument type to byte
+    public ApfGenerator addLeftShift(int val) {
+        return append(new Instruction(Opcodes.SH).addSigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to shift right register R0 by {@code value}
      * bits.
      */
-    public ApfGenerator addRightShift(int value) {
-        Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.addSignedImm(-value);
-        addInstruction(instruction);
-        return this;
+    // TODO: consider whether should change the argument type to byte
+    public ApfGenerator addRightShift(int val) {
+        return append(new Instruction(Opcodes.SH).addSigned(-val));
     }
 
     /**
      * Add an instruction to the end of the program to add register R1 to register R0.
      */
     public ApfGenerator addAddR1() {
-        Instruction instruction = new Instruction(Opcodes.ADD, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.ADD, R1));
     }
 
     /**
      * Add an instruction to the end of the program to multiply register R0 by register R1.
      */
     public ApfGenerator addMulR1() {
-        Instruction instruction = new Instruction(Opcodes.MUL, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.MUL, R1));
     }
 
     /**
      * Add an instruction to the end of the program to divide register R0 by register R1.
      */
     public ApfGenerator addDivR1() {
-        Instruction instruction = new Instruction(Opcodes.DIV, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.DIV, R1));
     }
 
     /**
@@ -680,9 +796,7 @@
      * and store the result back into register R0.
      */
     public ApfGenerator addAndR1() {
-        Instruction instruction = new Instruction(Opcodes.AND, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.AND, R1));
     }
 
     /**
@@ -690,9 +804,7 @@
      * and store the result back into register R0.
      */
     public ApfGenerator addOrR1() {
-        Instruction instruction = new Instruction(Opcodes.OR, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.OR, R1));
     }
 
     /**
@@ -700,111 +812,77 @@
      * register R1.
      */
     public ApfGenerator addLeftShiftR1() {
-        Instruction instruction = new Instruction(Opcodes.SH, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.SH, R1));
     }
 
     /**
      * Add an instruction to the end of the program to move {@code value} into {@code register}.
      */
     public ApfGenerator addLoadImmediate(Register register, int value) {
-        Instruction instruction = new Instruction(Opcodes.LI, register);
-        instruction.addSignedImm(value);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.LI, register).addSigned(value));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals {@code value}.
      */
-    public ApfGenerator addJumpIfR0Equals(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JEQ);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0Equals(int val, String tgt) {
+        return append(new Instruction(Opcodes.JEQ).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal {@code value}.
      */
-    public ApfGenerator addJumpIfR0NotEquals(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JNE);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0NotEquals(int val, String tgt) {
+        return append(new Instruction(Opcodes.JNE).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than {@code value}.
      */
-    public ApfGenerator addJumpIfR0GreaterThan(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JGT);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0GreaterThan(int val, String tgt) {
+        return append(new Instruction(Opcodes.JGT).addUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is less than {@code value}.
      */
-    public ApfGenerator addJumpIfR0LessThan(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JLT);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0LessThan(int val, String tgt) {
+        return append(new Instruction(Opcodes.JLT).addUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in {@code value}.
      */
-    public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JSET);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0AnyBitsSet(int val, String tgt) {
+        return append(new Instruction(Opcodes.JSET).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals register R1's value.
      */
-    public ApfGenerator addJumpIfR0EqualsR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JEQ, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0EqualsR1(String tgt) {
+        return append(new Instruction(Opcodes.JEQ, R1).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal register R1's value.
      */
-    public ApfGenerator addJumpIfR0NotEqualsR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JNE, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0NotEqualsR1(String tgt) {
+        return append(new Instruction(Opcodes.JNE, R1).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than register R1's value.
      */
-    public ApfGenerator addJumpIfR0GreaterThanR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JGT, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0GreaterThanR1(String tgt) {
+        return append(new Instruction(Opcodes.JGT, R1).setTargetLabel(tgt));
     }
 
     /**
@@ -812,327 +890,329 @@
      * value is less than register R1's value.
      */
     public ApfGenerator addJumpIfR0LessThanR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JLT, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.JLT, R1).setTargetLabel(target));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in R1's value.
      */
-    public ApfGenerator addJumpIfR0AnyBitsSetR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JSET, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0AnyBitsSetR1(String tgt) {
+        return append(new Instruction(Opcodes.JSET, R1).setTargetLabel(tgt));
     }
 
     /**
      * 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} don't match {@code bytes}, {@code register}
-     * must be R0.
+     * packet at an offset specified by {@code register} don't match {@code bytes}
      */
-    public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target)
-            throws IllegalInstructionException {
-        if (register == Register.R1) {
-            throw new IllegalInstructionException("JNEBS fails with R1");
-        }
-        Instruction instruction = new Instruction(Opcodes.JNEBS, register);
-        instruction.addUnsignedImm(bytes.length);
-        instruction.setTargetLabel(target);
-        instruction.setCompareBytes(bytes);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfBytesAtR0NotEqual(byte[] bytes, String tgt) {
+        return append(new Instruction(Opcodes.JNEBS).addUnsigned(
+                bytes.length).setTargetLabel(tgt).setBytesImm(bytes));
     }
 
     /**
      * Add an instruction to the end of the program to load memory slot {@code slot} into
      * {@code register}.
      */
-    public ApfGenerator addLoadFromMemory(Register register, int slot)
+    public ApfGenerator addLoadFromMemory(Register r, int slot)
             throws IllegalInstructionException {
-        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
-            throw new IllegalInstructionException("illegal memory slot number: " + slot);
-        }
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.LDM.value + slot);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.LDM, slot, r));
     }
 
     /**
      * Add an instruction to the end of the program to store {@code register} into memory slot
      * {@code slot}.
      */
-    public ApfGenerator addStoreToMemory(Register register, int slot)
+    public ApfGenerator addStoreToMemory(Register r, int slot)
             throws IllegalInstructionException {
-        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
-            throw new IllegalInstructionException("illegal memory slot number: " + slot);
-        }
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.STM.value + slot);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.STM, slot, r));
     }
 
     /**
      * Add an instruction to the end of the program to logically not {@code register}.
      */
-    public ApfGenerator addNot(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.NOT.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addNot(Register r) {
+        return append(new Instruction(ExtendedOpcodes.NOT, r));
     }
 
     /**
      * Add an instruction to the end of the program to negate {@code register}.
      */
-    public ApfGenerator addNeg(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.NEG.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addNeg(Register r) {
+        return append(new Instruction(ExtendedOpcodes.NEG, r));
     }
 
     /**
      * Add an instruction to swap the values in register R0 and register R1.
      */
     public ApfGenerator addSwap() {
-        Instruction instruction = new Instruction(Opcodes.EXT);
-        instruction.addUnsignedImm(ExtendedOpcodes.SWAP.value);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.SWAP));
     }
 
     /**
      * Add an instruction to the end of the program to move the value into
      * {@code register} from the other register.
      */
-    public ApfGenerator addMove(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.MOVE.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addMove(Register r) {
+        return append(new Instruction(ExtendedOpcodes.MOVE, r));
+    }
+
+    /**
+     * Add an instruction to the end of the program to let the program immediately return PASS.
+     */
+    public ApfGenerator addPass() {
+        // PASS requires using R0 because it shares opcode with DROP
+        return append(new Instruction(Opcodes.PASS));
+    }
+
+    /**
+     * Add an instruction to the end of the program to increment the counter value and
+     * immediately return PASS.
+     */
+    public ApfGenerator addCountAndPass(int cnt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // PASS requires using R0 because it shares opcode with DROP
+        return append(new Instruction(Opcodes.PASS).addUnsigned(cnt));
+    }
+
+    /**
+     * Add an instruction to the end of the program to let the program immediately return DROP.
+     */
+    public ApfGenerator addDrop() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.DROP, R1));
+    }
+
+    /**
+     * Add an instruction to the end of the program to increment the counter value and
+     * immediately return DROP.
+     */
+    public ApfGenerator addCountAndDrop(int cnt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.DROP, R1).addUnsigned(cnt));
+    }
+
+    /**
+     * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
+     * Buffer length to be allocated is stored in register 0.
+     */
+    public ApfGenerator addAllocateR0() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE));
     }
 
     /**
      * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
      *
-     * @param register the register value contains the buffer size.
+     * @param size the buffer length to be allocated.
      */
-    public ApfGenerator addAlloc(Register register) throws IllegalInstructionException {
-        requireApfVersion(5);
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.ALLOC.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAllocate(int size) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        // R1 means the extra be16 immediate is present
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE, R1).addU16(size));
     }
 
     /**
-     * Add an instruction to the end of the program to call the apf_transmit_buffer() function.
-     *
-     * @param register the register value contains the packet type.
+     * Add an instruction to the beginning of the program to reserve the data region.
+     * @param data the actual data byte
      */
-    public ApfGenerator addTrans(Register register) throws IllegalInstructionException {
-        requireApfVersion(5);
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.TRANS.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addData(byte[] data) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        if (!mInstructions.isEmpty()) {
+            throw new IllegalInstructionException("data instruction has to come first");
+        }
+        return append(new Instruction(Opcodes.JMP, R1).addUnsigned(data.length).setBytesImm(data));
     }
 
     /**
-     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value to output buffer.
-     *
-     * @param value the value to write
-     * @param size the size of the value
-     * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
+     * Add an instruction to the end of the program to transmit the allocated buffer.
      */
-    public ApfGenerator addWrite(int value, byte size) throws IllegalInstructionException {
-        requireApfVersion(5);
-        if (!(size == 1 || size == 2 || size == 4)) {
-            throw new IllegalInstructionException("length field must be 1, 2 or 4");
-        }
-        if (size < calculateImmSize(value, false)) {
-            throw new IllegalInstructionException(
-                    String.format("the value %d is unfit into size: %d", value, size));
-        }
-        Instruction instruction = new Instruction(Opcodes.WRITE);
-        instruction.addUnsignedImm(value, size);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addTransmit() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        // TRANSMIT requires using R0 because it shares opcode with DISCARD
+        return append(new Instruction(ExtendedOpcodes.TRANSMIT));
     }
 
     /**
-     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value from register
-     * to output buffer.
-     *
-     * @param register the register contains the value to be written
-     * @param size the size of the value
-     * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
+     * Add an instruction to the end of the program to discard the allocated buffer.
      */
-    public ApfGenerator addWrite(Register register, byte size)
-            throws IllegalInstructionException {
-        requireApfVersion(5);
-        if (!(size == 1 || size == 2 || size == 4)) {
-            throw new IllegalInstructionException(
-                    "length field must be 1, 2 or 4");
-        }
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        if (size == 1) {
-            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE1.value);
-        } else if (size == 2) {
-            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE2.value);
-        } else {
-            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE4.value);
-        }
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addDiscard() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        // DISCARD requires using R1 because it shares opcode with TRANSMIT
+        return append(new Instruction(ExtendedOpcodes.DISCARD, R1));
     }
 
     /**
-     * Add an instruction to the end of the program to copy data from APF data region to output
+     * Add an instruction to the end of the program to write 1 byte value to output buffer.
+     */
+    public ApfGenerator addWriteU8(int val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(1).addU8(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 2 bytes value to output buffer.
+     */
+    public ApfGenerator addWriteU16(int val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(2).addU16(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 4 bytes value to output buffer.
+     */
+    public ApfGenerator addWriteU32(long val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(4).addU32(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 1 byte value from register to output
      * buffer.
-     *
-     * @param srcOffset the offset inside the APF data region for where to start copy
-     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-     *               one time.
-     * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when imm size is incorrectly set.
      */
-    public ApfGenerator addDataCopy(int srcOffset, int length)
-            throws IllegalInstructionException {
-        return addMemCopy(srcOffset, length, Register.R1);
+    public ApfGenerator addWriteU8(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE1, reg));
     }
 
     /**
-     * Add an instruction to the end of the program to copy data from input packet to output buffer.
-     *
-     * @param srcOffset the offset inside the input packet for where to start copy
-     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-     *               one time.
-     * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when imm size is incorrectly set.
-     */
-    public ApfGenerator addPacketCopy(int srcOffset, int length)
-            throws IllegalInstructionException {
-        return addMemCopy(srcOffset, length, Register.R0);
-    }
-
-    private ApfGenerator addMemCopy(int srcOffset, int length, Register register)
-            throws IllegalInstructionException {
-        requireApfVersion(5);
-        checkCopyLength(length);
-        checkCopyOffset(srcOffset);
-        Instruction instruction = new Instruction(Opcodes.MEMCOPY,
-                register, 2 /* maxSupportedImms */);
-        // if the offset == 0, it should still be encoded with 1 byte size.
-        if (srcOffset == 0) {
-            instruction.addUnsignedImm(srcOffset, (byte) 1 /* size */);
-        } else {
-            instruction.addUnsignedImm(srcOffset);
-        }
-        instruction.addUnsignedImm(length, (byte) 1 /* size */);
-        addInstruction(instruction);
-        return this;
-    }
-
-    /**
-     * Add an instruction to the end of the program to copy data from APF data region to output
+     * Add an instruction to the end of the program to write 2 byte value from register to output
      * buffer.
-     *
-     * @param register the register that stored the base offset value.
-     * @param relativeOffset the offset inside the APF data region for where to start copy
-     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-     *               one time.
-     * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when imm size is incorrectly set.
      */
-    public ApfGenerator addDataCopy(Register register, int relativeOffset, int length)
-            throws IllegalInstructionException {
-        return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EDATACOPY.value);
+    public ApfGenerator addWriteU16(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE2, reg));
     }
 
     /**
-     * Add an instruction to the end of the program to copy data from input packet to output buffer.
+     * Add an instruction to the end of the program to write 4 byte value from register to output
+     * buffer.
+     */
+    public ApfGenerator addWriteU32(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE4, reg));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
      *
-     * @param register the register that stored the base offset value.
-     * @param relativeOffset the offset inside the input packet for where to start copy
-     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
+     * @param src the offset inside the APF program/data region for where to start copy.
+     * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at
      *               one time.
      * @return the ApfGenerator object
-     * @throws IllegalInstructionException throws when imm size is incorrectly set.
      */
-    public ApfGenerator addPacketCopy(Register register, int relativeOffset, int length)
+    public ApfGenerator addDataCopy(int src, int len)
             throws IllegalInstructionException {
-        return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EPKTCOPY.value);
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.PKTDATACOPY, R1).addUnsigned(src).addU8(len));
     }
 
-    private ApfGenerator addMemcopy(Register register, int relativeOffset, int length, int opcode)
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     *
+     * @param src the offset inside the input packet for where to start copy.
+     * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at
+     *               one time.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopy(int src, int len)
             throws IllegalInstructionException {
-        requireApfVersion(5);
-        checkCopyLength(length);
-        checkCopyOffset(relativeOffset);
-        Instruction instruction = new Instruction(Opcodes.EXT, register, 3 /* maxSupportedImms */);
-        instruction.addUnsignedImm(opcode);
-        // if the offset == 0, it should still be encoded with 1 byte size.
-        if (relativeOffset == 0) {
-            instruction.addUnsignedImm(relativeOffset, (byte) 1 /* size */);
-        } else {
-            instruction.addUnsignedImm(relativeOffset);
-        }
-        instruction.addUnsignedImm(length, (byte) 1 /* size */);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.PKTDATACOPY, R0).addUnsigned(src).addU8(len));
     }
 
-    private void checkCopyLength(int length) {
-        if (length < 0 || length > 255) {
-            throw new IllegalArgumentException(
-                    "copy length must between 0 to 255, length: " + length);
-        }
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     *
+     * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addDataCopyFromR0(int len) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EDATACOPY).addU8(len));
     }
 
-    private void checkCopyOffset(int offset) {
-        if (offset < 0) {
-            throw new IllegalArgumentException(
-                    "offset must be non less than zero, offset: " + offset);
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     *
+     * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopyFromR0(int len) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EPKTCOPY).addU8(len));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     * Copy length is stored in R1.
+     *
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addDataCopyFromR0LenR1() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EDATACOPY, R1));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     * Copy length is stored in R1.
+     *
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopyFromR0LenR1() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EPKTCOPY, R1));
+    }
+
+    private static void checkRange(@NonNull String variableName, long value, long lowerBound,
+            long upperBound) {
+        if (value >= lowerBound && value <= upperBound) {
+            return;
         }
+        throw new IllegalArgumentException(
+                String.format("%s: %d, must be in range [%d, %d]", variableName, value, lowerBound,
+                        upperBound));
     }
 
     /**
      * Add an instruction to the end of the program to load 32 bits from the data memory into
      * {@code register}. The source address is computed by adding the signed immediate
      * @{code offset} to the other register.
-     * Requires APF v3 or greater.
+     * Requires APF v4 or greater.
      */
-    public ApfGenerator addLoadData(Register destinationRegister, int offset)
+    public ApfGenerator addLoadData(Register dst, int ofs)
             throws IllegalInstructionException {
-        requireApfVersion(3);
-        Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister);
-        instruction.addSignedImm(offset);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(APF_VERSION_4);
+        return append(new Instruction(Opcodes.LDDW, dst).addSigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to store 32 bits from {@code register} into the
      * data memory. The destination address is computed by adding the signed immediate
      * @{code offset} to the other register.
-     * Requires APF v3 or greater.
+     * Requires APF v4 or greater.
      */
-    public ApfGenerator addStoreData(Register sourceRegister, int offset)
+    public ApfGenerator addStoreData(Register src, int ofs)
             throws IllegalInstructionException {
-        requireApfVersion(3);
-        Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister);
-        instruction.addSignedImm(offset);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(APF_VERSION_4);
+        return append(new Instruction(Opcodes.STDW, src).addSigned(ofs));
     }
 
     /**
@@ -1151,7 +1231,7 @@
     /**
      * Calculate the size of the imm.
      */
-    private static byte calculateImmSize(int imm, boolean signed) {
+    private static int calculateImmSize(int imm, boolean signed) {
         if (imm == 0) {
             return 0;
         }
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java
index 0300d34..5bd2515 100644
--- a/src/android/net/apf/DnsUtils.java
+++ b/src/android/net/apf/DnsUtils.java
@@ -301,7 +301,7 @@
         gen.addJumpIfR0NotEquals(label.length(), noMatchLabel);
         gen.addLoadFromMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
         gen.addAdd(1);
-        gen.addJumpIfBytesNotEqual(R0, label.getBytes(), noMatchLabel);
+        gen.addJumpIfBytesAtR0NotEqual(label.getBytes(), noMatchLabel);
 
         // Prep offset of next label.
         gen.addAdd(label.length());
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
index 63e2bbc..6b93d89 100644
--- a/src/android/net/apf/LegacyApfFilter.java
+++ b/src/android/net/apf/LegacyApfFilter.java
@@ -42,6 +42,7 @@
 import android.net.LinkProperties;
 import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
@@ -50,7 +51,7 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.RaEvent;
 import android.os.PowerManager;
-import android.os.SystemClock;
+import android.stats.connectivity.NetworkQuirkEvent;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.format.DateUtils;
@@ -67,6 +68,9 @@
 import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SocketUtils;
+import com.android.networkstack.metrics.ApfSessionInfoMetrics;
+import com.android.networkstack.metrics.IpClientRaInfoMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
 import com.android.networkstack.util.NetworkStackUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -85,6 +89,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * For networks that support packet filtering via APF programs, {@code ApfFilter}
@@ -117,62 +122,6 @@
     }
 
     /**
-     * APF packet counters.
-     *
-     * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
-     * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
-     * the last writable 32bit word.
-     */
-    @VisibleForTesting
-    public static enum Counter {
-        RESERVED_OOB,  // Points to offset 0 from the end of the buffer (out-of-bounds)
-        TOTAL_PACKETS,
-        PASSED_ARP,
-        PASSED_DHCP,
-        PASSED_IPV4,
-        PASSED_IPV6_NON_ICMP,
-        PASSED_IPV4_UNICAST,
-        PASSED_IPV6_ICMP,
-        PASSED_IPV6_UNICAST_NON_ICMP,
-        PASSED_ARP_NON_IPV4,
-        PASSED_ARP_UNKNOWN,
-        PASSED_ARP_UNICAST_REPLY,
-        PASSED_NON_IP_UNICAST,
-        PASSED_MDNS,
-        DROPPED_ETH_BROADCAST,
-        DROPPED_RA,
-        DROPPED_GARP_REPLY,
-        DROPPED_ARP_OTHER_HOST,
-        DROPPED_IPV4_L2_BROADCAST,
-        DROPPED_IPV4_BROADCAST_ADDR,
-        DROPPED_IPV4_BROADCAST_NET,
-        DROPPED_IPV4_MULTICAST,
-        DROPPED_IPV6_ROUTER_SOLICITATION,
-        DROPPED_IPV6_MULTICAST_NA,
-        DROPPED_IPV6_MULTICAST,
-        DROPPED_IPV6_MULTICAST_PING,
-        DROPPED_IPV6_NON_ICMP_MULTICAST,
-        DROPPED_802_3_FRAME,
-        DROPPED_ETHERTYPE_BLACKLISTED,
-        DROPPED_ARP_REPLY_SPA_NO_HOST,
-        DROPPED_IPV4_KEEPALIVE_ACK,
-        DROPPED_IPV6_KEEPALIVE_ACK,
-        DROPPED_IPV4_NATT_KEEPALIVE,
-        DROPPED_MDNS;
-
-        // Returns the negative byte offset from the end of the APF data segment for
-        // a given counter.
-        public int offset() {
-            return - this.ordinal() * 4;  // Currently, all counters are 32bit long.
-        }
-
-        // Returns the total size of the data segment in bytes.
-        public static int totalSize() {
-            return (Counter.class.getEnumConstants().length - 1) * 4;
-        }
-    }
-
-    /**
      * When APFv4 is supported, loads R1 with the offset of the specified counter.
      */
     private void maybeSetupCounter(ApfGenerator gen, Counter c) {
@@ -191,7 +140,7 @@
     public class ReceiveThread extends Thread {
         private final byte[] mPacket = new byte[1514];
         private final FileDescriptor mSocket;
-        private final long mStart = SystemClock.elapsedRealtime();
+        private final long mStart = mClock.elapsedRealtime();
 
         private int mReceivedRas = 0;
         private int mMatchingRas = 0;
@@ -254,7 +203,7 @@
         }
 
         private void logStats() {
-            final long nowMs = SystemClock.elapsedRealtime();
+            final long nowMs = mClock.elapsedRealtime();
             synchronized (this) {
                 final ApfStats stats = new ApfStats.Builder()
                         .setReceivedRas(mReceivedRas)
@@ -356,7 +305,6 @@
             ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
 
 
-
     private final ApfCapabilities mApfCapabilities;
     private final IpClientCallbacksWrapper mIpClientCallback;
     private final InterfaceParams mInterfaceParams;
@@ -375,9 +323,33 @@
     private final boolean mDrop802_3Frames;
     private final int[] mEthTypeBlackList;
 
+    private final ApfCounterTracker mApfCounterTracker = new ApfCounterTracker();
+    @GuardedBy("this")
+    private long mSessionStartMs = 0;
+    @GuardedBy("this")
+    private int mNumParseErrorRas = 0;
+    @GuardedBy("this")
+    private int mNumZeroLifetimeRas = 0;
+    @GuardedBy("this")
+    private int mLowestRouterLifetimeSeconds = Integer.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestPioValidLifetimeSeconds = Long.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestRioRouteLifetimeSeconds = Long.MAX_VALUE;
+    @GuardedBy("this")
+    private long mLowestRdnssLifetimeSeconds = Long.MAX_VALUE;
+
     // Ignore non-zero RDNSS lifetimes below this value.
     private final int mMinRdnssLifetimeSec;
 
+    // Minimum session time for metrics, duration less than this time will not be logged.
+    private final long mMinMetricsSessionDurationMs;
+
+    private final ApfFilter.Clock mClock;
+    private final NetworkQuirkMetrics mNetworkQuirkMetrics;
+    private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
+    private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
+
     // Detects doze mode state transitions.
     private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
         @Override
@@ -400,10 +372,21 @@
     @GuardedBy("this")
     private int mIPv4PrefixLength;
 
+    private final ApfFilter.Dependencies mDependencies;
+
     @VisibleForTesting
     public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
             InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
-            IpConnectivityLog log) {
+            IpConnectivityLog log, NetworkQuirkMetrics networkQuirkMetrics) {
+        this(context, config, ifParams, ipClientCallback, log, networkQuirkMetrics,
+                new ApfFilter.Dependencies(context), new ApfFilter.Clock());
+    }
+
+    @VisibleForTesting
+    public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            IpConnectivityLog log, NetworkQuirkMetrics networkQuirkMetrics,
+            ApfFilter.Dependencies dependencies, ApfFilter.Clock clock) {
         mApfCapabilities = config.apfCapabilities;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
@@ -411,6 +394,13 @@
         mDrop802_3Frames = config.ieee802_3Filter;
         mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
         mContext = context;
+        mClock = clock;
+        mDependencies = dependencies;
+        mNetworkQuirkMetrics = networkQuirkMetrics;
+        mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
+        mApfSessionInfoMetrics = dependencies.getApfSessionInfoMetrics();
+        mSessionStartMs = mClock.elapsedRealtime();
+        mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
 
         if (mApfCapabilities.hasDataAccess()) {
             mCountAndPassLabel = "countAndPass";
@@ -437,6 +427,7 @@
 
     public synchronized void setDataSnapshot(byte[] data) {
         mDataSnapshot = data;
+        mApfCounterTracker.updateCountersFromData(data);
     }
 
     private void log(String s) {
@@ -492,7 +483,9 @@
                 // a crash on some older devices (b/78905546).
                 if (mApfCapabilities.hasDataAccess()) {
                     byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
-                    mIpClientCallback.installPacketFilter(zeroes);
+                    if (!mIpClientCallback.installPacketFilter(zeroes)) {
+                        sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+                    }
                 }
 
                 // Install basic filters
@@ -513,7 +506,7 @@
     // Returns seconds since device boot.
     @VisibleForTesting
     protected long currentTimeSeconds() {
-        return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
+        return mClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
     }
 
     public static class InvalidRaException extends Exception {
@@ -598,6 +591,14 @@
         // List of sections in the packet.
         private final ArrayList<PacketSection> mPacketSections = new ArrayList<>();
 
+        // Router lifetime in packet
+        private final int mRouterLifetime;
+        // Minimum valid lifetime of PIOs in packet, Long.MAX_VALUE means not seen.
+        private long mMinPioValidLifetime = Long.MAX_VALUE;
+        // Minimum route lifetime of RIOs in packet, Long.MAX_VALUE means not seen.
+        private long mMinRioRouteLifetime = Long.MAX_VALUE;
+        // Minimum lifetime of RDNSSs in packet, Long.MAX_VALUE means not seen.
+        private long mMinRdnssLifetime = Long.MAX_VALUE;
         // Minimum lifetime in packet
         long mMinLifetime;
         // When the packet was last captured, in seconds since Unix Epoch
@@ -770,6 +771,34 @@
             return lifetime;
         }
 
+        /**
+         * Return the router lifetime of the RA
+         */
+        public int routerLifetime() {
+            return mRouterLifetime;
+        }
+
+        /**
+         * Return the minimum valid lifetime in PIOs
+         */
+        public long minPioValidLifetime() {
+            return mMinPioValidLifetime;
+        }
+
+        /**
+         * Return the minimum route lifetime in RIOs
+         */
+        public long minRioRouteLifetime() {
+            return mMinRioRouteLifetime;
+        }
+
+        /**
+         * Return the minimum lifetime in RDNSSs
+         */
+        public long minRdnssLifetime() {
+            return mMinRdnssLifetime;
+        }
+
         // http://b/66928272 http://b/65056012
         // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these
         // lifetimes for the purpose of filter lifetime calculations.
@@ -817,9 +846,9 @@
 
             // Parse router lifetime
             addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, routerLifetime);
-            builder.updateRouterLifetime(routerLifetime);
+            mRouterLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
+            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, mRouterLifetime);
+            builder.updateRouterLifetime(mRouterLifetime);
 
             // Add remaining fields (reachable time and retransmission timer) to match section.
             addMatchUntil(ICMP6_RA_OPTION_OFFSET);
@@ -839,6 +868,8 @@
                         addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
                                 ICMP6_PREFIX_OPTION_TYPE, lifetime);
                         builder.updatePrefixValidLifetime(lifetime);
+                        mMinPioValidLifetime = getMinForPositiveValue(
+                                mMinPioValidLifetime, lifetime);
 
                         // Parse preferred lifetime
                         lifetime = getUint32(mPacket, mPacket.position());
@@ -855,11 +886,14 @@
                         mRdnssOptionOffsets.add(position);
                         lifetime = add4ByteLifetimeOption(optionType, optionLength);
                         builder.updateRdnssLifetime(lifetime);
+                        mMinRdnssLifetime = getMinForPositiveValue(mMinRdnssLifetime, lifetime);
                         break;
                     case ICMP6_ROUTE_INFO_OPTION_TYPE:
                         mRioOptionOffsets.add(position);
                         lifetime = add4ByteLifetimeOption(optionType, optionLength);
                         builder.updateRouteInfoLifetime(lifetime);
+                        mMinRioRouteLifetime = getMinForPositiveValue(
+                                mMinRioRouteLifetime, lifetime);
                         break;
                     case ICMP6_DNSSL_OPTION_TYPE:
                         lifetime = add4ByteLifetimeOption(optionType, optionLength);
@@ -948,7 +982,7 @@
                 // Generate code to match the packet bytes.
                 if (section.type == PacketSection.Type.MATCH) {
                     gen.addLoadImmediate(Register.R0, section.start);
-                    gen.addJumpIfBytesNotEqual(Register.R0,
+                    gen.addJumpIfBytesAtR0NotEqual(
                             Arrays.copyOfRange(mPacket.array(), section.start,
                                     section.start + section.length),
                             nextFilterLabel);
@@ -1031,7 +1065,7 @@
             final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
             // Check payload length is 1
@@ -1046,11 +1080,11 @@
             // Check that the ports match
             gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addAdd(ETH_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel);
 
             // Payload offset = R0 + UDP header length
             gen.addAdd(UDP_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
             gen.addJump(mCountAndDropLabel);
@@ -1146,7 +1180,7 @@
             final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // Skip to the next filter if it's not zero-sized :
             // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
@@ -1168,7 +1202,7 @@
             gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN);
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
             gen.addJump(mCountAndDropLabel);
@@ -1241,6 +1275,12 @@
     // How many times the program was updated since we started.
     @GuardedBy("this")
     private int mNumProgramUpdates = 0;
+    // The maximum program size that updated since we started.
+    @GuardedBy("this")
+    private int mMaxProgramSize = 0;
+    // The maximum number of distinct RAs
+    @GuardedBy("this")
+    private int mMaxDistinctRas = 0;
     // How many times the program was updated since we started for allowing multicast traffic.
     @GuardedBy("this")
     private int mNumProgramUpdatesAllowingMulticast = 0;
@@ -1276,7 +1316,7 @@
         // Pass if not ARP IPv4.
         gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
-        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndPassLabel);
 
         // Pass if unknown ARP opcode.
         gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
@@ -1292,7 +1332,7 @@
         // Pass if unicast reply.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
 
         // Either a unicast request, a unicast reply, or a broadcast reply.
         gen.defineLabel(checkTargetIPv4);
@@ -1306,7 +1346,7 @@
             // and broadcast replies with a different target IPv4 address.
             gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
             maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
-            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel);
         }
 
         maybeSetupCounter(gen, Counter.PASSED_ARP);
@@ -1354,7 +1394,7 @@
             gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
             // NOTE: Relies on R1 containing IPv4 header offset.
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
             maybeSetupCounter(gen, Counter.PASSED_DHCP);
             gen.addJump(mCountAndPassLabel);
 
@@ -1388,7 +1428,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, ETHER_BROADCAST, mCountAndPassLabel);
+            gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
             gen.addJump(mCountAndDropLabel);
         } else {
@@ -1515,8 +1555,7 @@
         // 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, unsolicitedNaDropPrefix,
-                skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
         maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
         gen.addJump(mCountAndDropLabel);
@@ -1573,7 +1612,7 @@
 
         // Check it's L2 mDNS multicast address.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
                 skipMdnsv4Filter);
 
         // Checks it's IPv4.
@@ -1591,8 +1630,7 @@
 
         // Checks it's L2 mDNS multicast address.
         // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
-                skipMdnsFilter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
 
         // Checks it's IPv6.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
@@ -1623,7 +1661,7 @@
         for (int i = 0; i < mMdnsAllowList.size(); ++i) {
             final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
             final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
-            gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck);
+            gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck);
             // QNAME matched
             gen.addJump(mDnsAcceptPacket);
             // QNAME not matched
@@ -1669,7 +1707,7 @@
      * </ul>
      */
     @GuardedBy("this")
-    private ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+    protected ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
         // This is guaranteed to succeed because of the check in maybeCreate.
         ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported);
 
@@ -1706,7 +1744,7 @@
         }
 
         // Handle ether-type black list
-        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED);
+        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_DENYLISTED);
         for (int p : mEthTypeBlackList) {
             gen.addJumpIfR0Equals(p, mCountAndDropLabel);
         }
@@ -1737,7 +1775,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, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
         maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
         gen.addJump(mCountAndDropLabel);
 
@@ -1808,6 +1846,7 @@
             // Can't fit the program even without any RA filters?
             if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
                 Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                 return;
             }
 
@@ -1817,6 +1856,7 @@
                 // Stop if we get too big.
                 if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
                     if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
+                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                     break;
                 }
 
@@ -1832,13 +1872,17 @@
             program = gen.generate();
         } catch (IllegalInstructionException|IllegalStateException e) {
             Log.e(TAG, "Failed to generate APF program.", e);
+            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
             return;
         }
-        mIpClientCallback.installPacketFilter(program);
+        if (!mIpClientCallback.installPacketFilter(program)) {
+            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        }
         mLastTimeInstalledProgram = mProgramBaseTime;
         mLastInstalledProgramMinLifetime = programMinLifetime;
         mLastInstalledProgram = program;
         mNumProgramUpdates++;
+        mMaxProgramSize = Math.max(mMaxProgramSize, program.length);
 
         if (VDBG) {
             hexDump("Installing filter: ", program, program.length);
@@ -1891,6 +1935,20 @@
         }
     }
 
+    // Get the minimum value excludes zero. This is used for calculating the lowest lifetime values
+    // in RA packets. Zero lifetimes are excluded because we want to detect whether there is any
+    // unusually small lifetimes but zero lifetime is actually valid (cease to be a default router
+    // or the option is no longer be used). Number of zero lifetime RAs is collected in a different
+    // Metrics.
+    private long getMinForPositiveValue(long oldMinValue, long value) {
+        if (value < 1) return oldMinValue;
+        return Math.min(oldMinValue, value);
+    }
+
+    private int getMinForPositiveValue(int oldMinValue, int value) {
+        return (int) getMinForPositiveValue((long) oldMinValue, (long) value);
+    }
+
     /**
      * Process an RA packet, updating the list of known RAs and installing a new APF program
      * if the current APF program should be updated.
@@ -1927,6 +1985,9 @@
             }
         }
         purgeExpiredRasLocked();
+
+        mMaxDistinctRas = Math.max(mMaxDistinctRas, mRas.size() + 1);
+
         // TODO: figure out how to proceed when we've received more than MAX_RAS RAs.
         if (mRas.size() >= MAX_RAS) {
             return ProcessRaResult.DROPPED;
@@ -1936,10 +1997,23 @@
             ra = new Ra(packet, length);
         } catch (Exception e) {
             Log.e(TAG, "Error parsing RA", e);
+            mNumParseErrorRas++;
             return ProcessRaResult.PARSE_ERROR;
         }
+
+        // Update info for Metrics
+        mLowestRouterLifetimeSeconds = getMinForPositiveValue(
+                mLowestRouterLifetimeSeconds, ra.routerLifetime());
+        mLowestPioValidLifetimeSeconds = getMinForPositiveValue(
+                mLowestPioValidLifetimeSeconds, ra.minPioValidLifetime());
+        mLowestRioRouteLifetimeSeconds = getMinForPositiveValue(
+                mLowestRioRouteLifetimeSeconds, ra.minRioRouteLifetime());
+        mLowestRdnssLifetimeSeconds = getMinForPositiveValue(
+                mLowestRdnssLifetimeSeconds, ra.minRdnssLifetime());
+
         // Ignore 0 lifetime RAs.
         if (ra.isExpired()) {
+            mNumZeroLifetimeRas++;
             return ProcessRaResult.ZERO_LIFETIME;
         }
         log("Adding " + ra);
@@ -1953,7 +2027,8 @@
      * filtering using APF programs.
      */
     public static LegacyApfFilter maybeCreate(Context context, ApfFilter.ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) {
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
         ApfCapabilities apfCapabilities =  config.apfCapabilities;
         if (apfCapabilities == null) return null;
@@ -1972,10 +2047,42 @@
             return null;
         }
 
-        return new LegacyApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
+        return new LegacyApfFilter(context, config, ifParams, ipClientCallback,
+                new IpConnectivityLog(), networkQuirkMetrics);
+    }
+
+    private synchronized void collectAndSendMetrics() {
+        if (mIpClientRaInfoMetrics == null || mApfSessionInfoMetrics == null) return;
+        final long sessionDurationMs = mClock.elapsedRealtime() - mSessionStartMs;
+        if (sessionDurationMs < mMinMetricsSessionDurationMs) return;
+
+        // Collect and send IpClientRaInfoMetrics.
+        mIpClientRaInfoMetrics.setMaxNumberOfDistinctRas(mMaxDistinctRas);
+        mIpClientRaInfoMetrics.setNumberOfZeroLifetimeRas(mNumZeroLifetimeRas);
+        mIpClientRaInfoMetrics.setNumberOfParsingErrorRas(mNumParseErrorRas);
+        mIpClientRaInfoMetrics.setLowestRouterLifetimeSeconds(mLowestRouterLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestPioValidLifetimeSeconds(mLowestPioValidLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestRioRouteLifetimeSeconds(mLowestRioRouteLifetimeSeconds);
+        mIpClientRaInfoMetrics.setLowestRdnssLifetimeSeconds(mLowestRdnssLifetimeSeconds);
+        mIpClientRaInfoMetrics.statsWrite();
+
+        // Collect and send ApfSessionInfoMetrics.
+        mApfSessionInfoMetrics.setVersion(mApfCapabilities.apfVersionSupported);
+        mApfSessionInfoMetrics.setMemorySize(mApfCapabilities.maximumApfProgramSize);
+        mApfSessionInfoMetrics.setApfSessionDurationSeconds(
+                (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS));
+        mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates);
+        mApfSessionInfoMetrics.setMaxProgramSize(mMaxProgramSize);
+        for (Map.Entry<Counter, Long> entry : mApfCounterTracker.getCounters().entrySet()) {
+            if (entry.getValue() > 0) {
+                mApfSessionInfoMetrics.addApfCounter(entry.getKey(), entry.getValue());
+            }
+        }
+        mApfSessionInfoMetrics.statsWrite();
     }
 
     public synchronized void shutdown() {
+        collectAndSendMetrics();
         if (mReceiveThread != null) {
             log("shutting down");
             mReceiveThread.halt();  // Also closes socket.
@@ -2100,23 +2207,6 @@
         installNewProgramLocked();
     }
 
-    static public long counterValue(byte[] data, Counter counter)
-            throws ArrayIndexOutOfBoundsException {
-        // Follow the same wrap-around addressing scheme of the interpreter.
-        int offset = counter.offset();
-        if (offset < 0) {
-            offset = data.length + offset;
-        }
-
-        // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
-        long value = 0;
-        for (int i = 0; i < 4; i++) {
-            value = value << 8 | (data[offset] & 0xFF);
-            offset++;
-        }
-        return value;
-    }
-
     public synchronized void dump(IndentingPrintWriter pw) {
         pw.println("Capabilities: " + mApfCapabilities);
         pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
@@ -2201,11 +2291,17 @@
             try {
                 Counter[] counters = Counter.class.getEnumConstants();
                 for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
-                    long value = counterValue(mDataSnapshot, c);
+                    long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c);
                     // Only print non-zero counters
                     if (value != 0) {
                         pw.println(c.toString() + ": " + value);
                     }
+
+                    // If the counter's value decreases, it may have been cleaned up or there may be
+                    // a bug.
+                    if (value < mApfCounterTracker.getCounters().getOrDefault(c, 0L)) {
+                        Log.e(TAG, "Error: Counter value unexpectedly decreased.");
+                    }
                 }
             } catch (ArrayIndexOutOfBoundsException e) {
                 pw.println("Uh-oh: " + e);
@@ -2260,4 +2356,10 @@
         }
         return result;
     }
+
+    private void sendNetworkQuirkMetrics(final NetworkQuirkEvent event) {
+        if (mNetworkQuirkMetrics == null) return;
+        mNetworkQuirkMetrics.setEvent(event);
+        mNetworkQuirkMetrics.statsWrite();
+    }
 }
diff --git a/src/android/net/dhcp/DhcpAckPacket.java b/src/android/net/dhcp/DhcpAckPacket.java
index 225e447..3d20bda 100644
--- a/src/android/net/dhcp/DhcpAckPacket.java
+++ b/src/android/net/dhcp/DhcpAckPacket.java
@@ -50,6 +50,7 @@
                 + ", netmask " + mSubnetMask
                 + ", gateways " + mGateways + dnsServers
                 + ", lease time " + mLeaseTime
+                + ", domain " + mDomainName
                 + (mIpv6OnlyWaitTime != null ? ", V6ONLY_WAIT " + mIpv6OnlyWaitTime : "");
     }
 
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 4353746..b0bac04 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_DOMAIN_SEARCHLIST;
 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;
@@ -294,7 +295,14 @@
     @NonNull
     private byte[] getRequestedParams() {
         // Set an initial size large enough for all optional parameters that we might request.
-        final int numOptionalParams = 2;
+        // mCreatorId + the size is changed
+        final int numOptionalParams;
+        if (mConfiguration.isWifiManagedProfile) {
+            numOptionalParams = 3 + mConfiguration.options.size();
+        } else {
+            numOptionalParams = 2 + mConfiguration.options.size();
+        }
+
         final ByteArrayOutputStream params =
                 new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams);
         params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length);
@@ -306,6 +314,10 @@
         for (DhcpOption option : mConfiguration.options) {
             if (option.value == null) params.write(option.type);
         }
+        // Check if the target network is managed by user.
+        if (mConfiguration.isWifiManagedProfile) {
+            params.write(DHCP_DOMAIN_SEARCHLIST);
+        }
         return params.toByteArray();
     }
 
@@ -635,6 +647,9 @@
     private byte[] getOptionsToSkip() {
         final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2);
         if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL);
+        if (!mConfiguration.isWifiManagedProfile) {
+            optionsToSkip.write(DHCP_DOMAIN_SEARCHLIST);
+        }
         return optionsToSkip.toByteArray();
     }
 
@@ -992,12 +1007,15 @@
         public final boolean isPreconnectionEnabled;
         @NonNull
         public final List<DhcpOption> options;
+        public final boolean isWifiManagedProfile;
 
         public Configuration(@Nullable final String l2Key, final boolean isPreconnectionEnabled,
-                @NonNull final List<DhcpOption> options) {
+                @NonNull final List<DhcpOption> options,
+                final boolean isWifiManagedProfile) {
             this.l2Key = l2Key;
             this.isPreconnectionEnabled = isPreconnectionEnabled;
             this.options = options;
+            this.isWifiManagedProfile = isWifiManagedProfile;
         }
     }
 
diff --git a/src/android/net/dhcp/DhcpOfferPacket.java b/src/android/net/dhcp/DhcpOfferPacket.java
index e3e5d0f..daf520d 100644
--- a/src/android/net/dhcp/DhcpOfferPacket.java
+++ b/src/android/net/dhcp/DhcpOfferPacket.java
@@ -48,7 +48,7 @@
         }
 
         return s + " OFFER, ip " + mYourIp
-                + ", mask " + mSubnetMask + dnsServers
+                + ", netmask " + mSubnetMask + dnsServers
                 + ", gateways " + mGateways
                 + ", lease time " + mLeaseTime
                 + ", domain " + mDomainName
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
index 770baac..2649851 100644
--- a/src/android/net/dhcp/DhcpPacket.java
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -33,6 +33,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.net.module.util.DomainUtils;
 import com.android.net.module.util.Inet4AddressUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -338,6 +339,12 @@
     protected String mCaptivePortalUrl;
 
     /**
+     * DHCP Optional Type: Domain Search List, domain suffixes are space separated.
+     */
+    public static final byte DHCP_DOMAIN_SEARCHLIST = (byte) 119;
+    protected List<String> mDmnSrchList;
+
+    /**
      * DHCP zero-length option code: pad
      */
     public static final byte DHCP_OPTION_PAD = 0x00;
@@ -832,6 +839,7 @@
         }
     }
 
+    // The common server TLVs are corresponding to the parameter request list from client.
     protected void addCommonServerTlvs(ByteBuffer buf) {
         addTlv(buf, DHCP_LEASE_TIME, mLeaseTime);
         if (mLeaseTime != null && mLeaseTime != INFINITE_LEASE) {
@@ -854,6 +862,15 @@
         if (mIpv6OnlyWaitTime != null) {
             addTlv(buf, DHCP_IPV6_ONLY_PREFERRED, (int) Integer.toUnsignedLong(mIpv6OnlyWaitTime));
         }
+        if (mDmnSrchList != null && mDmnSrchList.size() > 0) {
+            // domain search list string is space separated.
+            String[] searchList = new String[mDmnSrchList.size()];
+            for (int i = 0; i < mDmnSrchList.size(); i++) {
+                searchList[i] = mDmnSrchList.get(i);
+            }
+            final byte[] domains = DomainUtils.encode(searchList, true /* compression */);
+            addTlv(buf, DHCP_DOMAIN_SEARCHLIST, domains);
+        }
         addTlv(buf, DHCP_CAPTIVE_PORTAL, mCaptivePortalUrl);
     }
 
@@ -987,6 +1004,7 @@
         byte[] clientId = null;
         List<Inet4Address> dnsServers = new ArrayList<>();
         List<Inet4Address> gateways = new ArrayList<>();  // aka router
+        ArrayList<String> dmnSrchList =  new ArrayList<>();
         Inet4Address serverIdentifier = null;
         Inet4Address netMask = null;
         String message = null;
@@ -1292,6 +1310,14 @@
                                 expectedLen = skipOption(packet, optionLen);
                             }
                             break;
+                        case DHCP_DOMAIN_SEARCHLIST:
+                            // TODO: should support multiple options(i.e. length > 255)?
+                            expectedLen = optionLen;
+                            final byte[] bytes = new byte[expectedLen];
+                            packet.get(bytes);
+                            final ByteBuffer buf = ByteBuffer.wrap(bytes);
+                            dmnSrchList = DomainUtils.decode(buf, true /* compression */);
+                            break;
                         default:
                             expectedLen = skipOption(packet, optionLen);
                     }
@@ -1359,7 +1385,6 @@
         newPacket.mBroadcastAddress = bcAddr;
         newPacket.mClientId = clientId;
         newPacket.mDnsServers = dnsServers;
-        newPacket.mDomainName = domainName;
         newPacket.mGateways = gateways;
         newPacket.mHostName = hostName;
         newPacket.mLeaseTime = leaseTime;
@@ -1382,6 +1407,10 @@
         } else {
             newPacket.mServerHostName = "";
         }
+        // Domain suffixes in the search list are concatenated to domain name with space separated,
+        // which will be set to DnsResolver via LinkProperties.
+        newPacket.mDmnSrchList = dmnSrchList;
+        newPacket.mDomainName = domainName;
         return newPacket;
     }
 
@@ -1451,7 +1480,10 @@
         results.mtu = (mMtu != null && MIN_MTU <= mMtu && mMtu <= MAX_MTU) ? mMtu : 0;
         results.serverHostName = mServerHostName;
         results.captivePortalApiUrl = mCaptivePortalUrl;
-
+        // Add the check before setting it
+        if (mDmnSrchList != null && mDmnSrchList.size() > 0) {
+            results.dmnsrchList.addAll(mDmnSrchList);
+        }
         return results;
     }
 
@@ -1512,7 +1544,8 @@
             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, Integer ipv6OnlyWaitTime) {
+            short mtu, String captivePortalUrl, Integer ipv6OnlyWaitTime,
+            List<String> domainSearchList) {
         DhcpPacket pkt = new DhcpOfferPacket(
                 transactionId, (short) 0, broadcast, serverIpAddr, relayIp,
                 INADDR_ANY /* clientIp */, yourIp, mac);
@@ -1526,6 +1559,9 @@
         pkt.mBroadcastAddress = bcAddr;
         pkt.mMtu = mtu;
         pkt.mCaptivePortalUrl = captivePortalUrl;
+        if (domainSearchList != null) {
+            pkt.mDmnSrchList = new ArrayList<>(domainSearchList);
+        }
         if (metered) {
             pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
         }
@@ -1546,7 +1582,8 @@
             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 */);
+                domainName, hostname, metered, mtu, captivePortalUrl, null /* V6ONLY_WAIT */,
+                null /* domainSearchList */);
     }
 
     /**
@@ -1557,7 +1594,8 @@
             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, Integer ipv6OnlyWaitTime) {
+            short mtu, boolean rapidCommit, String captivePortalUrl, Integer ipv6OnlyWaitTime,
+            List<String> domainSearchList) {
         DhcpPacket pkt = new DhcpAckPacket(
                 transactionId, (short) 0, broadcast, serverIpAddr, relayIp, requestClientIp, yourIp,
                 mac, rapidCommit);
@@ -1571,6 +1609,9 @@
         pkt.mBroadcastAddress = bcAddr;
         pkt.mMtu = mtu;
         pkt.mCaptivePortalUrl = captivePortalUrl;
+        if (domainSearchList != null) {
+            pkt.mDmnSrchList = new ArrayList<>(domainSearchList);
+        }
         if (metered) {
             pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
         }
@@ -1592,7 +1633,7 @@
         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 */);
+                captivePortalUrl, null /* V6ONLY_WAIT */, null /* domainSearchList */);
     }
 
     /**
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
index a107b9c..8d53048 100644
--- a/src/android/net/dhcp6/Dhcp6Client.java
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -20,9 +20,7 @@
 import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.IFA_F_NODAD;
 import static android.system.OsConstants.IPPROTO_UDP;
-import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 
@@ -31,14 +29,8 @@
 import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
-import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
-import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
-import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
-import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
 
 import android.content.Context;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
 import android.net.ip.IpClient;
 import android.net.util.SocketUtils;
 import android.os.Handler;
@@ -58,16 +50,14 @@
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.PacketReader;
-import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.structs.IaPrefixOption;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.Inet6Address;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Random;
 import java.util.function.IntSupplier;
 
@@ -95,8 +85,6 @@
     // Message.arg1 arguments to CMD_DHCP6_RESULT notification
     public static final int DHCP6_PD_SUCCESS = 1;
     public static final int DHCP6_PD_PREFIX_EXPIRED = 2;
-    public static final int DHCP6_PD_PREFIX_CHANGED = 3;
-    public static final int DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED = 4;
 
     // Notification from DHCPv6 state machine before quitting
     public static final int CMD_ON_QUIT = PUBLIC_BASE + 4;
@@ -385,8 +373,9 @@
     }
 
     private void scheduleLeaseTimers() {
-        // TODO: validate t1, t2, valid and preferred lifetimes before the timers are scheduled to
-        // prevent packet storms due to low timeouts.
+        // TODO: validate t1, t2, valid and preferred lifetimes before the timers are scheduled
+        // to prevent packet storms due to low timeouts. Preferred/valid lifetime of 0 should be
+        // excluded before scheduling the lease timer.
         int renewTimeout = mReply.t1;
         int rebindTimeout = mReply.t2;
         final long preferredTimeout = mReply.getMinimalPreferredLifetime();
@@ -435,8 +424,8 @@
         Log.d(TAG, "Scheduling IA_PD expiry in " + expirationTimeout + "s");
     }
 
-    private void notifyPrefixDelegation(int result, @Nullable final PrefixDelegation pd) {
-        mController.sendMessage(CMD_DHCP6_RESULT, result, 0, pd);
+    private void notifyPrefixDelegation(int result, @Nullable final List<IaPrefixOption> ipos) {
+        mController.sendMessage(CMD_DHCP6_RESULT, result, 0, ipos);
     }
 
     private void clearDhcp6State() {
@@ -556,10 +545,15 @@
             return sendSolicitPacket(transId, elapsedTimeMs, pd.build());
         }
 
-        // TODO: support multiple prefixes.
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
             final PrefixDelegation pd = packet.mPrefixDelegation;
+            // Ignore any Advertise or Reply for Solicit(with Rapid Commit) with NoPrefixAvail
+            // status code, retransmit Solicit to see if any valid response from other Servers.
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Solicit without available prefix, ignoring");
+                return;
+            }
             if (packet instanceof Dhcp6AdvertisePacket) {
                 Log.d(TAG, "Get prefix delegation option from Advertise: " + pd);
                 mAdvertise = pd;
@@ -600,6 +594,11 @@
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
             final PrefixDelegation pd = packet.mPrefixDelegation;
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Request without available prefix, restart Solicit");
+                transitionTo(mSolicitState);
+                return;
+            }
             Log.d(TAG, "Get prefix delegation option from Reply: " + pd);
             mReply = pd;
             mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs);
@@ -620,7 +619,7 @@
         public boolean processMessage(Message message) {
             switch (message.what) {
                 case CMD_DHCP6_PD_EXPIRE:
-                    notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, null);
+                    notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, mReply.getValidIaPrefixes());
                     transitionTo(mSolicitState);
                     return HANDLED;
                 default:
@@ -638,33 +637,6 @@
         }
     }
 
-    // Create an IPv6 address from the interface mac address with IFA_F_MANAGETEMPADDR
-    // flag, kernel will create another privacy IPv6 address on behalf of user space.
-    // We don't need to remember IPv6 addresses that need to extend the lifetime every
-    // time it enters BoundState.
-    private boolean addInterfaceAddress(@NonNull final Inet6Address address,
-            @NonNull final IaPrefixOption ipo) {
-        final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
-        final long now = SystemClock.elapsedRealtime();
-        final long deprecationTime = now + ipo.preferred;
-        final long expirationTime = now + ipo.valid;
-        final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags,
-                RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime);
-        if (!la.isGlobalPreferred()) {
-            Log.e(TAG, la + " is not a global preferred IPv6 address");
-            return false;
-        }
-        if (!NetlinkUtils.sendRtmNewAddressRequest(mIface.index, address,
-                (short) RFC7421_PREFIX_LENGTH,
-                flags, (byte) RT_SCOPE_UNIVERSE /* scope */,
-                ipo.preferred, ipo.valid)) {
-            Log.e(TAG, "Failed to set IPv6 address " + address.getHostAddress()
-                    + "%" + mIface.index);
-            return false;
-        }
-        return true;
-    }
-
     /**
      * Client has already obtained the lease(e.g. IA_PD option) from server and stays in Bound
      * state until T1 expires, and then transition to Renew state to extend the lease duration.
@@ -674,26 +646,9 @@
         public void enter() {
             super.enter();
             scheduleLeaseTimers();
-
-            // TODO: roll back to SOLICIT state after a delay if something wrong happens
-            // instead of returning directly.
-            for (IaPrefixOption ipo : mReply.getValidIaPrefixes()) {
-                // TODO: The prefix with preferred/valid lifetime of 0 is valid, but client
-                // should stop using the prefix immediately. Actually kernel doesn't accept
-                // the address with valid lifetime of 0 and returns EINVAL when it sees that.
-                // We should send RTM_DELADDR netlink message to kernel to delete these addresses
-                // from the interface if any.
-                // Configure IPv6 addresses based on the delegated prefix(es) on the interface.
-                // We've checked that delegated prefix is valid upon receiving the response from
-                // DHCPv6 server, and the server may assign a prefix with length less than 64. So
-                // for SLAAC use case we always set the prefix length to 64 even if the delegated
-                // prefix length is less than 64.
-                final IpPrefix prefix = ipo.getIpPrefix();
-                final Inet6Address address = createInet6AddressFromEui64(prefix,
-                        macAddressToEui64(mIface.macAddr));
-                if (!addInterfaceAddress(address, ipo)) continue;
-            }
-            notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply);
+            // Pass valid delegated prefix(es) to IpClient for IPv6 address configuration and
+            // active prefix(es) maintenance.
+            notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply.getValidIaPrefixes());
         }
 
         @Override
@@ -709,6 +664,27 @@
         }
     }
 
+
+    /**
+     *  Per RFC8415 section 18.2.10.1: Reply for renew or Rebind.
+     * - If all binding IA_PDs were renewed/rebound(so far we only support one IA_PD option per
+     *   interface), then move to BoundState to update the existing global IPv6 addresses lifetime
+     *   or install new global IPv6 address depending on the response from server.
+     * - Server may add new IA prefix option in Reply message(e.g. due to renumbering events), or
+     *   may choose to deprecate some prefixes if it cannot extend the lifetime by:
+     *     - either not including these requested IA prefixes in Reply message
+     *     - or setting the valid lifetime equals to T1/T2
+     *   That forces previous delegated prefixes to expire in a natural way, and client should
+     *   also stop trying to extend the lifetime for them. That being said, the global IPv6 address
+     *   lifetime won't be updated in BoundState if corresponding prefix doesn't appear in Reply
+     *   message, resulting in these global IPv6 addresses expire eventually and IpClient obtains
+     *   these updates via netlink message and remove the delegated prefix(es) from LinkProperties.
+     * - If some binding IA_PDs were absent in Reply message, client should still stay at RenewState
+     *   or RebindState and retransmit Renew/Rebind messages to see if it can get all later. So far
+     *   we only support one IA_PD option per interface, if the received Reply message doesn't take
+     *   any IA_Prefix option, then treat it as if IA_PD is absent, since there's no point in
+     *   returning BoundState again.
+     */
     abstract class ReacquireState extends MessageExchangeState {
         ReacquireState(final int irt, final int mrt) {
             super(0 /* delay */, irt, 0 /* MRC */, () -> mrt /* MRT */);
@@ -723,17 +699,16 @@
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
             final PrefixDelegation pd = packet.mPrefixDelegation;
-            final IaPrefixOption request = mReply.ipos.get(0);
-            final IaPrefixOption response = pd.ipos.get(0);
-            if (!(Arrays.equals(request.prefix, response.prefix)
-                    && request.prefixLen == response.prefixLen)) {
-                Log.i(TAG, "Renewal prefix " + HexDump.toHexString(response.prefix)
-                        + " does not match current prefix "
-                        + HexDump.toHexString(request.prefix));
-                notifyPrefixDelegation(DHCP6_PD_PREFIX_CHANGED, null);
-                transitionTo(mSolicitState);
+            // Stay at Renew/Rebind state if the Reply message takes NoPrefixAvail status code,
+            // retransmit Renew/Rebind message to server, to retry obtaining the prefixes.
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Renew/Rebind without available prefix, ignoring");
                 return;
             }
+            // TODO: send a Request message to the server that responded if any of the IA_PDs in
+            // Reply message contain NoBinding status code.
+            Log.d(TAG, "Get prefix delegation option from Reply as response to Renew/Rebind " + pd);
+            if (pd.ipos.isEmpty()) return;
             mReply = pd;
             mServerDuid = packet.mServerDuid;
             // Once the delegated prefix gets refreshed successfully we have to extend the
@@ -772,7 +747,9 @@
 
         @Override
         protected boolean sendPacket(int transId, long elapsedTimeMs) {
-            return sendRenewPacket(transId, elapsedTimeMs, mReply.build());
+            final List<IaPrefixOption> toBeRenewed = mReply.getRenewableIaPrefixes();
+            if (toBeRenewed.isEmpty()) return false;
+            return sendRenewPacket(transId, elapsedTimeMs, mReply.build(toBeRenewed));
         }
     }
 
@@ -788,7 +765,9 @@
 
         @Override
         protected boolean sendPacket(int transId, long elapsedTimeMs) {
-            return sendRebindPacket(transId, elapsedTimeMs, mReply.build());
+            final List<IaPrefixOption> toBeRebound = mReply.getRenewableIaPrefixes();
+            if (toBeRebound.isEmpty()) return false;
+            return sendRebindPacket(transId, elapsedTimeMs, mReply.build(toBeRebound));
         }
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index 7a977e5..53dd274 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -32,10 +32,8 @@
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.OptionalInt;
@@ -96,16 +94,16 @@
      * DHCPv6 Optional Type: Status Code.
      */
     public static final byte DHCP6_STATUS_CODE = 13;
+    private static final byte MIN_STATUS_CODE_OPT_LEN = 6;
     protected short mStatusCode;
-    protected String mStatusMsg;
 
     public static final short STATUS_SUCCESS           = 0;
     public static final short STATUS_UNSPEC_FAIL       = 1;
-    public static final short STATUS_NO_ADDR_AVAI      = 2;
+    public static final short STATUS_NO_ADDRS_AVAIL    = 2;
     public static final short STATUS_NO_BINDING        = 3;
-    public static final short STATUS_PREFIX_NOT_ONLINK = 4;
+    public static final short STATUS_NOT_ONLINK        = 4;
     public static final short STATUS_USE_MULTICAST     = 5;
-    public static final short STATUS_NO_PREFIX_AVAI    = 6;
+    public static final short STATUS_NO_PREFIX_AVAIL   = 6;
 
     /**
      * DHCPv6 zero-length Optional Type: Rapid Commit. Per RFC4039, both DHCPDISCOVER and DHCPACK
@@ -210,14 +208,22 @@
         public final int t2;
         @NonNull
         public final List<IaPrefixOption> ipos;
+        public final short statusCode;
 
+        @VisibleForTesting
         public PrefixDelegation(int iaid, int t1, int t2,
-                @NonNull final List<IaPrefixOption> ipos) {
+                @NonNull final List<IaPrefixOption> ipos, short statusCode) {
             Objects.requireNonNull(ipos);
             this.iaid = iaid;
             this.t1 = t1;
             this.t2 = t2;
             this.ipos = ipos;
+            this.statusCode = statusCode;
+        }
+
+        public PrefixDelegation(int iaid, int t1, int t2,
+                @NonNull final List<IaPrefixOption> ipos) {
+            this(iaid, t1, t2, ipos, STATUS_SUCCESS /* statusCode */);
         }
 
         /**
@@ -252,6 +258,7 @@
                 final int t1 = buffer.getInt();
                 final int t2 = buffer.getInt();
                 final List<IaPrefixOption> ipos = new ArrayList<IaPrefixOption>();
+                short statusCode = STATUS_SUCCESS;
                 while (buffer.remaining() > 0) {
                     final int original = buffer.position();
                     final short optionType = buffer.getShort();
@@ -263,12 +270,18 @@
                             Log.d(TAG, "IA Prefix Option: " + ipo);
                             ipos.add(ipo);
                             break;
-                        // TODO: support DHCP6_STATUS_CODE option
+                        case DHCP6_STATUS_CODE:
+                            statusCode = buffer.getShort();
+                            // Skip the status message if any.
+                            if (optionLen > 2) {
+                                skipOption(buffer, optionLen - 2);
+                            }
+                            break;
                         default:
                             skipOption(buffer, optionLen);
                     }
                 }
-                return new PrefixDelegation(iaid, t1, t2, ipos);
+                return new PrefixDelegation(iaid, t1, t2, ipos, statusCode);
             } catch (BufferUnderflowException e) {
                 throw new ParseException(e.getMessage());
             }
@@ -278,14 +291,31 @@
          * Build an IA_PD option from given specific parameters, including IA_PREFIX options.
          */
         public ByteBuffer build() {
+            return build(ipos);
+        }
+
+        /**
+         * Build an IA_PD option from given specific parameters, including IA_PREFIX options.
+         *
+         * Per RFC8415 section 21.13 if the Status Code option does not appear in a message in
+         * which the option could appear, the status of the message is assumed to be Success. So
+         * only put the Status Code option in IA_PD when the status code is not Success.
+         */
+        public ByteBuffer build(@NonNull final List<IaPrefixOption> input) {
             final ByteBuffer iapd = ByteBuffer.allocate(IaPdOption.LENGTH
-                    + Struct.getSize(IaPrefixOption.class) * ipos.size());
+                    + Struct.getSize(IaPrefixOption.class) * input.size()
+                    + (statusCode != STATUS_SUCCESS ? MIN_STATUS_CODE_OPT_LEN : 0));
             iapd.putInt(iaid);
             iapd.putInt(t1);
             iapd.putInt(t2);
-            for (IaPrefixOption ipo : ipos) {
+            for (IaPrefixOption ipo : input) {
                 ipo.writeToByteBuffer(iapd);
             }
+            if (statusCode != STATUS_SUCCESS) {
+                iapd.putShort(DHCP6_STATUS_CODE);
+                iapd.putShort((short) 2);
+                iapd.putShort(statusCode);
+            }
             iapd.flip();
             return iapd;
         }
@@ -308,29 +338,46 @@
 
         @Override
         public String toString() {
-            return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
-                    + ", IA prefix options: " + ipos;
+            return String.format("Prefix Delegation, iaid: %s, t1: %s, t2: %s, status code: %s,"
+                    + " IA prefix options: %s", iaid, t1, t2, statusCodeToString(statusCode), ipos);
         }
 
         /**
          * Compare the preferred lifetime in the IA prefix optin list and return the minimum one.
-         * TODO: exclude 0 preferred lifetime.
          */
         public long getMinimalPreferredLifetime() {
-            final IaPrefixOption ipo = Collections.min(ipos,
-                    (IaPrefixOption lhs, IaPrefixOption rhs) -> Long.compare(lhs.preferred,
-                            rhs.preferred));
-            return ipo.preferred;
+            long min = Long.MAX_VALUE;
+            for (IaPrefixOption ipo : ipos) {
+                min = (ipo.preferred != 0 && min > ipo.preferred) ? ipo.preferred : min;
+            }
+            return min;
         }
 
         /**
          * Compare the valid lifetime in the IA prefix optin list and return the minimum one.
-         * TODO: exclude 0 valid lifetime.
          */
         public long getMinimalValidLifetime() {
-            final IaPrefixOption ipo = Collections.min(ipos,
-                    (IaPrefixOption lhs, IaPrefixOption rhs) -> Long.compare(lhs.valid, rhs.valid));
-            return ipo.valid;
+            long min = Long.MAX_VALUE;
+            for (IaPrefixOption ipo : ipos) {
+                min = (ipo.valid != 0 && min > ipo.valid) ? ipo.valid : min;
+            }
+            return min;
+        }
+
+        /**
+         * Return IA prefix option list to be renewed/rebound.
+         *
+         * Per RFC8415#section-18.2.4, client must not include any prefixes that it didn't obtain
+         * from server or that are no longer valid (that have a valid lifetime of 0). Section-18.3.4
+         * also mentions that server can inform client that it will not extend the prefix by setting
+         * T1 and T2 to values equal to the valid lifetime, so in this case client has no point in
+         * renewing as well.
+         */
+        public List<IaPrefixOption> getRenewableIaPrefixes() {
+            final List<IaPrefixOption> toBeRenewed = getValidIaPrefixes();
+            toBeRenewed.removeIf(ipo -> ipo.preferred == 0 && ipo.valid == 0);
+            toBeRenewed.removeIf(ipo -> t1 == ipo.valid && t2 == ipo.valid);
+            return toBeRenewed;
         }
     }
 
@@ -343,6 +390,27 @@
         }
     }
 
+    private static String statusCodeToString(short statusCode) {
+        switch (statusCode) {
+            case STATUS_SUCCESS:
+                return "Success";
+            case STATUS_UNSPEC_FAIL:
+                return "UnspecFail";
+            case STATUS_NO_ADDRS_AVAIL:
+                return "NoAddrsAvail";
+            case STATUS_NO_BINDING:
+                return "NoBinding";
+            case STATUS_NOT_ONLINK:
+                return "NotOnLink";
+            case STATUS_USE_MULTICAST:
+                return "UseMulticast";
+            case STATUS_NO_PREFIX_AVAIL:
+                return "NoPrefixAvail";
+            default:
+                return "Unknown";
+        }
+    }
+
     private static void skipOption(@NonNull final ByteBuffer packet, int optionLen)
             throws BufferUnderflowException {
         for (int i = 0; i < optionLen; i++) {
@@ -351,35 +419,6 @@
     }
 
     /**
-     * Reads a string of specified length from the buffer.
-     *
-     * TODO: move to a common place which can be shared with DhcpClient.
-     */
-    private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount,
-            boolean isNullOk) {
-        final byte[] bytes = new byte[byteCount];
-        buf.get(bytes);
-        return readAsciiString(bytes, isNullOk);
-    }
-
-    private static String readAsciiString(@NonNull final byte[] payload, boolean isNullOk) {
-        final byte[] bytes = payload;
-        int length = bytes.length;
-        if (!isNullOk) {
-            // Stop at the first null byte. This is because some DHCP options (e.g., the domain
-            // name) are passed to netd via FrameworkListener, which refuses arguments containing
-            // null bytes. We don't do this by default because vendorInfo is an opaque string which
-            // could in theory contain null bytes.
-            for (length = 0; length < bytes.length; length++) {
-                if (bytes[length] == 0) {
-                    break;
-                }
-            }
-        }
-        return new String(bytes, 0, length, StandardCharsets.US_ASCII);
-    }
-
-    /**
      * Creates a concrete Dhcp6Packet from the supplied ByteBuffer.
      *
      * The buffer only starts with a UDP encapsulation (i.e. DHCPv6 message). A subset of the
@@ -403,7 +442,6 @@
         byte[] serverDuid = null;
         byte[] clientDuid = null;
         short statusCode = STATUS_SUCCESS;
-        String statusMsg = null;
         boolean rapidCommit = false;
         int solMaxRt = 0;
         PrefixDelegation pd = null;
@@ -464,7 +502,12 @@
                     case DHCP6_STATUS_CODE:
                         expectedLen = optionLen;
                         statusCode = packet.getShort();
-                        statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
+                        // Skip the status message (if any), which is a UTF-8 encoded text string
+                        // suitable for display to the end user, but is not useful for Dhcp6Client
+                        // to decide how to properly handle the status code.
+                        if (optionLen - 2 > 0) {
+                            skipOption(packet, optionLen - 2);
+                        }
                         break;
                     case DHCP6_SOL_MAX_RT:
                         expectedLen = 4;
@@ -518,9 +561,10 @@
         if (pd != null) {
             newPacket.mPrefixDelegation = pd;
             newPacket.mIaId = pd.iaid;
+        } else {
+            throw new ParseException("Missing IA_PD option");
         }
         newPacket.mStatusCode = statusCode;
-        newPacket.mStatusMsg = statusMsg;
         newPacket.mRapidCommit = rapidCommit;
         newPacket.mSolMaxRt =
                 (solMaxRt >= 60 && solMaxRt <= 86400)
@@ -556,10 +600,8 @@
             Log.e(TAG, "Unexpected transaction ID " + mTransId + ", expected " + transId);
             return false;
         }
-        if (mPrefixDelegation == null) {
-            Log.e(TAG, "DHCPv6 message without IA_PD option, ignoring");
-            return false;
-        }
+        // mPrefixDelegation is guaranteed to be non-null. In decode() function, we throw the
+        // exception if IA_PD option doesn't exist.
         if (!mPrefixDelegation.isValid()) {
             Log.e(TAG, "DHCPv6 message takes invalid IA_PD option, ignoring");
             return false;
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 9747c2c..d7ef4df 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -19,7 +19,6 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
-import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation;
 import static android.net.ip.IIpClient.PROV_IPV4_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_LINKLOCAL;
@@ -32,25 +31,35 @@
 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.IFA_F_NODAD;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
+import static com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 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.RFC7421_PREFIX_LENGTH;
 import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
+import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
+import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
 import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE;
 import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
+import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import android.annotation.SuppressLint;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.DhcpResults;
@@ -93,12 +102,14 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.stats.connectivity.DisconnectCode;
 import android.stats.connectivity.NetworkQuirkEvent;
 import android.stats.connectivity.NudEventType;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.LocalLog;
 import android.util.Log;
 import android.util.Pair;
@@ -117,8 +128,10 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.SocketUtils;
 import com.android.net.module.util.arp.ArpPacket;
@@ -148,7 +161,6 @@
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.net.URL;
-import java.net.UnknownHostException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -374,13 +386,15 @@
         /**
          * Called to indicate that a new APF program must be installed to filter incoming packets.
          */
-        public void installPacketFilter(byte[] filter) {
+        public boolean installPacketFilter(byte[] filter) {
             log("installPacketFilter(byte[" + filter.length + "])");
             try {
                 mCallback.installPacketFilter(filter);
             } catch (RemoteException e) {
                 log("Failed to call installPacketFilter", e);
+                return false;
             }
+            return true;
         }
 
         /**
@@ -516,6 +530,7 @@
     private static final int CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY = 18;
     private static final int CMD_UPDATE_APF_CAPABILITIES = 19;
     private static final int EVENT_IPV6_AUTOCONF_TIMEOUT = 20;
+    private static final int CMD_UPDATE_APF_DATA_SNAPSHOT = 21;
 
     private static final int ARG_LINKPROP_CHANGED_LINKSTATE_DOWN = 0;
     private static final int ARG_LINKPROP_CHANGED_LINKSTATE_UP = 1;
@@ -532,6 +547,8 @@
 
     // IpClient shares a handler with Dhcp6Client: commands must not overlap
     public static final int DHCP6CLIENT_CMD_BASE = 2000;
+    private static final int DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS =
+            IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE | IFA_F_NODAD;
 
     // Settings and default values.
     private static final int MAX_LOG_RECORDS = 500;
@@ -547,6 +564,12 @@
     @VisibleForTesting
     static final int DEFAULT_ACCEPT_RA_MIN_LFT = 180;
 
+    @VisibleForTesting
+    static final String CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS =
+            "ipclient_apf_counter_polling_interval_secs";
+    @VisibleForTesting
+    static final int DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS = 300;
+
     // Used to wait for the provisioning to complete eventually and then decide the target
     // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6
     // provisioning connection latency metrics, the latency of 95% can go up to 16s, so pick
@@ -662,6 +685,10 @@
     private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
     // Set of IPv6 addresses from which multicast NS packets have been sent.
     private final Set<Inet6Address> mMulticastNsSourceAddresses = new HashSet<>();
+    // Set of delegated prefixes.
+    private final Set<IpPrefix> mDelegatedPrefixes = new HashSet<>();
+    @Nullable
+    private final DevicePolicyManager mDevicePolicyManager;
 
     // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
     private final int mMinRdnssLifetimeSec;
@@ -669,11 +696,15 @@
     // Ignore any nonzero RA section with lifetime below this value.
     private final int mAcceptRaMinLft;
 
+    // Polling interval to update APF data snapshot
+    private final long mApfCounterPollingIntervalMs;
+
     // Experiment flag read from device config.
     private final boolean mDhcp6PrefixDelegationEnabled;
     private final boolean mUseNewApfFilter;
     private final boolean mEnableIpClientIgnoreLowRaLifetime;
     private final boolean mApfShouldHandleLightDoze;
+    private final boolean mEnableApfPollingCounters;
 
     private InterfaceParams mInterfaceParams;
 
@@ -691,6 +722,7 @@
     private AndroidPacketFilter mApfFilter;
     private String mL2Key; // The L2 key for this network, for writing into the memory store
     private String mCluster; // The cluster for this network, for writing into the memory store
+    private int mCreatorUid; // Uid of app creating the wifi configuration
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
     private long mIPv6ProvisioningDtimGracePeriodMillis;
@@ -699,7 +731,6 @@
     private Integer mDadTransmits = null;
     private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
     private ApfCapabilities mCurrentApfCapabilities;
-    private PrefixDelegation mPrefixDelegation;
     private WakeupMessage mIpv6AutoconfTimeoutAlarm = null;
 
     /**
@@ -828,11 +859,13 @@
          */
         public AndroidPacketFilter maybeCreateApfFilter(Context context,
                 ApfFilter.ApfConfiguration config, InterfaceParams ifParams,
-                IpClientCallbacksWrapper cb, boolean useNewApfFilter) {
+                IpClientCallbacksWrapper cb, NetworkQuirkMetrics networkQuirkMetrics,
+                boolean useNewApfFilter) {
             if (useNewApfFilter) {
-                return ApfFilter.maybeCreate(context, config, ifParams, cb);
+                return ApfFilter.maybeCreate(context, config, ifParams, cb, networkQuirkMetrics);
             } else {
-                return LegacyApfFilter.maybeCreate(context, config, ifParams, cb);
+                return LegacyApfFilter.maybeCreate(context, config, ifParams, cb,
+                        networkQuirkMetrics);
             }
         }
 
@@ -844,6 +877,14 @@
             final File sysctl = new File(path);
             return sysctl.exists();
         }
+         /**
+         * Get the configuration from RRO to check whether or not to send domain search list
+         * option in DHCPDISCOVER/DHCPREQUEST message.
+         */
+        public boolean getSendDomainSearchListOption(final Context context) {
+            return context.getResources().getBoolean(R.bool.config_dhcp_client_domain_search_list);
+        }
+
     }
 
     public IpClient(Context context, String ifName, IIpClientCallbacks callback,
@@ -861,6 +902,8 @@
 
         mTag = getName();
 
+        mDevicePolicyManager = (DevicePolicyManager)
+                context.getSystemService(Context.DEVICE_POLICY_SERVICE);
         mContext = context;
         mInterfaceName = ifName;
         mDependencies = deps;
@@ -890,7 +933,12 @@
                 CONFIG_MIN_RDNSS_LIFETIME, DEFAULT_MIN_RDNSS_LIFETIME);
         mAcceptRaMinLft = mDependencies.getDeviceConfigPropertyInt(CONFIG_ACCEPT_RA_MIN_LFT,
                 DEFAULT_ACCEPT_RA_MIN_LFT);
+        mApfCounterPollingIntervalMs = mDependencies.getDeviceConfigPropertyInt(
+                CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
+                DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS) * DateUtils.SECOND_IN_MILLIS;
         mUseNewApfFilter = mDependencies.isFeatureEnabled(context, APF_NEW_RA_FILTER_VERSION);
+        mEnableApfPollingCounters = mDependencies.isFeatureEnabled(context,
+                APF_POLLING_COUNTERS_VERSION);
         mEnableIpClientIgnoreLowRaLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
         // Light doze mode status checking API is only available at T or later releases.
@@ -1089,17 +1137,13 @@
     }
 
     private boolean isGratuitousNaEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION);
+        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_GRATUITOUS_NA_VERSION);
     }
 
     private boolean isGratuitousArpNaRoamingEnabled() {
         return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION);
     }
 
-    private boolean isMulticastNsEnabled() {
-        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_MULTICAST_NS_VERSION);
-    }
-
     @VisibleForTesting
     static MacAddress getInitialBssid(final Layer2Information layer2Info,
             final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
@@ -1150,6 +1194,7 @@
         mCurrentBssid = getInitialBssid(req.mLayer2Info, req.mScanResultInfo,
                 ShimUtils.isAtLeastS());
         mCurrentApfCapabilities = req.mApfCapabilities;
+        mCreatorUid = req.mCreatorUid;
         if (req.mLayer2Info != null) {
             mL2Key = req.mLayer2Info.mL2Key;
             mCluster = req.mLayer2Info.mCluster;
@@ -1420,7 +1465,6 @@
         mDhcpResults = null;
         mTcpBufferSizes = "";
         mHttpProxy = null;
-        mPrefixDelegation = null;
 
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
@@ -1701,6 +1745,31 @@
         addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
         mShim.setNat64Prefix(newLp, mShim.getNat64Prefix(netlinkLinkProperties));
 
+        // Check if any link address update from netlink.
+        final CompareResult<LinkAddress> results =
+                LinkPropertiesUtils.compareAddresses(mLinkProperties, newLp);
+        // In the case that there are multiple netlink update events about a global IPv6 address
+        // derived from the delegated prefix, a flag-only change event(e.g. due to the duplicate
+        // address detection) will cause an identical IP address to be put into both Added and
+        // Removed list based on the CompareResult implementation. To prevent a prefix from being
+        // mistakenly removed from the delegate prefix list, it is better to always check the
+        // removed list before checking the added list(e.g. anyway we can add the removed prefix
+        // back again).
+        for (LinkAddress la : results.removed) {
+            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
+                mDelegatedPrefixes.remove(prefix);
+            }
+            // TODO: remove onIpv6AddressRemoved callback.
+        }
+
+        for (LinkAddress la : results.added) {
+            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
+                mDelegatedPrefixes.add(prefix);
+            }
+        }
+
         // [3] Add in data from DHCPv4, if available.
         //
         // mDhcpResults is never shared with any other owner so we don't have
@@ -1712,7 +1781,12 @@
                 newLp.addRoute(route);
             }
             addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
-            newLp.setDomains(mDhcpResults.domains);
+            if (mDhcpResults.dmnsrchList.size() == 0) {
+                newLp.setDomains(mDhcpResults.domains);
+            } else {
+                final String domainsString = mDhcpResults.appendDomainsSearchList();
+                newLp.setDomains(TextUtils.isEmpty(domainsString) ? null : domainsString);
+            }
 
             if (mDhcpResults.mtu != 0) {
                 newLp.setMtu(mDhcpResults.mtu);
@@ -1732,26 +1806,22 @@
             // TODO: also look at the IPv6 RA (netlink) for captive portal URL
         }
 
-        // [4] Add in data from DHCPv6 Prefix Delegation, if available.
-        if (mPrefixDelegation != null) {
-            for (IaPrefixOption ipo : mPrefixDelegation.ipos) {
-                try {
-                    final IpPrefix destination =
-                            new IpPrefix(Inet6Address.getByAddress(ipo.prefix), ipo.prefixLen);
-                    // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to this route
-                    // based on the delegated prefix. To prevent the traffic loop between host and
-                    // upstream delegated router. Because we specify the IFA_F_NOPREFIXROUTE when
-                    // adding the IPv6 address, the kernel does not create a delegated prefix route,
-                    // as a result, the user space won't receive any RTM_NEWROUTE message about the
-                    // delegated prefix, we still need to install an unreachable route for the
-                    // delegated prefix manually in LinkProperties to notify the caller this update.
-                    // TODO: support RTN_BLACKHOLE in netd and use that on newer Android versions.
-                    final RouteInfo route = new RouteInfo(destination, null /* gateway */,
-                            mInterfaceName, RTN_UNREACHABLE);
-                    newLp.addRoute(route);
-                } catch (UnknownHostException e) {
-                    Log.wtf(mTag, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix));
-                }
+        // [4] Add route with delegated prefix according to the global address update.
+        if (mDhcp6PrefixDelegationEnabled) {
+            for (IpPrefix destination : mDelegatedPrefixes) {
+                // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to
+                // this route based on the delegated prefix. To prevent the traffic loop
+                // between host and upstream delegated router. Because we specify the
+                // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not
+                // create a delegated prefix route, as a result, the user space won't
+                // receive any RTM_NEWROUTE message about the delegated prefix, we still
+                // need to install an unreachable route for the delegated prefix manually
+                // in LinkProperties to notify the caller this update.
+                // TODO: support RTN_BLACKHOLE in netd and use that on newer Android
+                // versions.
+                final RouteInfo route = new RouteInfo(destination,
+                        null /* gateway */, mInterfaceName, RTN_UNREACHABLE);
+                newLp.addRoute(route);
             }
         }
 
@@ -1949,6 +2019,24 @@
         }
     }
 
+    private static boolean hasFlag(@NonNull final LinkAddress la, final int flags) {
+        return (la.getFlags() & flags) == flags;
+
+    }
+
+    // Check whether a global IPv6 stable address is derived from DHCPv6 prefix delegation.
+    // Address derived from delegated prefix should be:
+    // - unicast global routable address
+    // - with prefix length of 64
+    // - has IFA_F_MANAGETEMPADDR, IFA_F_NOPREFIXROUTE and IFA_F_NODAD flags
+    private static boolean isIpv6StableDelegatedAddress(@NonNull final LinkAddress la) {
+        return la.isIpv6()
+                && !ConnectivityUtils.isIPv6ULA(la.getAddress())
+                && (la.getPrefixLength() == RFC7421_PREFIX_LENGTH)
+                && (la.getScope() == (byte) RT_SCOPE_UNIVERSE)
+                && hasFlag(la, DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS);
+    }
+
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
@@ -1990,9 +2078,7 @@
         //
         // TODO: stop sending this multicast NS after deployment of RFC9131 in the field, leverage
         // the gratuitous NA to update the first-hop router's neighbor cache entry.
-        if (isMulticastNsEnabled()) {
-            maybeSendMulticastNSes(newLp);
-        }
+        maybeSendMulticastNSes(newLp);
 
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
@@ -2366,8 +2452,9 @@
         apfConfig.minRdnssLifetimeSec = mMinRdnssLifetimeSec;
         apfConfig.acceptRaMinLft = mAcceptRaMinLft;
         apfConfig.shouldHandleLightDoze = mApfShouldHandleLightDoze;
+        apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
-                mCallback, mUseNewApfFilter);
+                mCallback, mNetworkQuirkMetrics, mUseNewApfFilter);
     }
 
     private boolean handleUpdateApfCapabilities(@NonNull final ApfCapabilities apfCapabilities) {
@@ -2395,6 +2482,7 @@
             mHasDisabledAcceptRaDefrtrOnProvLoss = false;
             mGratuitousNaTargetAddresses.clear();
             mMulticastNsSourceAddresses.clear();
+            mDelegatedPrefixes.clear();
 
             resetLinkProperties();
             if (mStartTimeMillis > 0) {
@@ -2570,8 +2658,92 @@
         // registerForPreDhcpNotification is called later when processing the CMD_*_PRECONNECTION
         // messages.
         if (!isUsingPreconnection()) mDhcpClient.registerForPreDhcpNotification();
+        boolean isManagedWifiProfile = false;
+        if (mDependencies.getSendDomainSearchListOption(mContext)
+                && (mCreatorUid > 0) && (isDeviceOwnerNetwork(mCreatorUid)
+                || isProfileOwner(mCreatorUid))) {
+            isManagedWifiProfile = true;
+        }
         mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP, new DhcpClient.Configuration(mL2Key,
-                isUsingPreconnection(), options));
+                isUsingPreconnection(), options, isManagedWifiProfile));
+    }
+
+    private boolean hasPermission(String permissionName) {
+        return (mContext.checkCallingOrSelfPermission(permissionName)
+                == PackageManager.PERMISSION_GRANTED);
+    }
+
+    private boolean isDeviceOwnerNetwork(int creatorUid) {
+        if (mDevicePolicyManager == null) return false;
+        if (!hasPermission(android.Manifest.permission.MANAGE_USERS)) return false;
+        final ComponentName devicecmpName = mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser();
+        if (devicecmpName == null) return false;
+        final String deviceOwnerPackageName = devicecmpName.getPackageName();
+        if (deviceOwnerPackageName == null) return false;
+
+        final String[] packages = mContext.getPackageManager().getPackagesForUid(creatorUid);
+
+        for (String pkg : packages) {
+            if (pkg.equals(deviceOwnerPackageName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    private Context createPackageContextAsUser(int uid) {
+        Context userContext = null;
+        try {
+            userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
+                    UserHandle.getUserHandleForUid(uid));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Unknown package name");
+            return null;
+        }
+        return userContext;
+    }
+
+    /**
+     * Returns the DevicePolicyManager from context
+     */
+    private DevicePolicyManager retrieveDevicePolicyManagerFromContext(Context context) {
+        DevicePolicyManager devicePolicyManager =
+                context.getSystemService(DevicePolicyManager.class);
+        if (devicePolicyManager == null
+                && context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_DEVICE_ADMIN)) {
+            Log.wtf(TAG, "Error retrieving DPM service");
+        }
+        return devicePolicyManager;
+    }
+
+    private DevicePolicyManager retrieveDevicePolicyManagerFromUserContext(int uid) {
+        Context userContext = createPackageContextAsUser(uid);
+        if (userContext == null) return null;
+        return retrieveDevicePolicyManagerFromContext(userContext);
+    }
+
+    /**
+     * Returns {@code true} if the calling {@code uid} is the profile owner
+     *
+     */
+
+    private boolean isProfileOwner(int uid) {
+        DevicePolicyManager devicePolicyManager = retrieveDevicePolicyManagerFromUserContext(uid);
+        if (devicePolicyManager == null) return false;
+        String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+        if (packages == null) {
+            Log.w(TAG, "isProfileOwner: could not find packages for uid="
+                    + uid);
+            return false;
+        }
+        for (String packageName : packages) {
+            if (devicePolicyManager.isProfileOwnerApp(packageName)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     class ClearingIpAddressesState extends State {
@@ -2773,6 +2945,9 @@
             if (mApfFilter == null) {
                 mCallback.setFallbackMulticastFilter(mMulticastFiltering);
             }
+            if (mEnableApfPollingCounters) {
+                sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
+            }
 
             mPacketTracker = createPacketTracker();
             if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);
@@ -2830,6 +3005,8 @@
             }
 
             resetLinkProperties();
+
+            removeMessages(CMD_UPDATE_APF_DATA_SNAPSHOT);
         }
 
         private void enqueueJumpToStoppingState(final DisconnectCode code) {
@@ -2863,30 +3040,71 @@
             }
         }
 
-        private void clearIpv6PrefixDelegationAddresses() {
-            if (mPrefixDelegation == null) {
-                Log.wtf(mTag, "PrefixDelegation shouldn't be null when DHCPv6 PD fails.");
-                return;
-            }
-            final IaPrefixOption ipo = mPrefixDelegation.ipos.get(0);
-            final IpPrefix prefix;
-            try {
-                prefix = new IpPrefix(Inet6Address.getByAddress(ipo.prefix), RFC7421_PREFIX_LENGTH);
-            } catch (UnknownHostException e) {
-                Log.wtf(TAG, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix));
-                return;
-            }
-
-            // Delete the global IPv6 address based on delegated prefix from interface.
+        private void deleteIpv6PrefixDelegationAddresses(final IpPrefix prefix) {
             for (LinkAddress la : mLinkProperties.getLinkAddresses()) {
                 final InetAddress address = la.getAddress();
                 if (prefix.contains(address)) {
-                    NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index,
-                            (Inet6Address) address, (short) la.getPrefixLength());
+                    if (!NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index,
+                            (Inet6Address) address, (short) la.getPrefixLength())) {
+                        Log.e(TAG, "Failed to delete IPv6 address " + address.getHostAddress());
+                    }
                 }
             }
         }
 
+        private void addInterfaceAddress(@NonNull final Inet6Address address,
+                @NonNull final IaPrefixOption ipo) {
+            final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
+            final long now = SystemClock.elapsedRealtime();
+            final long deprecationTime = now + ipo.preferred;
+            final long expirationTime = now + ipo.valid;
+            final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags,
+                    RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime);
+            if (!la.isGlobalPreferred()) {
+                Log.w(TAG, la + " is not a global IPv6 address");
+                return;
+            }
+            if (!NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index, address,
+                    (short) RFC7421_PREFIX_LENGTH,
+                    flags, (byte) RT_SCOPE_UNIVERSE /* scope */,
+                    ipo.preferred, ipo.valid)) {
+                Log.e(TAG, "Failed to set IPv6 address on " + address.getHostAddress()
+                        + "%" + mInterfaceParams.index);
+            }
+        }
+
+        private void updateDelegatedAddresses(@NonNull final List<IaPrefixOption> valid) {
+            if (valid.isEmpty()) return;
+            for (IaPrefixOption ipo : valid) {
+                final IpPrefix prefix = ipo.getIpPrefix();
+                // The prefix with preferred/valid lifetime of 0 is considered as a valid prefix,
+                // it can be passed to IpClient from Dhcp6Client, however, client should stop using
+                // the global addresses derived from this prefix immediately.
+                if (ipo.withZeroLifetimes()) {
+                    Log.d(TAG, "Delete IPv6 address derived from prefix " + prefix
+                            + " with 0 preferred/valid lifetime");
+                    deleteIpv6PrefixDelegationAddresses(prefix);
+                }
+                // Otherwise, configure IPv6 addresses derived from the delegated prefix(es) on
+                // the interface. We've checked that delegated prefix is valid upon receiving the
+                // response from DHCPv6 server, and the server may assign a prefix with length less
+                // than 64. So for SLAAC use case we always set the prefix length to 64 even if the
+                // delegated prefix length is less than 64.
+                final Inet6Address address = createInet6AddressFromEui64(prefix,
+                        macAddressToEui64(mInterfaceParams.macAddr));
+                addInterfaceAddress(address, ipo);
+            }
+        }
+
+        private void removeExpiredDelegatedAddresses(@NonNull final List<IaPrefixOption> expired) {
+            if (expired.isEmpty()) return;
+            for (IaPrefixOption ipo : expired) {
+                final IpPrefix prefix = ipo.getIpPrefix();
+                Log.d(TAG, "Delete IPv6 address derived from expired prefix " + prefix);
+                deleteIpv6PrefixDelegationAddresses(prefix);
+            }
+        }
+
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
@@ -3081,15 +3299,14 @@
                 case Dhcp6Client.CMD_DHCP6_RESULT:
                     switch(msg.arg1) {
                         case Dhcp6Client.DHCP6_PD_SUCCESS:
-                            mPrefixDelegation = (PrefixDelegation) msg.obj;
+                            final List<IaPrefixOption> toBeUpdated = (List<IaPrefixOption>) msg.obj;
+                            updateDelegatedAddresses(toBeUpdated);
                             handleLinkPropertiesUpdate(SEND_CALLBACKS);
                             break;
 
                         case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED:
-                        case Dhcp6Client.DHCP6_PD_PREFIX_CHANGED:
-                        case Dhcp6Client.DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED:
-                            clearIpv6PrefixDelegationAddresses();
-                            mPrefixDelegation = null;
+                            final List<IaPrefixOption> toBeRemoved = (List<IaPrefixOption>) msg.obj;
+                            removeExpiredDelegatedAddresses(toBeRemoved);
                             handleLinkPropertiesUpdate(SEND_CALLBACKS);
                             break;
 
@@ -3121,6 +3338,11 @@
                     }
                     break;
 
+                case CMD_UPDATE_APF_DATA_SNAPSHOT:
+                    mCallback.startReadPacketFilter();
+                    sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
+                    break;
+
                 default:
                     return NOT_HANDLED;
             }
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 40a4bb6..e252a68 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -23,7 +23,6 @@
 
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -236,7 +235,6 @@
     private int mInterSolicitIntervalMs;
     @NonNull
     private final Callback mCallback;
-    private final boolean mMulticastResolicitEnabled;
     private final boolean mIgnoreIncompleteIpv6DnsServerEnabled;
     private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled;
 
@@ -260,8 +258,6 @@
         mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
         mCm = context.getSystemService(ConnectivityManager.class);
         mDependencies = dependencies;
-        mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context,
-                IP_REACHABILITY_MCAST_RESOLICIT_VERSION);
         mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context,
                 IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION);
         mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context,
@@ -274,10 +270,8 @@
         // In case the overylaid parameters specify an invalid configuration, set the parameters
         // to the hardcoded defaults first, then set them to the values used in the steady state.
         try {
-            int numResolicits = mMulticastResolicitEnabled
-                    ? NUD_MCAST_RESOLICIT_NUM
-                    : INVALID_NUD_MCAST_RESOLICIT_NUM;
-            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, numResolicits);
+            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS,
+                    NUD_MCAST_RESOLICIT_NUM);
         } catch (Exception e) {
             Log.e(TAG, "Failed to adjust neighbor parameters with hardcoded defaults");
         }
@@ -414,8 +408,7 @@
 
     private void handleNeighborReachable(@Nullable final NeighborEvent prev,
             @NonNull final NeighborEvent event) {
-        if (mMulticastResolicitEnabled
-                && hasDefaultRouterNeighborMacAddressChanged(prev, event)) {
+        if (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
@@ -581,8 +574,7 @@
 
     private long getProbeWakeLockDuration() {
         final long gracePeriodMs = 500;
-        final int numSolicits =
-                mNumSolicits + (mMulticastResolicitEnabled ? NUD_MCAST_RESOLICIT_NUM : 0);
+        final int numSolicits = mNumSolicits + NUD_MCAST_RESOLICIT_NUM;
         return (long) (numSolicits * mInterSolicitIntervalMs) + gracePeriodMs;
     }
 
diff --git a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
new file mode 100644
index 0000000..32f7c10
--- /dev/null
+++ b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 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.net.apf.ApfCounterTracker.Counter;
+import android.stats.connectivity.CounterName;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Class to record the network stack ApfSessionInfo metrics into statsd.
+ *
+ * This class is not thread-safe, and should always be accessed from the same thread.
+ *
+ * @hide
+ */
+public class ApfSessionInfoMetrics {
+    // Define the maximum size of the counter list
+    public static final int MAX_NUM_OF_COUNTERS = Counter.class.getEnumConstants().length - 1;
+    private final ApfSessionInfoReported.Builder mStatsBuilder =
+            ApfSessionInfoReported.newBuilder();
+    private final ApfCounterList.Builder mApfCounterListBuilder = ApfCounterList.newBuilder();
+
+    /**
+     * Write the version to mStatsBuilder.
+     */
+    public void setVersion(final int version) {
+        mStatsBuilder.setVersion(version);
+    }
+
+    /**
+     * Write the memory size to mStatsBuilder.
+     */
+    public void setMemorySize(final int memorySize) {
+        mStatsBuilder.setMemorySize(memorySize);
+    }
+
+    /**
+     * Add an APF counter to the metrics builder.
+     */
+    public void addApfCounter(final Counter counter, final long value) {
+        if (mApfCounterListBuilder.getApfCounterCount() >= MAX_NUM_OF_COUNTERS) return;
+        final ApfCounter.Builder apfCounterBuilder = ApfCounter.newBuilder()
+                .setCounterName(apfFilterCounterToEnum(counter))
+                .setCounterValue(value);
+
+        mApfCounterListBuilder.addApfCounter(apfCounterBuilder);
+    }
+
+    /**
+     * Write the session duration to mStatsBuilder.
+     */
+    public void setApfSessionDurationSeconds(final int durationSeconds) {
+        mStatsBuilder.setApfSessionDurationSeconds(durationSeconds);
+    }
+
+    /**
+     * Write the number of times APF program updated to mStatsBuilder.
+     */
+    public void setNumOfTimesApfProgramUpdated(final int updatedTimes) {
+        mStatsBuilder.setNumOfTimesApfProgramUpdated(updatedTimes);
+    }
+
+    /**
+     * Write the maximum program size to mStatsBuilder.
+     */
+    public void setMaxProgramSize(final int programSize) {
+        mStatsBuilder.setMaxProgramSize(programSize);
+    }
+
+    /**
+     * Write the ApfSessionInfoReported proto into statsd.
+     */
+    public ApfSessionInfoReported statsWrite() {
+        mStatsBuilder.setApfCounterList(mApfCounterListBuilder);
+        final ApfSessionInfoReported stats = mStatsBuilder.build();
+        final byte[] apfCounterList = stats.getApfCounterList().toByteArray();
+        NetworkStackStatsLog.write(NetworkStackStatsLog.APF_SESSION_INFO_REPORTED,
+                stats.getVersion(),
+                stats.getMemorySize(),
+                apfCounterList,
+                stats.getApfSessionDurationSeconds(),
+                stats.getNumOfTimesApfProgramUpdated(),
+                stats.getMaxProgramSize());
+        return stats;
+    }
+
+    /**
+     *  Map ApfCounterTracker.Counter to {@link CounterName}.
+     */
+    @VisibleForTesting
+    public static CounterName apfFilterCounterToEnum(final Counter counter) {
+        switch(counter) {
+            case TOTAL_PACKETS:
+                return CounterName.CN_TOTAL_PACKETS;
+            case PASSED_ARP:
+                return CounterName.CN_PASSED_ARP;
+            case PASSED_DHCP:
+                return CounterName.CN_PASSED_DHCP;
+            case PASSED_IPV4:
+                return CounterName.CN_PASSED_IPV4;
+            case PASSED_IPV6_NON_ICMP:
+                return CounterName.CN_PASSED_IPV6_NON_ICMP;
+            case PASSED_IPV4_UNICAST:
+                return CounterName.CN_PASSED_IPV4_UNICAST;
+            case PASSED_IPV6_ICMP:
+                return CounterName.CN_PASSED_IPV6_ICMP;
+            case PASSED_IPV6_UNICAST_NON_ICMP:
+                return CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP;
+            // PASSED_ARP_NON_IPV4 and PASSED_ARP_UNKNOWN were deprecated in ApfFilter:
+            //     PASSED_ARP_NON_IPV4 ==> DROPPED_ARP_NON_IPV4
+            //     PASSED_ARP_UNKNOWN  ==> DROPPED_ARP_UNKNOWN
+            // They are not supported in the metrics.
+            case PASSED_ARP_NON_IPV4:
+            case PASSED_ARP_UNKNOWN:
+                return CounterName.CN_UNKNOWN;
+            case PASSED_ARP_UNICAST_REPLY:
+                return CounterName.CN_PASSED_ARP_UNICAST_REPLY;
+            case PASSED_NON_IP_UNICAST:
+                return CounterName.CN_PASSED_NON_IP_UNICAST;
+            case PASSED_MDNS:
+                return CounterName.CN_PASSED_MDNS;
+            case DROPPED_ETH_BROADCAST:
+                return CounterName.CN_DROPPED_ETH_BROADCAST;
+            case DROPPED_RA:
+                return CounterName.CN_DROPPED_RA;
+            case DROPPED_GARP_REPLY:
+                return CounterName.CN_DROPPED_GARP_REPLY;
+            case DROPPED_ARP_OTHER_HOST:
+                return CounterName.CN_DROPPED_ARP_OTHER_HOST;
+            case DROPPED_IPV4_L2_BROADCAST:
+                return CounterName.CN_DROPPED_IPV4_L2_BROADCAST;
+            case DROPPED_IPV4_BROADCAST_ADDR:
+                return CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR;
+            case DROPPED_IPV4_BROADCAST_NET:
+                return CounterName.CN_DROPPED_IPV4_BROADCAST_NET;
+            case DROPPED_IPV4_MULTICAST:
+                return CounterName.CN_DROPPED_IPV4_MULTICAST;
+            case DROPPED_IPV6_ROUTER_SOLICITATION:
+                return CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION;
+            case DROPPED_IPV6_MULTICAST_NA:
+                return CounterName.CN_DROPPED_IPV6_MULTICAST_NA;
+            case DROPPED_IPV6_MULTICAST:
+                return CounterName.CN_DROPPED_IPV6_MULTICAST;
+            case DROPPED_IPV6_MULTICAST_PING:
+                return CounterName.CN_DROPPED_IPV6_MULTICAST_PING;
+            case DROPPED_IPV6_NON_ICMP_MULTICAST:
+                return CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST;
+            case DROPPED_802_3_FRAME:
+                return CounterName.CN_DROPPED_802_3_FRAME;
+            case DROPPED_ETHERTYPE_DENYLISTED:
+                return CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED;
+            case DROPPED_ARP_REPLY_SPA_NO_HOST:
+                return CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST;
+            case DROPPED_IPV4_KEEPALIVE_ACK:
+                return CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK;
+            case DROPPED_IPV6_KEEPALIVE_ACK:
+                return CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK;
+            case DROPPED_IPV4_NATT_KEEPALIVE:
+                return CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE;
+            case DROPPED_MDNS:
+                return CounterName.CN_DROPPED_MDNS;
+            case DROPPED_IPV4_TCP_PORT7_UNICAST:
+                // TODO: Not supported yet in the metrics backend.
+                return CounterName.CN_UNKNOWN;
+            case DROPPED_ARP_NON_IPV4:
+                return CounterName.CN_DROPPED_ARP_NON_IPV4;
+            case DROPPED_ARP_UNKNOWN:
+                return CounterName.CN_DROPPED_ARP_UNKNOWN;
+            default:
+                return CounterName.CN_UNKNOWN;
+        }
+    }
+}
diff --git a/src/com/android/networkstack/metrics/IpClientRaInfoMetrics.java b/src/com/android/networkstack/metrics/IpClientRaInfoMetrics.java
new file mode 100644
index 0000000..ba310ed
--- /dev/null
+++ b/src/com/android/networkstack/metrics/IpClientRaInfoMetrics.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 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;
+
+/**
+ * Class to record the network stack IpClientRaInfo metrics into statsd.
+ *
+ * This class is not thread-safe, and should always be accessed from the same thread.
+ *
+ * @hide
+ */
+public class IpClientRaInfoMetrics {
+    private final IpClientRaInfoReported.Builder mStatsBuilder =
+            IpClientRaInfoReported.newBuilder();
+
+    /**
+     * Write the maximum number of distinct RAs into mStatsBuilder.
+     */
+    public void setMaxNumberOfDistinctRas(final int maxNum) {
+        mStatsBuilder.setMaxNumberOfDistinctRas(maxNum);
+    }
+
+    /**
+     * Write the number of zero lifetime RAs into mStatsBuilder.
+     */
+    public void setNumberOfZeroLifetimeRas(final int number) {
+        mStatsBuilder.setNumberOfZeroLifetimeRas(number);
+    }
+
+    /**
+     * Write the number of parsing error RAs into mStatsBuilder.
+     */
+    public void setNumberOfParsingErrorRas(final int number) {
+        mStatsBuilder.setNumberOfParsingErrorRas(number);
+    }
+
+    /**
+     * Write the lowest router lifetime into mStatsBuilder.
+     */
+    public void setLowestRouterLifetimeSeconds(final int lifetime) {
+        mStatsBuilder.setLowestRouterLifetimeSeconds(lifetime);
+    }
+
+    /**
+     * Write the lowest valid lifetime of PIOs into mStatsBuilder.
+     */
+    public void setLowestPioValidLifetimeSeconds(final long lifetime) {
+        mStatsBuilder.setLowestPioValidLifetimeSeconds(lifetime);
+    }
+
+    /**
+     * Write the lowest route lifetime of RIOs into mStatsBuilder.
+     */
+    public void setLowestRioRouteLifetimeSeconds(final long lifetime) {
+        mStatsBuilder.setLowestRioRouteLifetimeSeconds(lifetime);
+    }
+
+    /**
+     * Write the lowest lifetime of RDNSSs into mStatsBuilder.
+     */
+    public void setLowestRdnssLifetimeSeconds(final long lifetime) {
+        mStatsBuilder.setLowestRdnssLifetimeSeconds(lifetime);
+    }
+
+    /**
+     * Write the IpClientRaInfoReported proto into statsd.
+     */
+    public IpClientRaInfoReported statsWrite() {
+        final IpClientRaInfoReported stats = mStatsBuilder.build();
+        NetworkStackStatsLog.write(NetworkStackStatsLog.IP_CLIENT_RA_INFO_REPORTED,
+                stats.getMaxNumberOfDistinctRas(),
+                stats.getNumberOfZeroLifetimeRas(),
+                stats.getNumberOfParsingErrorRas(),
+                stats.getLowestRouterLifetimeSeconds(),
+                stats.getLowestPioValidLifetimeSeconds(),
+                stats.getLowestRioRouteLifetimeSeconds(),
+                stats.getLowestRdnssLifetimeSeconds());
+        return stats;
+    }
+}
diff --git a/src/com/android/networkstack/metrics/stats.proto b/src/com/android/networkstack/metrics/stats.proto
index bd5e62b..06419f9 100644
--- a/src/com/android/networkstack/metrics/stats.proto
+++ b/src/com/android/networkstack/metrics/stats.proto
@@ -204,17 +204,17 @@
     // The number of parsing error for RAs (Router Advertisements).
     optional int32 number_of_parsing_error_ras = 3;
 
-    // The lowest router lifetime in seconds.
+    // The lowest router lifetime in seconds, excluding 0.
     optional int32 lowest_router_lifetime_seconds = 4;
 
-    // The lowest valid lifetime of PIO (Prefix Information Option) in seconds.
-    optional int32 lowest_pio_valid_lifetime_seconds = 5;
+    // The lowest valid lifetime of PIO (Prefix Information Option) in seconds, excluding 0.
+    optional int64 lowest_pio_valid_lifetime_seconds = 5;
 
-    // The lowest route lifetime of RIO (Route Information Option) in seconds.
-    optional int32 lowest_rio_route_lifetime_seconds = 6;
+    // The lowest route lifetime of RIO (Route Information Option) in seconds, excluding 0.
+    optional int64 lowest_rio_route_lifetime_seconds = 6;
 
-    // The lowest lifetime of RDNSS (Recursive DNS Server Option) in seconds.
-    optional int32 lowest_rdnss_lifetime_seconds = 7;
+    // The lowest lifetime of RDNSS (Recursive DNS Server Option) in seconds, excluding 0.
+    optional int64 lowest_rdnss_lifetime_seconds = 7;
 }
 
 /**
@@ -225,7 +225,7 @@
     optional .android.stats.connectivity.CounterName counter_name = 1;
 
     // The value of APF counter.
-    optional int32 counter_value = 2;
+    optional int64 counter_value = 2;
 }
 
 
@@ -249,8 +249,8 @@
     // The values of all APF counters.
     optional ApfCounterList apf_counter_list = 3;
 
-    // The duration of ip client in milliseconds.
-    optional int32 ip_client_session_duration_ms = 4;
+    // The duration of APF session in seconds.
+    optional int32 apf_session_duration_seconds = 4;
 
     // Number of times APF program updated.
     optional int32 num_of_times_apf_program_updated = 5;
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 2dbf86e..829d0c6 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -197,13 +197,6 @@
     public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";
 
     /**
-     * Experiment flag to send multicast NS from the global IPv6 GUA to the solicited-node
-     * multicast address based on the default router's IPv6 link-local address, which helps
-     * flush the first-hop routers' neighbor cache entry for the global IPv6 GUA.
-     */
-    public static final String IPCLIENT_MULTICAST_NS_VERSION = "ipclient_multicast_ns_version";
-
-    /**
      * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for
      * all assigned IPv4 and IPv6 GUAs after completing L2 roaming.
      */
@@ -219,13 +212,6 @@
             "ipclient_accept_ipv6_link_local_dns_version";
 
     /**
-     * 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";
-
-    /**
      * Experiment flag to attempt to ignore the on-link IPv6 DNS server which fails to respond to
      * address resolution.
      */
@@ -250,6 +236,10 @@
      */
     public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version";
     /**
+     * Experiment flag to enable the feature of polling counters in Apf.
+     */
+    public static final String APF_POLLING_COUNTERS_VERSION = "apf_polling_counters_version";
+    /**
      * Experiment flag to enable the feature of ignoring any individual RA section with lifetime
      * below accept_ra_min_lft sysctl.
      */
@@ -278,7 +268,7 @@
     public static final String SKIP_TCP_POLL_IN_LIGHT_DOZE = "skip_tcp_poll_in_light_doze_mode";
 
     /**
-     * Kill switch flag to disable the feature of re-evaluate when network resumes.
+     * Experiment flag to enable the feature of re-evaluate when network resumes.
      */
     public static final String REEVALUATE_WHEN_RESUME = "reevaluate_when_resume";
 
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 8c10138..9303f95 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -630,8 +630,8 @@
         mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled();
         mMetricsEnabled = deps.isFeatureNotChickenedOut(context,
                 NetworkStackUtils.VALIDATION_METRICS_VERSION);
-        mReevaluateWhenResumeEnabled = deps.isFeatureNotChickenedOut(context,
-                NetworkStackUtils.REEVALUATE_WHEN_RESUME);
+        mReevaluateWhenResumeEnabled = deps.isFeatureEnabled(
+                context, NetworkStackUtils.REEVALUATE_WHEN_RESUME);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 8300c73..ec058d7 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -68,6 +68,9 @@
     static_libs: [
         "NetworkStackApiStableLib",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack integration tests.
@@ -84,6 +87,9 @@
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack next integration tests.
@@ -107,6 +113,9 @@
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack integration root tests.
@@ -124,12 +133,18 @@
         "NetworkStackApiStableLib",
     ],
     platform_apis: true,
-    test_suites: ["general-tests", "mts-networking"],
+    test_suites: [
+        "general-tests",
+        "mts-networking",
+    ],
     compile_multilib: "both",
     manifest: "AndroidManifest_root.xml",
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Special version of the network stack tests that includes all tests necessary for code coverage
@@ -138,7 +153,10 @@
     name: "NetworkStackCoverageTests",
     certificate: "networkstack",
     platform_apis: true,
-    test_suites: ["device-tests", "mts-networking"],
+    test_suites: [
+        "device-tests",
+        "mts-networking",
+    ],
     test_config: "AndroidTest_Coverage.xml",
     defaults: [
         "NetworkStackReleaseTargetSdk",
@@ -154,4 +172,7 @@
     compile_multilib: "both",
     manifest: "AndroidManifest_coverage.xml",
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index d239379..2f1f7d1 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -27,7 +27,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.RouteInfo.RTN_UNICAST;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.dhcp.DhcpClient.EXPIRED_LEASE;
 import static android.net.dhcp.DhcpPacket.DHCP_BOOTREQUEST;
 import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
@@ -42,7 +41,9 @@
 import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
 import static android.net.ip.IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT;
 import static android.net.ip.IpClient.CONFIG_ACCEPT_RA_MIN_LFT;
+import static android.net.ip.IpClient.CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS;
 import static android.net.ip.IpClient.DEFAULT_ACCEPT_RA_MIN_LFT;
+import static android.net.ip.IpClient.DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS;
 import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX;
 import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
 import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM;
@@ -121,8 +122,11 @@
 import android.app.AlarmManager.OnAlarmListener;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.DhcpResultsParcelable;
@@ -190,6 +194,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.Ipv6Utils;
@@ -226,7 +231,6 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.HandlerUtils;
-import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TapPacketReader;
 import com.android.testutils.TestableNetworkAgent;
 import com.android.testutils.TestableNetworkCallback;
@@ -295,13 +299,20 @@
     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;
+    private static final int TEST_DEVICE_OWNER_APP_UID = 14242;
+    private static final String TEST_DEVICE_OWNER_APP_PACKAGE = "com.example.deviceowner";
     protected static final String TEST_L2KEY = "some l2key";
 
     // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants.
     private static final int IFA_F_STABLE_PRIVACY = 0x800;
+    // To fix below AndroidLint warning:
+    // [InlinedApi] Field requires version 3 of the U Extensions SDK (current min is 0).
+    private static final int RTN_UNREACHABLE =
+            SdkLevel.isAtLeastT() ? RouteInfo.RTN_UNREACHABLE : 7;
 
     protected static final long TEST_TIMEOUT_MS = 2_000L;
     private static final long TEST_WAIT_ENOBUFS_TIMEOUT_MS = 30_000L;
+    private static final long TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS = 15_000L;
 
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -351,7 +362,8 @@
     @Mock protected NetworkStackIpMemoryStore mIpMemoryStore;
     @Mock private NetworkQuirkMetrics.Dependencies mNetworkQuirkMetricsDeps;
     @Mock private IpReachabilityMonitorMetrics mIpReachabilityMonitorMetrics;
-
+    @Mock private DevicePolicyManager mDevicePolicyManager;
+    @Mock private PackageManager mPackageManager;
     @Spy private INetd mNetd;
     private NetworkObserverRegistry mNetworkObserverRegistry;
 
@@ -768,6 +780,8 @@
             enableRealAlarm("DhcpClient." + mIfaceName + ".KICK");
             // Enable alarm for IPv6 autoconf via SLAAC in IpClient.
             enableRealAlarm("IpClient." + mIfaceName + ".EVENT_IPV6_AUTOCONF_TIMEOUT");
+            // Enable packet retransmit alarm in Dhcp6Client.
+            enableRealAlarm("Dhcp6Client." + mIfaceName + ".KICK");
         }
 
         mIIpClient = makeIIpClient(mIfaceName, mCb);
@@ -789,8 +803,11 @@
         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.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarm);
+        when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mCm);
+        when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE))
+                .thenReturn(mDevicePolicyManager);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getInteger(eq(R.integer.config_nud_postroaming_solicit_num))).thenReturn(5);
         when(mResources.getInteger(eq(R.integer.config_nud_postroaming_solicit_interval)))
@@ -810,6 +827,11 @@
         // liftime.
         when(mCm.shouldAvoidBadWifi()).thenReturn(true);
 
+        when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn(
+                new ComponentName(TEST_DEVICE_OWNER_APP_PACKAGE, "com.example.SomeClass"));
+        when(mPackageManager.getPackagesForUid(TEST_DEVICE_OWNER_APP_UID)).thenReturn(
+                new String[] { TEST_DEVICE_OWNER_APP_PACKAGE });
+
         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);
@@ -829,6 +851,10 @@
         // Set the minimal RA lifetime value, any RA section with liftime below this value will be
         // ignored.
         mDependencies.setDeviceConfigProperty(CONFIG_ACCEPT_RA_MIN_LFT, DEFAULT_ACCEPT_RA_MIN_LFT);
+
+        // Set the polling interval to update APF data snapshot.
+        mDependencies.setDeviceConfigProperty(CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
+                DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS);
     }
 
     private void awaitIpClientShutdown() throws Exception {
@@ -1105,43 +1131,47 @@
 
     private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
             final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
-            final String captivePortalUrl, final Integer ipv6OnlyWaitTime) {
+            final String captivePortalUrl, final Integer ipv6OnlyWaitTime,
+            final String domainName, final List<String> domainSearchList) {
         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);
+                SERVER_ADDR /* dhcpServerIdentifier */, domainName, HOSTNAME,
+                false /* metered */, mtu, captivePortalUrl, ipv6OnlyWaitTime, domainSearchList);
     }
 
     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 */);
+                null /* ipv6OnlyWaitTime */, null /* domainName */, null /* domainSearchList */);
     }
 
     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) {
+            final Integer ipv6OnlyWaitTime, final String domainName,
+            final List<String> domainSearchList) {
         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);
+                SERVER_ADDR /* dhcpServerIdentifier */, domainName, HOSTNAME,
+                false /* metered */, mtu, rapidCommit, captivePortalApiUrl, ipv6OnlyWaitTime,
+                domainSearchList);
     }
 
     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 */);
+                captivePortalApiUrl, null /* ipv6OnlyWaitTime */, null /* domainName */,
+                null /* domainSearchList */);
     }
 
     private static ByteBuffer buildDhcpNakPacket(final DhcpPacket packet, final String message) {
@@ -1297,6 +1327,15 @@
     private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
             final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
             final String captivePortalApiUrl) throws Exception {
+        return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck,
+                mtu, captivePortalApiUrl, null /* ipv6OnlyWaitTime */,
+                null /* domainName */, null /* domainSearchList */);
+    }
+
+    private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
+            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
+            final String captivePortalApiUrl, final Integer ipv6OnlyWaitTime,
+            final String domainName, final List<String> domainSearchList) throws Exception {
         final List<DhcpPacket> packetList = new ArrayList<>();
         DhcpPacket packet;
         while ((packet = getNextDhcpPacket()) != null) {
@@ -1304,15 +1343,18 @@
             if (packet instanceof DhcpDiscoverPacket) {
                 if (shouldReplyRapidCommitAck) {
                     mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec,
-                              (short) mtu, true /* rapidCommit */, captivePortalApiUrl));
+                              (short) mtu, true /* rapidCommit */, captivePortalApiUrl,
+                              ipv6OnlyWaitTime, domainName, domainSearchList));
                 } else {
                     mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR,
-                            leaseTimeSec, (short) mtu, captivePortalApiUrl));
+                            leaseTimeSec, (short) mtu, captivePortalApiUrl, ipv6OnlyWaitTime,
+                            domainName, domainSearchList));
                 }
             } else if (packet instanceof DhcpRequestPacket) {
                 final ByteBuffer byteBuffer = isSuccessLease
                         ? buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec, (short) mtu,
-                                false /* rapidCommit */, captivePortalApiUrl)
+                                false /* rapidCommit */, captivePortalApiUrl, ipv6OnlyWaitTime,
+                                domainName, domainSearchList)
                         : buildDhcpNakPacket(packet, "duplicated request IP address");
                 mPacketReader.sendResponse(byteBuffer);
             } else {
@@ -3308,7 +3350,7 @@
         // 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));
+                ipv6OnlyWaitTime, null /* domainName */, null /* domainSearchList */));
     }
 
     private void doDiscoverIPv6OnlyPreferredOptionTest(final int optionSecs,
@@ -3378,7 +3420,8 @@
         // 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));
+                (short) TEST_DEFAULT_MTU, null /* captivePortalUrl */, TEST_IPV6_ONLY_WAIT_S,
+                null /* domainName */, null /* domainSearchList */));
 
         final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 1800,
                 mDependencies.mDhcpClient.getHandler());
@@ -3427,7 +3470,8 @@
         // 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));
+                null /* captivePortalUrl */, ipv6OnlyWaitTime, null /* domainName */,
+                null /* domainSearchList */));
 
         if (ipv6OnlyWaitTime != null) {
             expectAlarmSet(null /* inOrder */, "TIMEOUT", expectedWaitSecs,
@@ -4096,12 +4140,6 @@
         return ns;
     }
 
-    // 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 assertNotifyNeighborLost(Inet6Address targetIp, NudEventType eventType)
             throws Exception {
         // For root test suite, rely on the IIpClient aidl interface version constant defined in
@@ -4134,8 +4172,7 @@
         verify(mCb, never()).onReachabilityLost(any());
     }
 
-    private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled)
-            throws Exception {
+    private void prepareIpReachabilityMonitorTest() throws Exception {
         final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
@@ -4144,8 +4181,6 @@
                 .withDisplayName(TEST_DEFAULT_SSID)
                 .withoutIPv4()
                 .build();
-        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
-                isMulticastResolicitEnabled);
         startIpClientProvisioning(config);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
         doIpv6OnlyProvisioning();
@@ -4159,11 +4194,15 @@
 
         final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
         final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
-        assertEquals(expectedNudSolicitNum, nsList.size());
-        for (NeighborSolicitation ns : nsList) {
+        int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
+        assertEquals(expectedSize, nsList.size());
+        for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
             assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                     ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
         }
+        for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) {
+            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
+        }
     }
 
     @Test
@@ -4198,43 +4237,10 @@
         assertNeverNotifyNeighborLost();
     }
 
-    private void runIpReachabilityMonitorMcastResolicitProbeFailedTest() throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
-
-        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
-        final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
-        int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
-        assertEquals(expectedSize, nsList.size());
-        for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
-            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
-                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
-        }
-        for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) {
-            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
-        }
-    }
-
-    @Test
-    public void testIpReachabilityMonitor_mcastResolicitProbeFailed() throws Exception {
-        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
-        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
-                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
-    }
-
-    @Test @SignatureRequiredTest(reason = "requires mock callback object")
-    public void testIpReachabilityMonitor_mcastResolicitProbeFailed_legacyCallback()
-            throws Exception {
-        when(mCb.getInterfaceVersion()).thenReturn(12 /* assign an older interface aidl version */);
-
-        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any());
-        verify(mCb, never()).onReachabilityFailure(any());
-    }
-
     @Test
     public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithSameLinkLayerAddress()
             throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+        prepareIpReachabilityMonitorTest();
 
         final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
                 ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4251,7 +4257,7 @@
     @Test
     public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithDiffLinkLayerAddress()
             throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+        prepareIpReachabilityMonitorTest();
 
         final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
                 ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4623,9 +4629,6 @@
                 .withoutIPv4()
                 .build();
 
-        setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION,
-                true /* isUnsolicitedNsEnabled */);
-        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION));
         startIpClientProvisioning(config);
 
         doIpv6OnlyProvisioning();
@@ -5105,9 +5108,8 @@
     }
 
     @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
-    public void testDhcp6Pd_prefixMismatchOnRenew() throws Exception {
+    public void testDhcp6Pd_prefixMismatchOnRenew_newPrefix() throws Exception {
         prepareDhcp6PdRenewTest();
 
         final InOrder inOrder = inOrder(mAlarm);
@@ -5120,17 +5122,396 @@
         Dhcp6Packet packet = getNextDhcp6Packet();
         assertTrue(packet instanceof Dhcp6RenewPacket);
 
-        // Reply with a different prefix with requested one, per RFC8415#section-18.2.10.1
+        // Reply with a new prefix apart of the requested one, per RFC8415#section-18.2.10.1
         // any new prefix should be added.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 5000 /* preferred */,
+                6000 /* valid */);
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, Arrays.asList(ipo, ipo1));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        verify(mCb, never()).onProvisioningFailure(any());
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.isIpv6Provisioned()
+                        && hasIpv6AddressPrefixedWith(x, prefix)
+                        && hasIpv6AddressPrefixedWith(x, prefix1)
+                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
+                        // IPv6 link-local, four global delegated IPv6 addresses
+                        && x.getLinkAddresses().size() == 5
+        ));
+    }
+
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_prefixMismatchOnRenew_requestedPrefixAbsent() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply with a new prefix but the requested one is absent, per RFC8415#section-18.2.10.1
+        // the new prefix should be added and the absent prefix will expire in nature.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
         final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
         final IaPrefixOption ipo = buildIaPrefixOption(prefix1, 4500 /* preferred */,
                 7200 /* valid */);
         final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
-                4500 /* t2 */, Collections.singletonList(ipo));
+                4500 /* t2 */, Arrays.asList(ipo));
         final ByteBuffer iapd = pd.build();
         mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                 (Inet6Address) mClientIpAddress, false /* rapidCommit */));
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
+        verify(mCb, never()).onProvisioningFailure(any());
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.isIpv6Provisioned()
+                        && hasIpv6AddressPrefixedWith(x, prefix)
+                        && hasIpv6AddressPrefixedWith(x, prefix1)
+                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
+                        // IPv6 link-local, four global delegated IPv6 addresses
+                        && x.getLinkAddresses().size() == 5
+        ));
+    }
+
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_prefixMismatchOnRenew_allPrefixesAbsent() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        clearInvocations(mCb);
+
+        // Reply with IA_PD but IA_Prefix is absent, client should still stay at the RenewState
+        // and restransmit the Renew message, that should not result in any LinkProperties update.
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, new ArrayList<IaPrefixOption>(0));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        verify(mCb, never()).onLinkPropertiesChange(any());
+    }
+
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_renewInvalidPrefixes_zeroPreferredAndValidLifetime() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply with the requested prefix with preferred/valid lifetime of 0.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 0 /* preferred */,
+                0 /* valid */);
+        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 5000 /* preferred */,
+                6000 /* valid */);
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, Arrays.asList(ipo, ipo1));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        verify(mCb, never()).onProvisioningFailure(any());
+        // IPv6 addresses derived from prefix with 0 preferred/valid lifetime should be deleted.
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.isIpv6Provisioned()
+                        && !hasIpv6AddressPrefixedWith(x, prefix)
+                        && hasIpv6AddressPrefixedWith(x, prefix1)
+                        && !hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
+                        // IPv6 link-local, two global delegated IPv6 addresses with prefix1
+                        && x.getLinkAddresses().size() == 3
+        ));
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+        final List<IaPrefixOption> renewIpos = packet.getPrefixDelegation().ipos;
+        assertEquals(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64 with 0
+                                           // preferred/valid lifetime
+        assertEquals(prefix1, renewIpos.get(0).getIpPrefix());
+    }
+
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_renewInvalidPrefixes_theSameT1T2ValidLifetime() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        clearInvocations(mCb);
+
+        // Reply with the requested prefix with the same t1/t2/lifetime.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
+                3600 /* valid */);
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                3600 /* t2 */, Collections.singletonList(ipo));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        // The prefix doesn't change only the lifetime is updated, therefore, LinkProperties update
+        // isn't expected.
+        verify(mCb, never()).onProvisioningFailure(any());
+        verify(mCb, never()).onLinkPropertiesChange(any());
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
+    @Test
+    public void testDhcp6Pd_multipleIaPrefixOptions() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        final IpPrefix prefix1 = new IpPrefix("2001:db8:1::/64");
+        final IpPrefix prefix2 = new IpPrefix("2400:db8:100::/64");
+        final IpPrefix prefix3 = new IpPrefix("fd7c:9df8:7f39:dc89::/64");
+        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 4500 /* preferred */,
+                7200 /* valid */);
+        final IaPrefixOption ipo2 = buildIaPrefixOption(prefix2, 5600 /* preferred */,
+                6000 /* valid */);
+        final IaPrefixOption ipo3 = buildIaPrefixOption(prefix3, 7200 /* preferred */,
+                14400 /* valid */);
+        prepareDhcp6PdTest();
+        handleDhcp6Packets(Arrays.asList(ipo1, ipo2, ipo3), 3600 /* t1 */, 4500 /* t2 */,
+                true /* shouldReplyRapidCommit */);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
+        LinkProperties lp = captor.getValue();
+
+        // Sometimes privacy address or route may appear later along with onLinkPropertiesChange
+        // callback, in this case we wait a bit longer to see all of these properties appeared and
+        // then verify if they are what we are looking for.
+        if (lp.getLinkAddresses().size() < 5 || lp.getRoutes().size() < 4) {
+            final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(x -> {
+                if (!x.isIpv6Provisioned()) return false;
+                if (x.getLinkAddresses().size() != 5) return false;
+                if (x.getRoutes().size() != 4) return false;
+                lpFuture.complete(x);
+                return true;
+            }));
+            lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+        assertNotNull(lp);
+        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix1));
+        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix2));
+        assertFalse(hasIpv6AddressPrefixedWith(lp, prefix3));
+        assertTrue(hasRouteTo(lp, prefix1.toString(), RTN_UNREACHABLE));
+        assertTrue(hasRouteTo(lp, prefix2.toString(), RTN_UNREACHABLE));
+        assertFalse(hasRouteTo(lp, prefix3.toString(), RTN_UNREACHABLE));
+    }
+
+    private void runDhcp6PacketWithNoPrefixAvailStatusCodeTest(boolean shouldReplyWithAdvertise)
+            throws Exception {
+        prepareDhcp6PdTest();
+        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */,
+                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        final ByteBuffer iapd = pd.build();
+        if (shouldReplyWithAdvertise) {
+            mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
+                    (Inet6Address) mClientIpAddress));
+        } else {
+            mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                    (Inet6Address) mClientIpAddress, true /* rapidCommit */));
+        }
+
+        // Check if client will ignore Advertise or Reply for Rapid Commit Solicit and
+        // retransmit Solicit.
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+    }
+
+    @Test
+    public void testDhcp6AdvertiseWithNoPrefixAvailStatusCode() throws Exception {
+        // Advertise
+        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(true /* shouldReplyWithAdvertise */);
+    }
+
+    @Test
+    public void testDhcp6ReplyForRapidCommitSolicitWithNoPrefixAvailStatusCode() throws Exception {
+        // Reply
+        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(false /* shouldReplyWithAdvertise */);
+    }
+
+    @Test
+    public void testDhcp6ReplyForRequestWithNoPrefixAvailStatusCode() throws Exception {
+        prepareDhcp6PdTest();
+        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress));
+
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6RequestPacket);
+
+        // Reply for Request with NoPrefixAvail status code. Not sure if this is reasonable in
+        // practice, but Server can do everything it wants.
+        pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */,
+                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        // Check if client will ignore Reply for Request with NoPrefixAvail status code, and
+        // rollback to SolicitState.
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    public void testDhcp6ReplyForRenewWithNoPrefixAvailStatusCode() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply with normal IA_PD.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        // Trigger another Renew message.
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply for Renew with NoPrefixAvail status code, check if client will retransmit the
+        // Renew message.
+        pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */, 4500 /* t2 */,
+                new ArrayList<IaPrefixOption>(0) /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    public void testDhcp6ReplyForRebindWithNoPrefixAvailStatusCode() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+        final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 4500, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        handler.post(() -> rebindAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RebindPacket);
+
+        // Reply with normal IA_PD.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        // Trigger another Rebind message.
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        handler.post(() -> rebindAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RebindPacket);
+
+        // Reply for Rebind with NoPrefixAvail status code, check if client will retransmit the
+        // Rebind message.
+        pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, new ArrayList<IaPrefixOption>(0) /* ipos */,
+                Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
+        assertTrue(packet instanceof Dhcp6RebindPacket);
     }
 
     @Test
@@ -5190,4 +5571,51 @@
                 mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra_defrtr"));
         assertEquals(1, acceptRaDefRtr);
     }
+    private void runDhcpDomainSearchListOptionTest(final String domainName,
+            final List<String> domainSearchList, final String expectedDomain) throws Exception {
+        when(mResources.getBoolean(R.bool.config_dhcp_client_domain_search_list)).thenReturn(true);
+        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv6()
+                .withCreatorUid(TEST_DEVICE_OWNER_APP_UID)
+                .build();
+
+        startIpClientProvisioning(cfg);
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
+                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
+                domainName, domainSearchList);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        assertEquals(expectedDomain, lp.getDomains());
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
+    public void testDhcpDomainSearchListOption() throws Exception {
+        final String domainName = "google.com";
+        final List<String> searchList = List.of("suffix1.google.com", "suffix2.google.com");
+        final String expectedDomain = "google.com suffix1.google.com suffix2.google.com";
+        runDhcpDomainSearchListOptionTest(domainName, searchList, expectedDomain);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
+    public void testDhcpDomainSearchListOption_invalidSuffix() throws Exception {
+        final String domainName = "google.com";
+        final List<String> searchList = List.of("google com");
+        runDhcpDomainSearchListOptionTest(domainName, searchList, domainName /* expectedDomain */);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
+    public void testDhcpDomainSearchListOption_onlySearchList() throws Exception {
+        final List<String> searchList = List.of("google.com", "example.com");
+        final String expectedDomain = "google.com example.com";
+        runDhcpDomainSearchListOptionTest(null /* domainName */, searchList,
+                expectedDomain);
+    }
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 464e4a1..ea29714 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -21,7 +21,10 @@
 java_defaults {
     name: "NetworkStackTestsDefaults",
     platform_apis: true,
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     resource_dirs: ["res"],
     static_libs: [
         "androidx.test.ext.junit",
@@ -39,7 +42,7 @@
     ],
     defaults: [
         "framework-connectivity-test-defaults",
-        "libnetworkstackutilsjni_deps"
+        "libnetworkstackutilsjni_deps",
     ],
     jni_libs: [
         // For mockito extended
@@ -66,6 +69,9 @@
     static_libs: ["NetworkStackApiCurrentLib"],
     compile_multilib: "both", // Workaround for b/147785146 for mainline-presubmit
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Library containing the unit tests. This is used by the coverage test target to pull in the
@@ -76,18 +82,24 @@
     min_sdk_version: "30",
     defaults: ["NetworkStackTestsDefaults"],
     static_libs: ["NetworkStackApiStableLib"],
-    lint: { test: true },
+    lint: {
+        test: true,
+        baseline_filename: "lint-baseline.xml",
+    },
     visibility: [
         "//packages/modules/NetworkStack/tests/integration",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-    ]
+    ],
 }
 
 android_test {
     name: "NetworkStackTests",
     min_sdk_version: "30",
-    test_suites: ["general-tests", "mts"],
+    test_suites: [
+        "general-tests",
+        "mts",
+    ],
     defaults: [
         "NetworkStackTestsDefaults",
         "connectivity-mainline-presubmit-java-defaults",
@@ -95,6 +107,9 @@
     static_libs: ["NetworkStackApiStableLib"],
     compile_multilib: "both",
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Additional dependencies of libnetworkstackutilsjni that are not provided by the system when
diff --git a/tests/unit/jni/apf_jni.cpp b/tests/unit/jni/apf_jni.cpp
index 84c5c1a..8e14b3a 100644
--- a/tests/unit/jni/apf_jni.cpp
+++ b/tests/unit/jni/apf_jni.cpp
@@ -40,7 +40,7 @@
     return accept_packet(program, program_len, ram_len, packet, packet_len,
                          filter_age);
   } else {
-    return apf_run(program, program_len, ram_len, packet, packet_len,
+    return apf_run(nullptr, program, program_len, ram_len, packet, packet_len,
                          filter_age << 14);
   }
 }
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 6a54e1c..4e1187b 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -16,6 +16,7 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfGenerator.APF_VERSION_4;
 import static android.net.apf.ApfGenerator.Register.R0;
 import static android.net.apf.ApfGenerator.Register.R1;
 import static android.net.apf.ApfJniUtils.compareBpfApf;
@@ -44,6 +45,10 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.BroadcastReceiver;
@@ -56,14 +61,20 @@
 import android.net.MacAddress;
 import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfTestUtils.MockIpClientCallback;
 import android.net.apf.ApfTestUtils.TestApfFilter;
+import android.net.apf.ApfTestUtils.TestLegacyApfFilter;
+import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
 import android.os.PowerManager;
+import android.stats.connectivity.NetworkQuirkEvent;
 import android.system.ErrnoException;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
@@ -75,6 +86,9 @@
 import com.android.net.module.util.Inet4AddressUtils;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.PacketBuilder;
+import com.android.networkstack.metrics.ApfSessionInfoMetrics;
+import com.android.networkstack.metrics.IpClientRaInfoMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
 import com.android.server.networkstack.tests.R;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -106,6 +120,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 
 /**
@@ -133,10 +148,17 @@
     @Mock
     private ApfFilter.Dependencies mDependencies;
     @Mock private PowerManager mPowerManager;
+    @Mock private IpConnectivityLog mIpConnectivityLog;
+    @Mock private NetworkQuirkMetrics mNetworkQuirkMetrics;
+    @Mock private ApfSessionInfoMetrics mApfSessionInfoMetrics;
+    @Mock private IpClientRaInfoMetrics mIpClientRaInfoMetrics;
+    @Mock private ApfFilter.Clock mClock;
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class);
+        doReturn(mApfSessionInfoMetrics).when(mDependencies).getApfSessionInfoMetrics();
+        doReturn(mIpClientRaInfoMetrics).when(mDependencies).getIpClientRaInfoMetrics();
     }
 
     private static final String TAG = "ApfTest";
@@ -151,6 +173,7 @@
     private static final boolean ALLOW_802_3_FRAMES = false;
 
     private static final int MIN_RDNSS_LIFETIME_SEC = 0;
+    private static final int MIN_METRICS_SESSION_DURATIONS_MS = 300_000;
 
     // Constants for opcode encoding
     private static final byte LI_OP   = (byte)(13 << 3);
@@ -170,6 +193,7 @@
         config.ethTypeBlackList = new int[0];
         config.minRdnssLifetimeSec = MIN_RDNSS_LIFETIME_SEC;
         config.minRdnssLifetimeSec = 67;
+        config.minMetricsSessionDurationMs = MIN_METRICS_SESSION_DURATIONS_MS;
         return config;
     }
 
@@ -608,7 +632,7 @@
         // Test jump if bytes not equal.
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         program = gen.generate();
         assertEquals(6, program.length);
         assertEquals((13 << 3) | (1 << 1) | 0, program[0]);
@@ -620,20 +644,20 @@
         assertDrop(program, new byte[MIN_PKT_SIZE], 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0};
         assertPass(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         assertDrop(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 30, 4, 5}, gen.DROP_LABEL);
         byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0};
         assertDrop(gen, packet12345, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 3, 4, 5}, gen.DROP_LABEL);
         assertPass(gen, packet12345, 0);
     }
 
@@ -724,23 +748,23 @@
         ApfGenerator gen;
 
         // Load data with no offset: lddw R0, [0 + r1]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadData(R0, 0);
         assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate());
 
         // Store data with 8bit negative offset: lddw r0, [-42 + r1]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addStoreData(R0, -42);
         assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate());
 
         // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addStoreData(R1, -0x1122);
         assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadData(R1, 0xDEADBEEF);
         assertProgramEquals(
                 new byte[]{LDDW_OP | SIZE32 | R1_REG,
@@ -758,12 +782,12 @@
         byte[] expected_data = data.clone();
 
         // No memory access instructions: should leave the data segment untouched.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Expect value 0x87654321 to be stored starting from address -11 from the end of the
         // data buffer, in big-endian order.
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 0x87654321);
         gen.addLoadImmediate(R1, -5);
         gen.addStoreData(R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
@@ -780,7 +804,7 @@
     @Test
     public void testApfDataRead() throws IllegalInstructionException, Exception {
         // Program that DROPs if address 10 (-6) contains 0x87654321.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R1, 1000);
         gen.addLoadData(R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
         gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL);
@@ -809,7 +833,7 @@
      */
     @Test
     public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception {
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R1, -22);
         gen.addLoadData(R0, 0);  // Load from address 32 -22 + 0 = 10
         gen.addAdd(0x78453412);  // 87654321 + 78453412 = FFAA7733
@@ -836,35 +860,35 @@
         byte[] expected_data = data;
 
         // Program that DROPs unconditionally. This is our the baseline.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 3);
         gen.addLoadData(R1, 7);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // Same program as before, but this time we're trying to load past the end of the data.
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, 15);  // 20 + 15 > 32
         gen.addJump(gen.DROP_LABEL);  // Not reached.
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Subtracting an immediate should work...
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -4);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...and underflowing simply wraps around to the end of the buffer...
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -30);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...but doesn't allow accesses before the start of the buffer
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -1000);
         gen.addJump(gen.DROP_LABEL);  // Not reached.
@@ -907,10 +931,11 @@
         config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES;
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         apfFilter.setLinkProperties(lp);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-        byte[] data = new byte[ApfFilter.Counter.totalSize()];
+        byte[] data = new byte[Counter.totalSize()];
         final boolean result;
 
         result = dropsAllPackets(mApfVersion, program, data, pcapFilename);
@@ -1083,7 +1108,8 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         apfFilter.setLinkProperties(lp);
 
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
@@ -1135,7 +1161,8 @@
     public void testApfFilterIPv6() throws Exception {
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty IPv6 packet is passed
@@ -1390,7 +1417,8 @@
         lp.addLinkAddress(link);
 
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         apfFilter.setLinkProperties(lp);
 
         // Construct IPv4 mDNS packet
@@ -1627,7 +1655,8 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         apfFilter.setLinkProperties(lp);
 
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
@@ -1688,7 +1717,7 @@
         apfFilter.shutdown();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics);
         apfFilter.setLinkProperties(lp);
         program = ipClientCallback.assertProgramUpdateAndGet();
         assertDrop(program, mcastv4packet.array());
@@ -1721,7 +1750,7 @@
         final ApfConfiguration configuration = getDefaultConfig();
         configuration.shouldHandleLightDoze = false;
         final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
-                configuration, mDependencies);
+                configuration, mNetworkQuirkMetrics, mDependencies);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
@@ -1736,7 +1765,7 @@
         final ApfConfiguration configuration = getDefaultConfig();
         configuration.shouldHandleLightDoze = true;
         final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
-                configuration, mDependencies);
+                configuration, mNetworkQuirkMetrics, mDependencies);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
@@ -1791,7 +1820,7 @@
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mDependencies);
+                mNetworkQuirkMetrics, mDependencies);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
@@ -1812,7 +1841,7 @@
         apfFilter.shutdown();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mDependencies);
+                mNetworkQuirkMetrics, mDependencies);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IEEE802.3 frame is dropped
@@ -1840,7 +1869,7 @@
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mDependencies);
+                mNetworkQuirkMetrics, mDependencies);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
@@ -1861,7 +1890,7 @@
         apfFilter.shutdown();
         config.ethTypeBlackList = ipv4BlackList;
         apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mDependencies);
+                mNetworkQuirkMetrics, mDependencies);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
@@ -1877,7 +1906,7 @@
         apfFilter.shutdown();
         config.ethTypeBlackList = ipv4Ipv6BlackList;
         apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mDependencies);
+                mNetworkQuirkMetrics, mDependencies);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
@@ -1924,7 +1953,8 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
         verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS);
@@ -1984,7 +2014,8 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb,
+                mNetworkQuirkMetrics);
         byte[] program;
         final int srcPort = 12345;
         final int dstPort = 54321;
@@ -2177,7 +2208,8 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb,
+                mNetworkQuirkMetrics);
         byte[] program;
         final int srcPort = 1024;
         final int dstPort = 4500;
@@ -2426,7 +2458,7 @@
     public void testRaToString() throws Exception {
         MockIpClientCallback cb = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics);
 
         byte[] packet = buildLargeRa();
         ApfFilter.Ra ra = apfFilter.new Ra(packet, packet.length);
@@ -2496,7 +2528,8 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int ROUTER_LIFETIME = 1000;
@@ -2587,7 +2620,8 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
@@ -2628,7 +2662,8 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int routerLifetime = 1000;
@@ -2697,7 +2732,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2719,7 +2754,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2736,7 +2771,7 @@
     public void testMatchedRaUpdatesLifetime() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final TestApfFilter apfFilter = new TestApfFilter(mContext, getDefaultConfig(),
-                ipClientCallback);
+                ipClientCallback, mNetworkQuirkMetrics);
 
         // Create an RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
@@ -2764,7 +2799,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
@@ -2793,7 +2829,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
@@ -2829,7 +2866,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build();
@@ -2859,7 +2897,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build();
@@ -2896,7 +2935,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build();
@@ -2929,7 +2969,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
@@ -2968,7 +3009,8 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
@@ -3035,4 +3077,328 @@
                 (Inet4Address) InetAddresses.parseNumericAddress(expected));
         assertEquals(want, got);
     }
+
+    private TestAndroidPacketFilter makeTestApfFilter(ApfConfiguration config,
+            MockIpClientCallback ipClientCallback, boolean isLegacy) throws Exception {
+        final TestAndroidPacketFilter apfFilter;
+        if (isLegacy) {
+            apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                    mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
+        } else {
+            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+                    mDependencies, mClock);
+        }
+        return apfFilter;
+    }
+
+    // LegacyApfFilter ignores zero lifetime RAs (doesn't update program) but ApfFilter won't.
+    private void verifyUpdateProgramForZeroLifetimeRa(MockIpClientCallback ipClientCallback,
+            boolean isLegacy) {
+        if (isLegacy) {
+            ipClientCallback.assertNoProgramUpdate();
+        } else {
+            ipClientCallback.assertProgramUpdateAndGet();
+        }
+    }
+
+    private void verifyInstallPacketFilterFailure(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback(false);
+        final ApfConfiguration config = getDefaultConfig();
+        final TestAndroidPacketFilter apfFilter =
+                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+        reset(mNetworkQuirkMetrics);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testInstallPacketFilterFailure() throws Exception {
+        verifyInstallPacketFilterFailure(false /* isLegacy */);
+    }
+
+    @Test
+    public void testInstallPacketFilterFailure_LegacyApfFilter() throws Exception {
+        verifyInstallPacketFilterFailure(true /* isLegacy */);
+    }
+
+    private void verifyApfProgramOverSize(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final ApfCapabilities capabilities = new ApfCapabilities(2, 512, ARPHRD_ETHER);
+        config.apfCapabilities = capabilities;
+        final TestAndroidPacketFilter apfFilter =
+                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        final byte[] ra = buildLargeRa();
+        apfFilter.pretendPacketReceived(ra);
+        // The generated program size will be 529, which is larger than 512
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfProgramOverSize() throws Exception {
+        verifyApfProgramOverSize(false /* isLegacy */);
+    }
+
+    @Test
+    public void testApfProgramOverSize_LegacyApfFilter() throws Exception {
+        verifyApfProgramOverSize(true /* isLegacy */);
+    }
+
+    private void verifyGenerateApfProgramException(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final TestAndroidPacketFilter apfFilter;
+        if (isLegacy) {
+            apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                    mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies,
+                    true /* throwsExceptionWhenGeneratesProgram */);
+        } else {
+            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+                    mDependencies, true /* throwsExceptionWhenGeneratesProgram */);
+        }
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
+        verify(mNetworkQuirkMetrics).statsWrite();
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testGenerateApfProgramException() throws Exception {
+        verifyGenerateApfProgramException(false /* isLegacy */);
+    }
+
+    @Test
+    public void testGenerateApfProgramException_LegacyApfFilter() throws Exception {
+        verifyGenerateApfProgramException(true /* isLegacy */);
+    }
+
+    private void verifyApfSessionInfoMetrics(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final ApfCapabilities capabilities = new ApfCapabilities(4, 4096, ARPHRD_ETHER);
+        config.apfCapabilities = capabilities;
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter =
+                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        int maxProgramSize = 0;
+        int numProgramUpdated = 0;
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        final byte[] data = new byte[Counter.totalSize()];
+        final byte[] expectedData = data.clone();
+        final int totalPacketsCounterIdx = Counter.totalSize() + Counter.TOTAL_PACKETS.offset();
+        final int passedIpv6IcmpCounterIdx =
+                Counter.totalSize() + Counter.PASSED_IPV6_ICMP.offset();
+        final int droppedIpv4MulticastIdx =
+                Counter.totalSize() + Counter.DROPPED_IPV4_MULTICAST.offset();
+
+        // Receive an RA packet (passed).
+        final byte[] ra = buildLargeRa();
+        expectedData[totalPacketsCounterIdx + 3] += 1;
+        expectedData[passedIpv6IcmpCounterIdx + 3] += 1;
+        assertDataMemoryContents(PASS, program, ra, data, expectedData);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        apfFilter.setMulticastFilter(true);
+        // setMulticastFilter will trigger program installation.
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        // Receive IPv4 multicast packet (dropped).
+        final byte[] multicastIpv4Addr = {(byte) 224, 0, 0, 1};
+        ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
+        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+        expectedData[totalPacketsCounterIdx + 3] += 1;
+        expectedData[droppedIpv4MulticastIdx + 3] += 1;
+        assertDataMemoryContents(DROP, program, mcastv4packet.array(), data, expectedData);
+
+        // Set data snapshot and update counters.
+        apfFilter.setDataSnapshot(data);
+
+        // Write metrics data to statsd pipeline when shutdown.
+        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+        verify(mApfSessionInfoMetrics).setVersion(4);
+        verify(mApfSessionInfoMetrics).setMemorySize(4096);
+
+        // Verify Counters
+        final Map<Counter, Long> expectedCounters = Map.of(Counter.TOTAL_PACKETS, 2L,
+                Counter.PASSED_IPV6_ICMP, 1L, Counter.DROPPED_IPV4_MULTICAST, 1L);
+        final ArgumentCaptor<Counter> counterCaptor = ArgumentCaptor.forClass(Counter.class);
+        final ArgumentCaptor<Long> valueCaptor = ArgumentCaptor.forClass(Long.class);
+        verify(mApfSessionInfoMetrics, times(expectedCounters.size())).addApfCounter(
+                counterCaptor.capture(), valueCaptor.capture());
+        final List<Counter> counters = counterCaptor.getAllValues();
+        final List<Long> values = valueCaptor.getAllValues();
+        final ArrayMap<Counter, Long> capturedCounters = new ArrayMap<>();
+        for (int i = 0; i < counters.size(); i++) {
+            capturedCounters.put(counters.get(i), values.get(i));
+        }
+        assertEquals(expectedCounters, capturedCounters);
+
+        verify(mApfSessionInfoMetrics).setApfSessionDurationSeconds(
+                (int) (durationTimeMs / DateUtils.SECOND_IN_MILLIS));
+        verify(mApfSessionInfoMetrics).setNumOfTimesApfProgramUpdated(numProgramUpdated);
+        verify(mApfSessionInfoMetrics).setMaxProgramSize(maxProgramSize);
+        verify(mApfSessionInfoMetrics).statsWrite();
+    }
+
+    @Test
+    public void testApfSessionInfoMetrics() throws Exception {
+        verifyApfSessionInfoMetrics(false /* isLegacy */);
+    }
+
+    @Test
+    public void testApfSessionInfoMetrics_LegacyApfFilter() throws Exception {
+        verifyApfSessionInfoMetrics(true /* isLegacy */);
+    }
+
+    private void verifyIpClientRaInfoMetrics(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter =
+                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        final int routerLifetime = 1000;
+        final int prefixValidLifetime = 200;
+        final int prefixPreferredLifetime = 100;
+        final int rdnssLifetime  = 300;
+        final int routeLifetime  = 400;
+
+        // Construct 2 RAs with partial lifetimes larger than predefined constants
+        final RaPacketBuilder ra1 = new RaPacketBuilder(routerLifetime);
+        ra1.addPioOption(prefixValidLifetime + 123, prefixPreferredLifetime, "2001:db8::/64");
+        ra1.addRdnssOption(rdnssLifetime, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ra1.addRioOption(routeLifetime + 456, "64:ff9b::/96");
+        final RaPacketBuilder ra2 = new RaPacketBuilder(routerLifetime + 123);
+        ra2.addPioOption(prefixValidLifetime, prefixPreferredLifetime, "2001:db9::/64");
+        ra2.addRdnssOption(rdnssLifetime + 456, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ra2.addRioOption(routeLifetime, "64:ff9b::/96");
+
+        // Construct an invalid RA packet
+        final RaPacketBuilder raInvalid = new RaPacketBuilder(routerLifetime);
+        raInvalid.addZeroLengthOption();
+
+        // Construct 4 different kinds of zero lifetime RAs
+        final RaPacketBuilder raZeroRouterLifetime = new RaPacketBuilder(0 /* routerLft */);
+        final RaPacketBuilder raZeroPioValidLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroPioValidLifetime.addPioOption(0, prefixPreferredLifetime, "2001:db10::/64");
+        final RaPacketBuilder raZeroRdnssLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroRdnssLifetime.addPioOption(
+                prefixValidLifetime, prefixPreferredLifetime, "2001:db11::/64");
+        raZeroRdnssLifetime.addRdnssOption(0, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        final RaPacketBuilder raZeroRioRouteLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroRioRouteLifetime.addPioOption(
+                prefixValidLifetime, prefixPreferredLifetime, "2001:db12::/64");
+        raZeroRioRouteLifetime.addRioOption(0, "64:ff9b::/96");
+
+        // Inject RA packets. Calling assertProgramUpdateAndGet()/assertNoProgramUpdate() is to make
+        // sure that the RA packet has been processed.
+        apfFilter.pretendPacketReceived(ra1.build());
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        apfFilter.pretendPacketReceived(ra2.build());
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        apfFilter.pretendPacketReceived(raInvalid.build());
+        ipClientCallback.assertNoProgramUpdate();
+        apfFilter.pretendPacketReceived(raZeroRouterLifetime.build());
+        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        apfFilter.pretendPacketReceived(raZeroPioValidLifetime.build());
+        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        apfFilter.pretendPacketReceived(raZeroRdnssLifetime.build());
+        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        apfFilter.pretendPacketReceived(raZeroRioRouteLifetime.build());
+        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+
+        // Write metrics data to statsd pipeline when shutdown.
+        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+
+        // Verify each metric fields in IpClientRaInfoMetrics.
+        if (isLegacy) {
+            // LegacyApfFilter will purge expired RAs before adding new RA. Every time a new zero
+            // lifetime RA is received, zero lifetime RAs except the newly added one will be
+            // cleared, so the number of distinct RAs is 3 (ra1, ra2 and the newly added RA).
+            verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(3);
+        } else {
+            verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(6);
+        }
+        verify(mIpClientRaInfoMetrics).setNumberOfZeroLifetimeRas(4);
+        verify(mIpClientRaInfoMetrics).setNumberOfParsingErrorRas(1);
+        verify(mIpClientRaInfoMetrics).setLowestRouterLifetimeSeconds(routerLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestPioValidLifetimeSeconds(prefixValidLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestRioRouteLifetimeSeconds(routeLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestRdnssLifetimeSeconds(rdnssLifetime);
+        verify(mIpClientRaInfoMetrics).statsWrite();
+    }
+
+    @Test
+    public void testIpClientRaInfoMetrics() throws Exception {
+        verifyIpClientRaInfoMetrics(false /* isLegacy */);
+    }
+
+    @Test
+    public void testIpClientRaInfoMetrics_LegacyApfFilter() throws Exception {
+        verifyIpClientRaInfoMetrics(true /* isLegacy */);
+    }
+
+    private void verifyNoMetricsWrittenForShortDuration(boolean isLegacy) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+
+        // Verify no metrics data written to statsd for duration less than durationTimeMs.
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter =
+                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        doReturn(startTimeMs + durationTimeMs - 1).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+        verify(mApfSessionInfoMetrics, never()).statsWrite();
+        verify(mIpClientRaInfoMetrics, never()).statsWrite();
+
+        // Verify metrics data written to statsd for duration greater than or equal to
+        // durationTimeMs.
+        ApfFilter.Clock clock = mock(ApfFilter.Clock.class);
+        doReturn(startTimeMs).when(clock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter2 = new TestApfFilter(mContext, config,
+                ipClientCallback, mNetworkQuirkMetrics, mDependencies, clock);
+        doReturn(startTimeMs + durationTimeMs).when(clock).elapsedRealtime();
+        apfFilter2.shutdown();
+        verify(mApfSessionInfoMetrics).statsWrite();
+        verify(mIpClientRaInfoMetrics).statsWrite();
+    }
+
+    @Test
+    public void testNoMetricsWrittenForShortDuration() throws Exception {
+        verifyNoMetricsWrittenForShortDuration(false /* isLegacy */);
+    }
+
+    @Test
+    public void testNoMetricsWrittenForShortDuration_LegacyApfFilter() throws Exception {
+        verifyNoMetricsWrittenForShortDuration(true /* isLegacy */);
+    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfTestUtils.java b/tests/unit/src/android/net/apf/ApfTestUtils.java
index 69c4079..abbdd6b 100644
--- a/tests/unit/src/android/net/apf/ApfTestUtils.java
+++ b/tests/unit/src/android/net/apf/ApfTestUtils.java
@@ -28,18 +28,22 @@
 import android.content.Context;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.ip.IIpClientCallbacks;
 import android.net.ip.IpClient;
+import android.net.metrics.IpConnectivityLog;
 import android.os.ConditionVariable;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.format.DateUtils;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.HexDump;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
 
 import libcore.io.IoUtils;
 
@@ -216,16 +220,24 @@
     public static class MockIpClientCallback extends IpClient.IpClientCallbacksWrapper {
         private final ConditionVariable mGotApfProgram = new ConditionVariable();
         private byte[] mLastApfProgram;
+        private boolean mInstallPacketFilterReturn = true;
 
         MockIpClientCallback() {
             super(mock(IIpClientCallbacks.class), mock(SharedLog.class),
                     NetworkInformationShimImpl.newInstance());
         }
 
+        MockIpClientCallback(boolean installPacketFilterReturn) {
+            super(mock(IIpClientCallbacks.class), mock(SharedLog.class),
+                    NetworkInformationShimImpl.newInstance());
+            mInstallPacketFilterReturn = installPacketFilterReturn;
+        }
+
         @Override
-        public void installPacketFilter(byte[] filter) {
+        public boolean installPacketFilter(byte[] filter) {
             mLastApfProgram = filter;
             mGotApfProgram.open();
+            return mInstallPacketFilterReturn;
         }
 
         /**
@@ -254,23 +266,52 @@
     /**
      * The test apf filter class.
      */
-    public static class TestApfFilter extends ApfFilter {
+    public static class TestApfFilter extends ApfFilter implements TestAndroidPacketFilter {
         public static final byte[] MOCK_MAC_ADDR = {1, 2, 3, 4, 5, 6};
         private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1};
 
         private FileDescriptor mWriteSocket;
         private long mCurrentTimeMs = SystemClock.elapsedRealtime();
         private final MockIpClientCallback mMockIpClientCb;
+        private final boolean mThrowsExceptionWhenGeneratesProgram;
 
         public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback) throws Exception {
-            this(context, config, ipClientCallback, new Dependencies(context));
+                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics)
+                throws Exception {
+            this(context, config, ipClientCallback, networkQuirkMetrics, new Dependencies(context),
+                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
         }
 
         public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, Dependencies dependencies) {
-            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback, dependencies);
+                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+                Dependencies dependencies) throws Exception {
+            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
+                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
+        }
+
+        public TestApfFilter(Context context, ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+                Dependencies dependencies, boolean throwsExceptionWhenGeneratesProgram)
+                throws Exception {
+            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
+                    throwsExceptionWhenGeneratesProgram, new ApfFilter.Clock());
+        }
+
+        public TestApfFilter(Context context, ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+                Dependencies dependencies, ApfFilter.Clock clock) throws Exception {
+            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
+                    false /* throwsExceptionWhenGeneratesProgram */, clock);
+        }
+
+        public TestApfFilter(Context context, ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
+                Dependencies dependencies, boolean throwsExceptionWhenGeneratesProgram,
+                ApfFilter.Clock clock) throws Exception {
+            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback,
+                    networkQuirkMetrics, dependencies, clock);
             mMockIpClientCb = ipClientCallback;
+            mThrowsExceptionWhenGeneratesProgram = throwsExceptionWhenGeneratesProgram;
         }
 
         /**
@@ -278,12 +319,13 @@
          */
         public static ApfFilter createTestApfFilter(Context context,
                 MockIpClientCallback ipClientCallback, ApfConfiguration config,
-                ApfFilter.Dependencies dependencies) throws Exception {
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies)
+                throws Exception {
             LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
             LinkProperties lp = new LinkProperties();
             lp.addLinkAddress(link);
             TestApfFilter apfFilter = new TestApfFilter(context, config, ipClientCallback,
-                    dependencies);
+                    networkQuirkMetrics, dependencies);
             apfFilter.setLinkProperties(lp);
             return apfFilter;
         }
@@ -330,7 +372,7 @@
         }
 
         @Override
-        public void shutdown() {
+        public synchronized void shutdown() {
             super.shutdown();
             if (mReceiveThread != null) {
                 mReceiveThread.halt();
@@ -338,5 +380,110 @@
             }
             IoUtils.closeQuietly(mWriteSocket);
         }
+
+        @Override
+        @GuardedBy("this")
+        protected ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+            if (mThrowsExceptionWhenGeneratesProgram) {
+                throw new IllegalStateException();
+            }
+            return super.emitPrologueLocked();
+        }
+    }
+
+    /**
+     * The test legacy apf filter class.
+     */
+    public static class TestLegacyApfFilter extends LegacyApfFilter
+            implements TestAndroidPacketFilter {
+        public static final byte[] MOCK_MAC_ADDR = {1, 2, 3, 4, 5, 6};
+        private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1};
+
+        private FileDescriptor mWriteSocket;
+        private final MockIpClientCallback mMockIpClientCb;
+        private final boolean mThrowsExceptionWhenGeneratesProgram;
+
+        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    new ApfFilter.Dependencies(context),
+                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
+        }
+
+        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                boolean throwsExceptionWhenGeneratesProgram) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    dependencies, throwsExceptionWhenGeneratesProgram, new ApfFilter.Clock());
+        }
+
+        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                ApfFilter.Clock clock) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    dependencies, false /* throwsExceptionWhenGeneratesProgram */, clock);
+        }
+
+        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                boolean throwsExceptionWhenGeneratesProgram, ApfFilter.Clock clock)
+                throws Exception {
+            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback,
+                    ipConnectivityLog, networkQuirkMetrics, dependencies, clock);
+            mMockIpClientCb = ipClientCallback;
+            mThrowsExceptionWhenGeneratesProgram = throwsExceptionWhenGeneratesProgram;
+        }
+
+        /**
+         * Pretend an RA packet has been received and show it to LegacyApfFilter.
+         */
+        public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
+            mMockIpClientCb.resetApfProgramWait();
+            // ApfFilter's ReceiveThread will be waiting to read this.
+            Os.write(mWriteSocket, packet, 0, packet.length);
+        }
+
+        @Override
+        public synchronized void maybeStartFilter() {
+            mHardwareAddress = MOCK_MAC_ADDR;
+            installNewProgramLocked();
+
+            // Create two sockets, "readSocket" and "mWriteSocket" and connect them together.
+            FileDescriptor readSocket = new FileDescriptor();
+            mWriteSocket = new FileDescriptor();
+            try {
+                Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
+            } catch (ErrnoException e) {
+                fail();
+                return;
+            }
+            // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs.
+            // This allows us to pretend RA packets have been received via pretendPacketReceived().
+            mReceiveThread = new ReceiveThread(readSocket);
+            mReceiveThread.start();
+        }
+
+        @Override
+        public synchronized void shutdown() {
+            super.shutdown();
+            if (mReceiveThread != null) {
+                mReceiveThread.halt();
+                mReceiveThread = null;
+            }
+            IoUtils.closeQuietly(mWriteSocket);
+        }
+
+        @Override
+        @GuardedBy("this")
+        protected ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+            if (mThrowsExceptionWhenGeneratesProgram) {
+                throw new IllegalStateException();
+            }
+            return super.emitPrologueLocked();
+        }
     }
 }
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt
index 8cfa316..1977a6c 100644
--- a/tests/unit/src/android/net/apf/ApfV5Test.kt
+++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -15,9 +15,14 @@
  */
 package android.net.apf
 
+import android.net.apf.ApfGenerator.IllegalInstructionException
+import android.net.apf.ApfGenerator.Register.R0
+import android.net.apf.ApfGenerator.Register.R1
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
+import java.lang.IllegalArgumentException
 import kotlin.test.assertContentEquals
+import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -29,82 +34,221 @@
 class ApfV5Test {
 
     @Test
+    fun testApfInstructionVersionCheck() {
+        var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION)
+        assertFailsWith<IllegalInstructionException> { gen.addDrop() }
+        assertFailsWith<IllegalInstructionException> { gen.addCountAndDrop(12) }
+        assertFailsWith<IllegalInstructionException> { gen.addCountAndPass(1000) }
+        assertFailsWith<IllegalInstructionException> { gen.addTransmit() }
+        assertFailsWith<IllegalInstructionException> { gen.addDiscard() }
+        assertFailsWith<IllegalInstructionException> { gen.addAllocateR0() }
+        assertFailsWith<IllegalInstructionException> { gen.addAllocate(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopy(100, 100) }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopy(100, 100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0LenR1() }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0LenR1() }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0(10) }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0(10) }
+    }
+
+    @Test
+    fun testDataInstructionMustComeFirst() {
+        var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addAllocateR0()
+        assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
+    }
+
+    @Test
+    fun testApfInstructionEncodingSizeCheck() {
+        var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) }
+        assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(-1, 1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(-1, 1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, 256) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, 256) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, -1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, -1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(256) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(256) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(-1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(-1) }
+    }
+
+    @Test
     fun testApfInstructionsEncoding() {
-        var gen = ApfGenerator(MIN_APF_VERSION)
-        gen.addAlloc(ApfGenerator.Register.R0)
+        var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION)
+        gen.addPass()
         var program = gen.generate()
-        assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 0), 36), program)
-        assertContentEquals(arrayOf("       0: alloc r0"), ApfJniUtils.disassembleApf(program))
+        // encoding PASS opcode: opcode=0, imm_len=0, R=0
+        assertContentEquals(
+                byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 0)), program)
 
-        gen = ApfGenerator(MIN_APF_VERSION)
-        gen.addTrans(ApfGenerator.Register.R1)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addDrop()
         program = gen.generate()
-        assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 1), 37), program)
-        assertContentEquals(arrayOf("       0: trans r1"), ApfJniUtils.disassembleApf(program))
+        // encoding DROP opcode: opcode=0, imm_len=0, R=1
+        assertContentEquals(
+                byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 1)), program)
 
-        gen = ApfGenerator(MIN_APF_VERSION)
-        gen.addWrite(0x01, 1)
-        gen.addWrite(0x0102, 2)
-        gen.addWrite(0x01020304, 4)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addCountAndPass(129)
+        program = gen.generate()
+        // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
+        assertContentEquals(
+                byteArrayOf(encodeInstruction(opcode = 0, immLength = 1, register = 0),
+                        0x81.toByte()), program)
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addCountAndDrop(1000)
+        program = gen.generate()
+        // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber
+        assertContentEquals(
+                byteArrayOf(encodeInstruction(opcode = 0, immLength = 2, register = 1),
+                        0x03, 0xe8.toByte()), program)
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addAllocateR0()
+        gen.addAllocate(1500)
+        program = gen.generate()
+        // encoding ALLOC opcode: opcode=21(EXT opcode number), imm=36(TRANS opcode number).
+        // R=0 means length stored in R0. R=1 means the length stored in imm1.
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(opcode = 21, immLength = 1, register = 0), 36,
+                encodeInstruction(opcode = 21, immLength = 1, register = 1), 36, 0x05,
+                0xDC.toByte()),
+        program)
+        // TODO: add back disassembling test check after we update the apf_disassembler
+        // assertContentEquals(arrayOf("       0: alloc"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addTransmit()
+        gen.addDiscard()
+        program = gen.generate()
+        // encoding TRANSMIT/DISCARD opcode: opcode=21(EXT opcode number),
+        // imm=37(TRANSMIT/DISCARD opcode number),
+        // R=0 means discard the buffer. R=1 means transmit the buffer.
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(opcode = 21, immLength = 1, register = 0), 37,
+                encodeInstruction(opcode = 21, immLength = 1, register = 1), 37,
+        ), program)
+        // TODO: add back disassembling test check after we update the apf_disassembler
+        // assertContentEquals(arrayOf("       0: trans"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        val largeByteArray = ByteArray(256) { 0x01 }
+        gen.addData(largeByteArray)
+        program = gen.generate()
+        // encoding DATA opcode: opcode=14(JMP), R=1
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(opcode = 14, immLength = 2, register = 1), 0x01, 0x00) +
+                largeByteArray, program)
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addWriteU8(0x01)
+        gen.addWriteU16(0x0102)
+        gen.addWriteU32(0x01020304)
+        gen.addWriteU8(0x00)
+        gen.addWriteU8(0x80)
+        gen.addWriteU16(0x0000)
+        gen.addWriteU16(0x8000)
+        gen.addWriteU32(0x00000000)
+        gen.addWriteU32(0x80000000)
         program = gen.generate()
         assertContentEquals(byteArrayOf(
                 encodeInstruction(24, 1, 0), 0x01,
                 encodeInstruction(24, 2, 0), 0x01, 0x02,
-                encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04
-        ), program)
+                encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04,
+                encodeInstruction(24, 1, 0), 0x00,
+                encodeInstruction(24, 1, 0), 0x80.toByte(),
+                encodeInstruction(24, 2, 0), 0x00, 0x00,
+                encodeInstruction(24, 2, 0), 0x80.toByte(), 0x00,
+                encodeInstruction(24, 4, 0), 0x00, 0x00, 0x00, 0x00,
+                encodeInstruction(24, 4, 0), 0x80.toByte(), 0x00, 0x00,
+                0x00), program)
         assertContentEquals(arrayOf(
                 "       0: write 0x01",
                 "       2: write 0x0102",
-                "       5: write 0x01020304"), ApfJniUtils.disassembleApf(program))
+                "       5: write 0x01020304",
+                "      10: write 0x00",
+                "      12: write 0x80",
+                "      14: write 0x0000",
+                "      17: write 0x8000",
+                "      20: write 0x00000000",
+                "      25: write 0x80000000"),
+        ApfJniUtils.disassembleApf(program))
 
-        gen = ApfGenerator(MIN_APF_VERSION)
-        gen.addWrite(ApfGenerator.Register.R0, 1)
-        gen.addWrite(ApfGenerator.Register.R0, 2)
-        gen.addWrite(ApfGenerator.Register.R0, 4)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addWriteU8(R0)
+        gen.addWriteU16(R0)
+        gen.addWriteU32(R0)
+        gen.addWriteU8(R1)
+        gen.addWriteU16(R1)
+        gen.addWriteU32(R1)
         program = gen.generate()
         assertContentEquals(byteArrayOf(
                 encodeInstruction(21, 1, 0), 38,
                 encodeInstruction(21, 1, 0), 39,
-                encodeInstruction(21, 1, 0), 40
+                encodeInstruction(21, 1, 0), 40,
+                encodeInstruction(21, 1, 1), 38,
+                encodeInstruction(21, 1, 1), 39,
+                encodeInstruction(21, 1, 1), 40
         ), program)
-        assertContentEquals(arrayOf(
-                "       0: write r0, 1",
-                "       2: write r0, 2",
-                "       4: write r0, 4"), ApfJniUtils.disassembleApf(program))
+        // TODO: add back disassembling test check after we update the apf_disassembler
+//        assertContentEquals(arrayOf(
+//                "       0: ewrite1 r0",
+//                "       2: ewrite2 r0",
+//                "       4: ewrite4 r0",
+//                "       6: ewrite1 r1",
+//                "       8: ewrite2 r1",
+//                "      10: ewrite4 r1"), ApfJniUtils.disassembleApf(program))
 
-        gen = ApfGenerator(MIN_APF_VERSION)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addDataCopy(0, 10)
         gen.addDataCopy(1, 5)
         gen.addPacketCopy(1000, 255)
         program = gen.generate()
         assertContentEquals(byteArrayOf(
+                encodeInstruction(25, 0, 1), 10,
                 encodeInstruction(25, 1, 1), 1, 5,
                 encodeInstruction(25, 2, 0),
                 0x03.toByte(), 0xe8.toByte(), 0xff.toByte(),
         ), program)
-        assertContentEquals(arrayOf(
-                "       0: dcopy 1, 5",
-                "       3: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program))
+        // TODO: add back disassembling test check after we update the apf_disassembler
+//        assertContentEquals(arrayOf(
+//                "       0: dcopy 0, 5",
+//                "       3: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program))
 
-        gen = ApfGenerator(MIN_APF_VERSION)
-        gen.addDataCopy(ApfGenerator.Register.R1, 0, 5)
-        gen.addPacketCopy(ApfGenerator.Register.R0, 1000, 255)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addPacketCopyFromR0LenR1()
+        gen.addPacketCopyFromR0(5)
+        gen.addDataCopyFromR0LenR1()
+        gen.addDataCopyFromR0(5)
         program = gen.generate()
         assertContentEquals(byteArrayOf(
-                encodeInstruction(21, 1, 1), 42, 0, 5,
-                encodeInstruction(21, 2, 0),
-                0, 41, 0x03.toByte(), 0xe8.toByte(), 0xff.toByte()
+                encodeInstruction(21, 1, 1), 41,
+                encodeInstruction(21, 1, 0), 41, 5,
+                encodeInstruction(21, 1, 1), 42,
+                encodeInstruction(21, 1, 0), 42, 5,
         ), program)
-        assertContentEquals(arrayOf(
-                "       0: dcopy [r1+0], 5",
-                "       4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program))
+        // TODO: add back the following test case when implementing EPKTCOPY, EDATACOPY opcodes.
+//        assertContentEquals(arrayOf(
+//                "       0: dcopy [r1+0], 5",
+//                "       4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program))
     }
 
     private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte {
         val immLengthEncoding = if (immLength == 4) 3 else immLength
         return opcode.shl(3).or(immLengthEncoding.shl(1)).or(register).toByte()
     }
-
-    companion object {
-        private const val MIN_APF_VERSION = 5
-    }
 }
diff --git a/tests/unit/src/android/net/apf/TestAndroidPacketFilter.java b/tests/unit/src/android/net/apf/TestAndroidPacketFilter.java
new file mode 100644
index 0000000..39386cd
--- /dev/null
+++ b/tests/unit/src/android/net/apf/TestAndroidPacketFilter.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.apf;
+
+import android.system.ErrnoException;
+
+import java.io.IOException;
+
+/**
+ * The interface for TestAndroidPacketFilter
+ */
+public interface TestAndroidPacketFilter extends AndroidPacketFilter {
+    /**
+     * Pretend an RA packet has been received and show it to ApfFilter.
+     */
+    void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException;
+
+    /**
+     * Generate and install a new filter program.
+     */
+    void installNewProgramLocked();
+}
diff --git a/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java b/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
index 2810301..2d0916f 100644
--- a/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpResultsParcelableUtilTest.java
@@ -40,6 +40,7 @@
 import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
+import java.util.Arrays;
 
 /**
  * Tests for {@link IpConfigurationParcelableUtil}.
@@ -63,8 +64,9 @@
         mDhcpResults.serverHostName = "dhcp.example.com";
         mDhcpResults.mtu = 1450;
         mDhcpResults.captivePortalApiUrl = "https://example.com/testapi";
+        mDhcpResults.dmnsrchList.addAll(Arrays.asList("google.com", "example.com"));
         // Any added DhcpResults field must be included in equals() to be tested properly
-        assertFieldCountEquals(10, DhcpResults.class);
+        assertFieldCountEquals(11, DhcpResults.class);
     }
 
     @Test
@@ -126,10 +128,16 @@
         assertEquals(mDhcpResults, unparceled);
     }
 
-    private static void setFieldsLostWhileParceling(@NonNull DhcpResults unparceledResults) {
+    private void setFieldsLostWhileParceling(@NonNull DhcpResults unparceledResults) {
         // TODO: add other fields that are not part of DhcpResultsParcelable here
         // e.g. if the dmnsrchList field is added,
-        // parceledResults.dmnsrchList.addAll(mDhcpResults.dmnSrchList);
+        unparceledResults.dmnsrchList.clear();
+        unparceledResults.dnsServers.clear();
+        unparceledResults.dmnsrchList.addAll(mDhcpResults.dmnsrchList);
+        unparceledResults.domains = mDhcpResults.domains;
+        unparceledResults.dnsServers.addAll(mDhcpResults.dnsServers);
+        unparceledResults.gateway = mDhcpResults.gateway;
+        unparceledResults.ipAddress = mDhcpResults.ipAddress;
     }
 
     /**
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
index 00e480b..32cf464 100644
--- a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
+++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -29,6 +29,21 @@
 @SmallTest
 class Dhcp6PacketTest {
     @Test
+    fun testDecodeDhcp6PacketWithoutIaPdOption() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                        // client identifier option(option_len=12)
+                        "0001000C0003001B024CCBFFFE5F6EA9" +
+                        // elapsed time option(option_len=2)
+                        "000800020000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+            Dhcp6Packet.decode(bytes, bytes.size)
+        }
+    }
+
+    @Test
     fun testDecodeDhcp6SolicitPacket() {
         val solicitHex =
                 // Solicit, Transaction ID
@@ -175,4 +190,198 @@
         assertEquals(400, packet.prefixDelegation.minimalPreferredLifetime)
         assertEquals(1623, packet.prefixDelegation.minimalValidLifetime)
     }
+
+    @Test
+    fun testGetMinimalPreferredValidLifetime() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option(option_len=70, including IA prefix option)
+            "0019004629cc56c7000000d300000152" +
+            // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64", preferred=0,
+            // valid=0)
+            "001a00190000000000000000402401fa00049c04120000000000000000" +
+            // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64", preferred=423,
+            // valid=43200)
+            "001a0019000001a70000a8c040fdfd9ed6795000020000000000000000"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(423, packet.prefixDelegation.minimalPreferredLifetime)
+        assertEquals(43200, packet.prefixDelegation.minimalValidLifetime)
+    }
+
+    @Test
+    fun testStatusCodeOptionWithStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "0019000c000000000000000000000000" +
+            // Status code option: status code=NoPrefixAvail
+            "000d00150006" +
+            // Status code option: status message="no prefix available"
+            "6e6f2070726566697820617661696c61626c65"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionWithoutStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "0019000c000000000000000000000000" +
+            // Status code option: status code=NoPrefixAvail
+            "000d00020006"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, status code=NoPrefixAvail,
+            // status message="no prefix available")
+            "00190025000000000000000000000000000d00150006" +
+            "6e6f2070726566697820617661696c61626c65"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithoutStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, status code=NoPrefixAvail)
+            "00190012000000000000000000000000000d00020006"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionWithTruncatedStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // Status code option: len=21, status code=NoPrefixAvail
+            "000d00150006" +
+            // Status code option: truncated status message="no prefix available"
+            "6e6f2070726566697820617661696c6162"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+            Dhcp6Packet.decode(bytes, bytes.size)
+        }
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithTruncatedStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "00190025000000000000000000000000" +
+            // Status code option: len=21, status code=NoPrefixAvail
+            "000d00150006" +
+            // truncated status message="no prefix available")
+            "6e6f2070726566697820617661696c6162"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+            Dhcp6Packet.decode(bytes, bytes.size)
+        }
+    }
 }
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 4e40fa2..1849776 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -707,7 +707,7 @@
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
                 ApfConfiguration.class);
         verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                any(), configCaptor.capture(), any(), any(), anyBoolean());
+                any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
 
         return configCaptor.getValue();
     }
@@ -776,7 +776,7 @@
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
                 ApfConfiguration.class);
         verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                any(), configCaptor.capture(), any(), any(), anyBoolean());
+                any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         final ApfConfiguration actual = configCaptor.getValue();
         assertNotNull(actual);
         assertEquals(4, actual.apfCapabilities.apfVersionSupported);
@@ -809,7 +809,7 @@
                 8192 /* maxProgramSize */, 4 /* format */);
         ipc.updateApfCapabilities(newApfCapabilities);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
-        verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(),
+        verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
                 anyBoolean());
         verifyShutdown(ipc);
     }
@@ -824,7 +824,7 @@
 
         ipc.updateApfCapabilities(null /* apfCapabilities */);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
-        verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(),
+        verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
                 anyBoolean());
         verifyShutdown(ipc);
     }
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index 6471f3a..4d57df5 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -56,7 +56,6 @@
 import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE
 import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE
 import com.android.networkstack.metrics.IpReachabilityMonitorMetrics
-import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import com.android.testutils.makeNewNeighMessage
 import com.android.testutils.waitForIdle
 import java.io.FileDescriptor
@@ -259,8 +258,6 @@
         }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any())
         doReturn(mIpReachabilityMonitorMetrics)
                 .`when`(dependencies).getIpReachabilityMonitorMetrics()
-        doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(),
-                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION))
 
         val monitorFuture = CompletableFuture<IpReachabilityMonitor>()
         // IpReachabilityMonitor needs to be started from the handler thread
diff --git a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
index 033cdbc..29ddf21 100644
--- a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
+++ b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
@@ -112,6 +112,7 @@
         config.mIPv4ProvisioningMode = PROV_IPV4_DHCP;
         config.mIPv6ProvisioningMode = PROV_IPV6_SLAAC;
         config.mUniqueEui64AddressesOnly = false;
+        config.mCreatorUid = 10136;
         return config;
     }
 
@@ -142,6 +143,7 @@
                 MacAddress.fromString("00:01:02:03:04:05"));
         p.layer2Info = layer2Info.toStableParcelable();
         p.options = makeCustomizedDhcpOptions((byte) 60, new String("android-dhcp-11").getBytes());
+        p.creatorUid = 10136;
         return p;
     }
 
@@ -149,7 +151,7 @@
     public void setUp() {
         mConfig = makeTestProvisioningConfiguration();
         // Any added field must be included in equals() to be tested properly
-        assertFieldCountEquals(17, ProvisioningConfiguration.class);
+        assertFieldCountEquals(18, ProvisioningConfiguration.class);
     }
 
     @Test
@@ -281,7 +283,8 @@
         assertNotEqualsAfterChange(c -> c.mIPv6ProvisioningMode = PROV_IPV6_DISABLED);
         assertNotEqualsAfterChange(c -> c.mIPv6ProvisioningMode = PROV_IPV6_LINKLOCAL);
         assertNotEqualsAfterChange(c -> c.mUniqueEui64AddressesOnly = true);
-        assertFieldCountEquals(17, ProvisioningConfiguration.class);
+        assertNotEqualsAfterChange(c -> c.mCreatorUid = 10138);
+        assertFieldCountEquals(18, ProvisioningConfiguration.class);
     }
 
     private void assertNotEqualsAfterChange(Consumer<ProvisioningConfiguration> mutator) {
diff --git a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
new file mode 100644
index 0000000..332b42f
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2023 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 static org.junit.Assert.assertEquals;
+
+import android.net.apf.ApfCounterTracker.Counter;
+import android.stats.connectivity.CounterName;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for ApfSessionInfoMetrics.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ApfSessionInfoMetricsTest {
+    @Test
+    public void testApfSessionInfoMetrics_VerifyCollectMetrics() throws Exception {
+        ApfSessionInfoReported mStats;
+        final ApfSessionInfoMetrics mMetrics = new ApfSessionInfoMetrics();
+        mMetrics.setVersion(4);
+        mMetrics.setMemorySize(4096);
+        mMetrics.setApfSessionDurationSeconds(123);
+        mMetrics.setNumOfTimesApfProgramUpdated(456);
+        mMetrics.setMaxProgramSize(1234);
+        mMetrics.addApfCounter(Counter.TOTAL_PACKETS, 5678);
+        mMetrics.addApfCounter(Counter.PASSED_ARP_UNICAST_REPLY, 1010);
+        mMetrics.addApfCounter(Counter.DROPPED_MDNS, 333);
+        mStats = mMetrics.statsWrite();
+        assertEquals(4, mStats.getVersion());
+        assertEquals(4096, mStats.getMemorySize());
+        assertEquals(123, mStats.getApfSessionDurationSeconds());
+        assertEquals(456, mStats.getNumOfTimesApfProgramUpdated());
+        assertEquals(1234, mStats.getMaxProgramSize());
+
+        // ApfCounter count: 3 (CN_TOTAL_PACKETS, CN_PASSED_ARP_UNICAST_REPLY, CN_DROPPED_MDNS)
+        final ApfCounterList apfCounterList = mStats.getApfCounterList();
+        assertEquals(3, apfCounterList.getApfCounterCount());
+
+        // Verify 1st ApfCounter: CounterName = CN_TOTAL_PACKETS, CounterValue = 5678
+        ApfCounter apfCounter = apfCounterList.getApfCounter(0);
+        assertEquals(CounterName.CN_TOTAL_PACKETS, apfCounter.getCounterName());
+        assertEquals(5678, apfCounter.getCounterValue());
+
+        // Verify 1st ApfCounter: CounterName = CN_PASSED_ARP_UNICAST_REPLY, CounterValue = 1010
+        apfCounter = apfCounterList.getApfCounter(1);
+        assertEquals(CounterName.CN_PASSED_ARP_UNICAST_REPLY, apfCounter.getCounterName());
+        assertEquals(1010, apfCounter.getCounterValue());
+
+        // Verify 1st ApfCounter: CounterName = CN_DROPPED_MDNS, CounterValue = 333
+        apfCounter = apfCounterList.getApfCounter(2);
+        assertEquals(CounterName.CN_DROPPED_MDNS, apfCounter.getCounterName());
+        assertEquals(333, apfCounter.getCounterValue());
+    }
+
+    @Test
+    public void testApfSessionInfoMetrics_VerifyMaxApfCounter() throws Exception {
+        ApfSessionInfoReported mStats;
+        final ApfSessionInfoMetrics mMetrics = new ApfSessionInfoMetrics();
+        for (Counter counter : Counter.class.getEnumConstants()) {
+            mMetrics.addApfCounter(counter, 1);
+        }
+        final int expectedApfCounterCount = Counter.class.getEnumConstants().length - 1;
+        mStats = mMetrics.statsWrite();
+        final ApfCounterList apfCounterList = mStats.getApfCounterList();
+        assertEquals(expectedApfCounterCount, apfCounterList.getApfCounterCount());
+    }
+
+    private void verifyCounterName(Counter counter,
+            CounterName expectedCounterName) {
+        assertEquals(expectedCounterName, ApfSessionInfoMetrics.apfFilterCounterToEnum(counter));
+    }
+
+    @Test
+    public void testApfSessionInfoMetrics_VerifyApfCounterToEnum() throws Exception {
+        verifyCounterName(Counter.RESERVED_OOB, CounterName.CN_UNKNOWN);
+        verifyCounterName(Counter.TOTAL_PACKETS, CounterName.CN_TOTAL_PACKETS);
+        verifyCounterName(Counter.PASSED_ARP, CounterName.CN_PASSED_ARP);
+        verifyCounterName(Counter.PASSED_DHCP, CounterName.CN_PASSED_DHCP);
+        verifyCounterName(Counter.PASSED_IPV4, CounterName.CN_PASSED_IPV4);
+        verifyCounterName(Counter.PASSED_IPV6_NON_ICMP, CounterName.CN_PASSED_IPV6_NON_ICMP);
+        verifyCounterName(Counter.PASSED_IPV4_UNICAST,  CounterName.CN_PASSED_IPV4_UNICAST);
+        verifyCounterName(Counter.PASSED_IPV6_ICMP, CounterName.CN_PASSED_IPV6_ICMP);
+        verifyCounterName(Counter.PASSED_IPV6_UNICAST_NON_ICMP,
+                CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP);
+        verifyCounterName(Counter.PASSED_ARP_NON_IPV4, CounterName.CN_UNKNOWN);
+        verifyCounterName(Counter.PASSED_ARP_UNKNOWN, CounterName.CN_UNKNOWN);
+        verifyCounterName(Counter.PASSED_ARP_UNICAST_REPLY,
+                CounterName.CN_PASSED_ARP_UNICAST_REPLY);
+        verifyCounterName(Counter.PASSED_NON_IP_UNICAST, CounterName.CN_PASSED_NON_IP_UNICAST);
+        verifyCounterName(Counter.PASSED_MDNS, CounterName.CN_PASSED_MDNS);
+        verifyCounterName(Counter.DROPPED_ETH_BROADCAST, CounterName.CN_DROPPED_ETH_BROADCAST);
+        verifyCounterName(Counter.DROPPED_RA, CounterName.CN_DROPPED_RA);
+        verifyCounterName(Counter.DROPPED_GARP_REPLY, CounterName.CN_DROPPED_GARP_REPLY);
+        verifyCounterName(Counter.DROPPED_ARP_OTHER_HOST, CounterName.CN_DROPPED_ARP_OTHER_HOST);
+        verifyCounterName(Counter.DROPPED_IPV4_L2_BROADCAST,
+                CounterName.CN_DROPPED_IPV4_L2_BROADCAST);
+        verifyCounterName(Counter.DROPPED_IPV4_BROADCAST_ADDR,
+                CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR);
+        verifyCounterName(Counter.DROPPED_IPV4_BROADCAST_NET,
+                CounterName.CN_DROPPED_IPV4_BROADCAST_NET);
+        verifyCounterName(Counter.DROPPED_IPV4_MULTICAST, CounterName.CN_DROPPED_IPV4_MULTICAST);
+        verifyCounterName(Counter.DROPPED_IPV6_ROUTER_SOLICITATION,
+                CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION);
+        verifyCounterName(Counter.DROPPED_IPV6_MULTICAST_NA,
+                CounterName.CN_DROPPED_IPV6_MULTICAST_NA);
+        verifyCounterName(Counter.DROPPED_IPV6_MULTICAST, CounterName.CN_DROPPED_IPV6_MULTICAST);
+        verifyCounterName(Counter.DROPPED_IPV6_MULTICAST_PING,
+                CounterName.CN_DROPPED_IPV6_MULTICAST_PING);
+        verifyCounterName(Counter.DROPPED_IPV6_NON_ICMP_MULTICAST,
+                CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST);
+        verifyCounterName(Counter.DROPPED_802_3_FRAME, CounterName.CN_DROPPED_802_3_FRAME);
+        verifyCounterName(Counter.DROPPED_ETHERTYPE_DENYLISTED,
+                CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED);
+        verifyCounterName(Counter.DROPPED_ARP_REPLY_SPA_NO_HOST,
+                CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST);
+        verifyCounterName(Counter.DROPPED_IPV4_KEEPALIVE_ACK,
+                CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK);
+        verifyCounterName(Counter.DROPPED_IPV6_KEEPALIVE_ACK,
+                CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK);
+        verifyCounterName(Counter.DROPPED_IPV4_NATT_KEEPALIVE,
+                CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE);
+        verifyCounterName(Counter.DROPPED_MDNS, CounterName.CN_DROPPED_MDNS);
+        verifyCounterName(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST, CounterName.CN_UNKNOWN);
+        verifyCounterName(Counter.DROPPED_ARP_NON_IPV4, CounterName.CN_DROPPED_ARP_NON_IPV4);
+        verifyCounterName(Counter.DROPPED_ARP_UNKNOWN, CounterName.CN_DROPPED_ARP_UNKNOWN);
+    }
+}
diff --git a/tests/unit/src/com/android/networkstack/metrics/IpClientRaInfoMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/IpClientRaInfoMetricsTest.java
new file mode 100644
index 0000000..2c16f86
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/metrics/IpClientRaInfoMetricsTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 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 static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for IpClientRaInfoMetrics.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpClientRaInfoMetricsTest {
+    @Test
+    public void testIpClientRaInfoMetrics_VerifyCollectMetrics() throws Exception {
+        IpClientRaInfoReported mStats;
+        final IpClientRaInfoMetrics mMetrics = new IpClientRaInfoMetrics();
+        mMetrics.setMaxNumberOfDistinctRas(12);
+        mMetrics.setNumberOfZeroLifetimeRas(34);
+        mMetrics.setNumberOfParsingErrorRas(56);
+        mMetrics.setLowestRouterLifetimeSeconds(78);
+        mMetrics.setLowestPioValidLifetimeSeconds(123);
+        mMetrics.setLowestRioRouteLifetimeSeconds(456);
+        mMetrics.setLowestRdnssLifetimeSeconds(789);
+        mStats = mMetrics.statsWrite();
+        assertEquals(12, mStats.getMaxNumberOfDistinctRas());
+        assertEquals(34, mStats.getNumberOfZeroLifetimeRas());
+        assertEquals(56, mStats.getNumberOfParsingErrorRas());
+        assertEquals(78, mStats.getLowestRouterLifetimeSeconds());
+        assertEquals(123, mStats.getLowestPioValidLifetimeSeconds());
+        assertEquals(456, mStats.getLowestRioRouteLifetimeSeconds());
+        assertEquals(789, mStats.getLowestRdnssLifetimeSeconds());
+    }
+}
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 5be2573..77e3a12 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -2302,8 +2302,7 @@
     @Test
     public void testReevaluationInterval_networkResume() throws Exception {
         // Setup nothing and expect validation to fail.
-        doReturn(true).when(mDependencies)
-                .isFeatureNotChickenedOut(any(), eq(REEVALUATE_WHEN_RESUME));
+        doReturn(true).when(mDependencies).isFeatureEnabled(any(), eq(REEVALUATE_WHEN_RESUME));
         final NetworkMonitor nm = runFailedNetworkTest();
         verifyNetworkTested(VALIDATION_RESULT_INVALID, 0 /* probesSucceeded */,
                 1 /* interactions */);