Snap for 12316586 from 01092368c71b2fd7a370ad279b6e0311dfc2ee12 to mainline-cellbroadcast-release

Change-Id: I5badfc2fdc69af635254c56bb583125eab8f3f8e
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index a5d7230..004ec09 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -97,8 +97,13 @@
             version: "10",
             imports: [],
         },
+        {
+            version: "11",
+            imports: [],
+        },
 
     ],
+    frozen: true,
 
 }
 
@@ -167,7 +172,7 @@
             enabled: false,
         },
     },
-    imports: ["ipmemorystore-aidl-interfaces-V10"],
+    imports: ["ipmemorystore-aidl-interfaces-V11"],
     // TODO: have tethering depend on networkstack-client and set visibility to private
     visibility: [
         "//system/tools/aidl/build",
@@ -178,39 +183,43 @@
         // Remove old networkstack aidl interface version info that is no longer used.
         {
             version: "13",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "14",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "15",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "16",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "17",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "18",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "19",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "20",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
         {
             version: "21",
-            imports: ["ipmemorystore-aidl-interfaces-V10"],
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
+        },
+        {
+            version: "22",
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
 
     ],
@@ -222,8 +231,8 @@
     sdk_version: "system_current",
     min_sdk_version: "30",
     static_libs: [
-        "ipmemorystore-aidl-interfaces-V10-java",
-        "networkstack-aidl-interfaces-V21-java",
+        "ipmemorystore-aidl-interfaces-V11-java",
+        "networkstack-aidl-interfaces-V22-java",
     ],
     visibility: ["//packages/modules/NetworkStack:__subpackages__"],
     apex_available: [
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/.hash b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/.hash
new file mode 100644
index 0000000..bd4b743
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/.hash
@@ -0,0 +1 @@
+b24ee617afb80799cb35b6241f5847d786c649f3
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStore.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStore.aidl
new file mode 100644
index 0000000..e2c94a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStore.aidl
@@ -0,0 +1,52 @@
+/*
+ * 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 IIpMemoryStore {
+  oneway void storeNetworkAttributes(String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void storeBlob(String l2Key, String clientId, String name, in android.net.ipmemorystore.Blob data, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void findL2Key(in android.net.ipmemorystore.NetworkAttributesParcelable attributes, android.net.ipmemorystore.IOnL2KeyResponseListener listener);
+  oneway void isSameNetwork(String l2Key1, String l2Key2, android.net.ipmemorystore.IOnSameL3NetworkResponseListener listener);
+  oneway void retrieveNetworkAttributes(String l2Key, android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener listener);
+  oneway void retrieveBlob(String l2Key, String clientId, String name, android.net.ipmemorystore.IOnBlobRetrievedListener listener);
+  oneway void factoryReset();
+  oneway void delete(String l2Key, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void deleteCluster(String cluster, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void storeNetworkEvent(String cluster, long timestamp, long expiry, int eventType, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void retrieveNetworkEventCount(String cluster, in long[] sinceTimes, in int[] eventTypes, android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener listener);
+  const int NETWORK_EVENT_NUD_FAILURE_ROAM = 0;
+  const int NETWORK_EVENT_NUD_FAILURE_CONFIRM = 1;
+  const int NETWORK_EVENT_NUD_FAILURE_ORGANIC = 2;
+  const int NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED = 3;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStoreCallbacks.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStoreCallbacks.aidl
new file mode 100644
index 0000000..7dbbc98
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/IIpMemoryStoreCallbacks.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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;
+/* @hide */
+interface IIpMemoryStoreCallbacks {
+  oneway void onIpMemoryStoreFetched(in android.net.IIpMemoryStore ipMemoryStore);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/Blob.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/Blob.aidl
new file mode 100644
index 0000000..4300c83
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/Blob.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.ipmemorystore;
+/* @hide */
+parcelable Blob {
+  byte[] data;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnBlobRetrievedListener.aidl
new file mode 100644
index 0000000..3a263e2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnBlobRetrievedListener.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.ipmemorystore;
+/* @hide */
+interface IOnBlobRetrievedListener {
+  oneway void onBlobRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in String name, in android.net.ipmemorystore.Blob data);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnL2KeyResponseListener.aidl
new file mode 100644
index 0000000..c663ccf
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnL2KeyResponseListener.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.ipmemorystore;
+/* @hide */
+interface IOnL2KeyResponseListener {
+  oneway void onL2KeyResponse(in android.net.ipmemorystore.StatusParcelable status, in String l2Key);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.aidl
new file mode 100644
index 0000000..3740e15
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkAttributesRetrievedListener.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.ipmemorystore;
+/* @hide */
+interface IOnNetworkAttributesRetrievedListener {
+  oneway void onNetworkAttributesRetrieved(in android.net.ipmemorystore.StatusParcelable status, in String l2Key, in android.net.ipmemorystore.NetworkAttributesParcelable attributes);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
new file mode 100644
index 0000000..29d7781
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.ipmemorystore;
+/* @hide */
+interface IOnNetworkEventCountRetrievedListener {
+  oneway void onNetworkEventCountRetrieved(in android.net.ipmemorystore.StatusParcelable status, in int[] counts);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.aidl
new file mode 100644
index 0000000..9d87fbb
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnSameL3NetworkResponseListener.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.ipmemorystore;
+/* @hide */
+interface IOnSameL3NetworkResponseListener {
+  oneway void onSameL3NetworkResponse(in android.net.ipmemorystore.StatusParcelable status, in android.net.ipmemorystore.SameL3NetworkResponseParcelable response);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusAndCountListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusAndCountListener.aidl
new file mode 100644
index 0000000..1e6a41c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusAndCountListener.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 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.ipmemorystore;
+/* @hide */
+interface IOnStatusAndCountListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status, int count);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusListener.aidl
new file mode 100644
index 0000000..dccdf27
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/IOnStatusListener.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.ipmemorystore;
+/* @hide */
+interface IOnStatusListener {
+  oneway void onComplete(in android.net.ipmemorystore.StatusParcelable status);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
new file mode 100644
index 0000000..85af005
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/NetworkAttributesParcelable.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 permissions and
+ * limitations under the License.
+ */// Blob[] is used to represent an array of byte[], as structured AIDL does not support arrays
+// of arrays.
+///////////////////////////////////////////////////////////////////////////////
+// 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.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable NetworkAttributesParcelable {
+  byte[] assignedV4Address;
+  long assignedV4AddressExpiry;
+  String cluster;
+  android.net.ipmemorystore.Blob[] dnsAddresses;
+  int mtu;
+  @nullable android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable ipv6ProvisioningLossQuirk;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
new file mode 100644
index 0000000..377a3ec
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/SameL3NetworkResponseParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable SameL3NetworkResponseParcelable {
+  String l2Key1;
+  String l2Key2;
+  float confidence;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/StatusParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/StatusParcelable.aidl
new file mode 100644
index 0000000..59b96cd
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/ipmemorystore/StatusParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ipmemorystore;
+/* @hide */
+@JavaDerive(toString=true)
+parcelable StatusParcelable {
+  int resultCode;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.aidl
new file mode 100644
index 0000000..c01564b
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/11/android/net/networkstack/aidl/quirks/IPv6ProvisioningLossQuirkParcelable.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.networkstack.aidl.quirks;
+@JavaDerive(toString=true)
+parcelable IPv6ProvisioningLossQuirkParcelable {
+  int detectionCount;
+  long quirkExpiry;
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/IIpMemoryStore.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/IIpMemoryStore.aidl
index 048e84c..e2c94a2 100644
--- a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/IIpMemoryStore.aidl
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/IIpMemoryStore.aidl
@@ -43,4 +43,10 @@
   oneway void factoryReset();
   oneway void delete(String l2Key, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
   oneway void deleteCluster(String cluster, boolean needWipe, android.net.ipmemorystore.IOnStatusAndCountListener listener);
+  oneway void storeNetworkEvent(String cluster, long timestamp, long expiry, int eventType, android.net.ipmemorystore.IOnStatusListener listener);
+  oneway void retrieveNetworkEventCount(String cluster, in long[] sinceTimes, in int[] eventTypes, android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener listener);
+  const int NETWORK_EVENT_NUD_FAILURE_ROAM = 0;
+  const int NETWORK_EVENT_NUD_FAILURE_CONFIRM = 1;
+  const int NETWORK_EVENT_NUD_FAILURE_ORGANIC = 2;
+  const int NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED = 3;
 }
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
new file mode 100644
index 0000000..29d7781
--- /dev/null
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.ipmemorystore;
+/* @hide */
+interface IOnNetworkEventCountRetrievedListener {
+  oneway void onNetworkEventCountRetrieved(in android.net.ipmemorystore.StatusParcelable status, in int[] counts);
+}
diff --git a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
index 227785d..85af005 100644
--- a/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
+++ b/common/networkstackclient/aidl_api/ipmemorystore-aidl-interfaces/current/android/net/ipmemorystore/NetworkAttributesParcelable.aidl
@@ -12,7 +12,8 @@
  * 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.
- */
+ */// Blob[] is used to represent an array of byte[], as structured AIDL does not support arrays
+// of arrays.
 ///////////////////////////////////////////////////////////////////////////////
 // THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/.hash
new file mode 100644
index 0000000..c5e7453
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/.hash
@@ -0,0 +1 @@
+653a7f7fd2390682f0c3739b4d82d9477d1d79f9
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..771deda
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..31f2194
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..fb13c0c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..36eda8e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..8120ffc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..0b6b778
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..6103774
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6a597e6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..83796ee
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..4b3fff5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..18cf954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..4d6d5a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..b624ee4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/PrivateDnsConfigParcel.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 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 */;
+  boolean ddrEnabled = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..0ce91f0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,65 @@
+/*
+**
+** 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;
+  int hostnameSetting;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..94fc27f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..0e1c21c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3cd8860
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..7997936
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..9312f47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..1109f35
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..ab8577c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..87de4a6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ip/IIpClient.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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;
+  const int HOSTNAME_SETTING_UNSET = 0x00;
+  const int HOSTNAME_SETTING_SEND = 0x01;
+  const int HOSTNAME_SETTING_DO_NOT_SEND = 0x02;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9d36419
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/networkstack/aidl/NetworkMonitorParameters.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
new file mode 100644
index 0000000..2ab9db0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..eea3e0d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
new file mode 100644
index 0000000..bb88434
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/22/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
new file mode 100644
index 0000000..f9bb3c4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/22/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/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
index ab62fe7..b624ee4 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/PrivateDnsConfigParcel.aidl
@@ -41,4 +41,5 @@
   String[] dohIps = {};
   String dohPath = "";
   int dohPort = (-1) /* -1 */;
+  boolean ddrEnabled = false;
 }
diff --git a/common/networkstackclient/src/android/net/IIpMemoryStore.aidl b/common/networkstackclient/src/android/net/IIpMemoryStore.aidl
index 3bb58bf..e294f54 100644
--- a/common/networkstackclient/src/android/net/IIpMemoryStore.aidl
+++ b/common/networkstackclient/src/android/net/IIpMemoryStore.aidl
@@ -21,6 +21,7 @@
 import android.net.ipmemorystore.IOnBlobRetrievedListener;
 import android.net.ipmemorystore.IOnL2KeyResponseListener;
 import android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener;
+import android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener;
 import android.net.ipmemorystore.IOnSameL3NetworkResponseListener;
 import android.net.ipmemorystore.IOnStatusAndCountListener;
 import android.net.ipmemorystore.IOnStatusListener;
@@ -152,4 +153,48 @@
      * @return (through the listener) A status to indicate success and the number of deleted records
      */
     void deleteCluster(String cluster, boolean needWipe, IOnStatusAndCountListener listener);
+
+    /**
+     * The network event types related to Neighbor Unreachability Detection(NUD) probe failure
+     * including probe fails due to L2 roam, low Wi-Fi RSSI checks, periodic kernel organic checks,
+     * or a neighbor's MAC address changing during a probe.
+     */
+    const int NETWORK_EVENT_NUD_FAILURE_ROAM = 0;
+    const int NETWORK_EVENT_NUD_FAILURE_CONFIRM = 1;
+    const int NETWORK_EVENT_NUD_FAILURE_ORGANIC = 2;
+    const int NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED = 3;
+
+    /**
+     * Store a specific network event to database for a given cluster.
+     *
+     * @param cluster The cluster representing a notion of network group (e.g., BSSIDs with the
+     *                same SSID).
+     * @param timestamp The timestamp {@link System.currentTimeMillis} when a specific network
+     *                  event occurred.
+     * @param expiry The timestamp {@link System.currentTimeMillis} when a specific network
+     *               event stored in the database expires, e.g. it might be one week from now.
+     * @param eventType One of the NETWORK_EVENT constants above.
+     * @param listener A listener that will be invoked to inform of the completion of this call.
+     * @return (through the listener) A status to indicate success or failure.
+     */
+    void storeNetworkEvent(String cluster, long timestamp, long expiry, int eventType,
+            IOnStatusListener listener);
+
+    /**
+     * Retrieve the specific network event counts for a given cluster and event type since one or
+     * more timestamps in the past.
+     *
+     * @param cluster The cluster to query.
+     * @param sinceTimes An array of timestamps in the past. The query will return an array of
+     *                   equal size. Each element in the array will contain the number of network
+     *                   events between the corresponding timestamp and the current time, e.g. query
+     *                   since the last week and/or the last day.
+     * @param eventTypes An array of network event types to query, which can be one or more of the
+     *                   above NETWORK_EVENT constants.
+     * @param listener The listener that will be invoked to return the answer.
+     * @return (through the listener) The event counts associated with the query, or an empty array
+     *                                if the query failed.
+     */
+    void retrieveNetworkEventCount(String cluster, in long[] sinceTimes, in int[] eventTypes,
+            IOnNetworkEventCountRetrievedListener listener);
 }
diff --git a/common/networkstackclient/src/android/net/IpMemoryStoreClient.java b/common/networkstackclient/src/android/net/IpMemoryStoreClient.java
index a1c5694..8b93124 100644
--- a/common/networkstackclient/src/android/net/IpMemoryStoreClient.java
+++ b/common/networkstackclient/src/android/net/IpMemoryStoreClient.java
@@ -25,6 +25,7 @@
 import android.net.ipmemorystore.OnDeleteStatusListener;
 import android.net.ipmemorystore.OnL2KeyResponseListener;
 import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
+import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener;
 import android.net.ipmemorystore.OnSameL3NetworkResponseListener;
 import android.net.ipmemorystore.OnStatusListener;
 import android.net.ipmemorystore.Status;
@@ -289,4 +290,64 @@
             Log.e(TAG, "Error executing factory reset", m);
         }
     }
+
+    /**
+     * Retrieve the specific network event counts for a given cluster and event type since one or
+     * more timestamps in the past.
+     *
+     * @param cluster The cluster to query.
+     * @param sinceTimes An array of timestamps in the past. The query will return an array of
+     *                   equal size. Each element in the array will contain the number of network
+     *                   events between the corresponding timestamp and the current time, e.g. query
+     *                   since the last week and/or the last day.
+     * @param eventTypes An array of network event types to query, which can be one or more of the
+     *                   above NETWORK_EVENT constants.
+     * @param listener The listener that will be invoked to return the answer.
+     * returns (through the listener) The event counts associated with the query, or an empty array
+     *                                if the query failed.
+     */
+    public void retrieveNetworkEventCount(@NonNull final String cluster,
+            @NonNull final long[] sinceTimes,
+            @NonNull final int[] eventTypes,
+            @Nullable final OnNetworkEventCountRetrievedListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.retrieveNetworkEventCount(cluster, sinceTimes, eventTypes,
+                            OnNetworkEventCountRetrievedListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error retrieving network event count",
+                    () -> listener.onNetworkEventCountRetrieved(
+                            new Status(Status.ERROR_UNKNOWN),
+                            new int[0]) /* empty counts */);
+        }
+    }
+
+    /**
+     * Store a specific network event to database for a given cluster.
+     *
+     * @param cluster The cluster representing a notion of network group (e.g., BSSIDs with the
+     *                same SSID).
+     * @param timestamp The timestamp {@link System.currentTimeMillis} when a specific network
+     *                  event occurred.
+     * @param expiry The timestamp {@link System.currentTimeMillis} when a specific network
+     *               event stored in the database expires, e.g. it might be one week from now.
+     * @param eventType One of the NETWORK_EVENT constants above.
+     * @param listener A listener that will be invoked to inform of the completion of this call.
+     * returns (through the listener) A status to indicate success or failure.
+     */
+    public void storeNetworkEvent(@NonNull final String cluster,
+            final long timestamp,
+            final long expiry,
+            final int eventType,
+            @Nullable final OnStatusListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.storeNetworkEvent(cluster, timestamp, expiry, eventType,
+                            OnStatusListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            if (null == listener) return;
+            ignoringRemoteException("Error storing network event",
+                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
+        }
+    }
 }
diff --git a/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
index e747d61..2b4bc3f 100644
--- a/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
+++ b/common/networkstackclient/src/android/net/PrivateDnsConfigParcel.aidl
@@ -72,4 +72,12 @@
      * The port used to reach the DoH servers.
      */
     int dohPort = -1;
+
+    /**
+     * Whether DDR discovery is enabled. If DDR is enabled, DoH servers will only be discovered
+     * using DDR. If DDR is not enabled, DoH servers will only be discovered using the list of
+     * known providers hardcoded in DnsResolver.
+     */
+    boolean ddrEnabled = false;
+
 }
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl b/common/networkstackclient/src/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
new file mode 100644
index 0000000..b5ef0a1
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ipmemorystore/IOnNetworkEventCountRetrievedListener.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.ipmemorystore;
+
+import android.net.ipmemorystore.StatusParcelable;
+
+/**
+ * A listener for the IpMemoryStore to return the counts of network event that matches the query.
+ * {@hide}
+ */
+oneway interface IOnNetworkEventCountRetrievedListener {
+    /**
+     * The network event counts were fetched for a specified cluster and network event types
+     * (IIpMemoryStore#NETWORK_EVENT_* constants) since one or more timestamps in the past.
+     *
+     * See {@link IIpMemoryStore#retrieveNetworkEventCount} parameter description for more details.
+     */
+    void onNetworkEventCountRetrieved(in StatusParcelable status, in int[] counts);
+}
diff --git a/common/networkstackclient/src/android/net/ipmemorystore/OnNetworkEventCountRetrievedListener.java b/common/networkstackclient/src/android/net/ipmemorystore/OnNetworkEventCountRetrievedListener.java
new file mode 100644
index 0000000..1c30150
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ipmemorystore/OnNetworkEventCountRetrievedListener.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+/**
+ * A listener for the IpMemoryStore to return specific network event counts.
+ * @hide
+ */
+public interface OnNetworkEventCountRetrievedListener {
+    /**
+     * The memory store has come up with the answer to a query that was sent.
+     */
+    void onNetworkEventCountRetrieved(Status status, int[] counts);
+
+    /** Converts this OnNetworkEventCountRetrievedListener to a parcelable object */
+    @NonNull
+    static IOnNetworkEventCountRetrievedListener toAIDL(
+            @NonNull final OnNetworkEventCountRetrievedListener listener) {
+        return new IOnNetworkEventCountRetrievedListener.Stub() {
+            @Override
+            public void onNetworkEventCountRetrieved(
+                    final StatusParcelable statusParcelable,
+                    final int[] counts) {
+                // NonNull, but still don't crash the system server if null
+                if (null != listener) {
+                    listener.onNetworkEventCountRetrieved(new Status(statusParcelable), counts);
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+
+            @Override
+            public String getInterfaceHash() {
+                return this.HASH;
+            }
+        };
+    }
+}
diff --git a/common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java b/common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java
index 632d1d6..9752bae 100644
--- a/common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java
+++ b/common/networkstackclient/src/android/net/shared/PrivateDnsConfig.java
@@ -41,6 +41,12 @@
     @NonNull
     public final InetAddress[] ips;
 
+    // Whether DDR discovery is enabled.
+    // If DDR is enabled, then empty dohName / dohIps indicate that DoH is disabled.
+    // If DDR is disabled, then empty dohName / dohIps indicate that DNS resolver should attempt to
+    // enable DoH based on using its hardcoded list of known providers.
+    public final boolean ddrEnabled;
+
     // These fields store the DoH information discovered from SVCB lookups.
     @NonNull
     public final String dohName;
@@ -67,8 +73,8 @@
      */
     public PrivateDnsConfig(boolean useTls) {
         this(useTls ? PRIVATE_DNS_MODE_OPPORTUNISTIC : PRIVATE_DNS_MODE_OFF, null /* hostname */,
-                null /* ips */, null /* dohName */, null /* dohIps */, null /* dohPath */,
-                -1 /* dohPort */);
+                null /* ips */, false /* ddrEnabled */, null /* dohName */, null /* dohIps */,
+                null /* dohPath */, -1 /* dohPort */);
     }
 
     /**
@@ -78,8 +84,8 @@
      */
     public PrivateDnsConfig(@Nullable String hostname, @Nullable InetAddress[] ips) {
         this(TextUtils.isEmpty(hostname) ? PRIVATE_DNS_MODE_OFF :
-                PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, hostname, ips, null /* dohName */,
-                null /* dohIps */, null /* dohPath */, -1 /* dohPort */);
+                PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, hostname, ips, false /* ddrEnabled */,
+                null /* dohName */, null /* dohIps */, null /* dohPath */, -1 /* dohPort */);
     }
 
     /**
@@ -88,11 +94,12 @@
      * and empty arrays as equivalent.
      */
     public PrivateDnsConfig(int mode, @Nullable String hostname, @Nullable InetAddress[] ips,
-            @Nullable String dohName, @Nullable InetAddress[] dohIps, @Nullable String dohPath,
-            int dohPort) {
+            boolean ddrEnabled,  @Nullable String dohName, @Nullable InetAddress[] dohIps,
+            @Nullable String dohPath, int dohPort) {
         this.mode = mode;
         this.hostname = (hostname != null) ? hostname : "";
         this.ips = (ips != null) ? ips.clone() : new InetAddress[0];
+        this.ddrEnabled = ddrEnabled;
         this.dohName = (dohName != null) ? dohName : "";
         this.dohIps = (dohIps != null) ? dohIps.clone() : new InetAddress[0];
         this.dohPath = (dohPath != null) ? dohPath : "";
@@ -103,6 +110,7 @@
         mode = cfg.mode;
         hostname = cfg.hostname;
         ips = cfg.ips;
+        ddrEnabled = cfg.ddrEnabled;
         dohName = cfg.dohName;
         dohIps = cfg.dohIps;
         dohPath = cfg.dohPath;
@@ -123,6 +131,13 @@
         return mode == PRIVATE_DNS_MODE_OPPORTUNISTIC;
     }
 
+    /**
+     * Returns whether the fields related to private DNS settings are the same.
+     */
+    public boolean areSettingsSameAs(PrivateDnsConfig other) {
+        return mode == other.mode && TextUtils.equals(hostname, other.hostname);
+    }
+
     @Override
     public String toString() {
         return PrivateDnsConfig.class.getSimpleName()
@@ -158,6 +173,7 @@
                 Arrays.asList(dohIps), IpConfigurationParcelableUtil::parcelAddress, String.class);
         parcel.dohPath = dohPath;
         parcel.dohPort = dohPort;
+        parcel.ddrEnabled = ddrEnabled;
         return parcel;
     }
 
@@ -187,7 +203,7 @@
         InetAddress[] dohIps = new InetAddress[parcel.dohIps.length];
         dohIps = fromParcelableArray(parcel.dohIps,
                 IpConfigurationParcelableUtil::unparcelAddress).toArray(dohIps);
-        return new PrivateDnsConfig(parcel.privateDnsMode, parcel.hostname, ips, parcel.dohName,
-                dohIps, parcel.dohPath, parcel.dohPort);
+        return new PrivateDnsConfig(parcel.privateDnsMode, parcel.hostname, ips,
+                parcel.ddrEnabled, parcel.dohName, dohIps, parcel.dohPath, parcel.dohPort);
     }
 }
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
index 6dd4fad..c88587b 100644
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -113,4 +113,11 @@
     default boolean supportNdOffload() {
         return false;
     }
+
+    /**
+     * Return if the ApfFilter should enable mDNS offload.
+     */
+    default boolean shouldEnableMdnsOffload() {
+        return false;
+    }
 }
diff --git a/src/android/net/apf/ApfConstants.java b/src/android/net/apf/ApfConstants.java
index 09be67b..a23e970 100644
--- a/src/android/net/apf/ApfConstants.java
+++ b/src/android/net/apf/ApfConstants.java
@@ -87,6 +87,8 @@
     public static final int ICMP6_MTU_OPTION_TYPE = 5;
     // From RFC6106: Recursive DNS Server option
     public static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+    // From RFC5175: RA Flags Extension option
+    public static final int ICMP6_RA_FLAGS_EXTENSION_OPTION_TYPE = 26;
     // From RFC6106: DNS Search List option
     public static final int ICMP6_DNSSL_OPTION_TYPE = 31;
     // From RFC8910: Captive-Portal option
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
index 6c5b566..9700b5b 100644
--- a/src/android/net/apf/ApfCounterTracker.java
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -59,6 +59,7 @@
         PASSED_ARP_UNICAST_REPLY,
         PASSED_ARP_UNKNOWN,
         PASSED_DHCP,
+        PASSED_ETHER_OUR_SRC_MAC,
         PASSED_IPV4,
         PASSED_IPV4_FROM_DHCPV4_SERVER,
         PASSED_IPV4_UNICAST,
@@ -203,7 +204,7 @@
                 value = 0;
             }
             long oldValue = mCounters.getOrDefault(counter, 0L);
-            // All counters are increamental
+            // All counters are incremental
             if (value > oldValue) {
                 mCounters.put(counter, value);
             }
@@ -216,4 +217,11 @@
     public Map<Counter, Long> getCounters() {
         return mCounters;
     }
+
+    /**
+     * Clear all counters.
+     */
+    public void clearCounters() {
+        mCounters.clear();
+    }
 }
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index c3dc9ac..90bd832 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -52,6 +52,7 @@
 import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_LEN;
 import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_FLAGS_EXTENSION_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_RA_OPTION_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_LEN;
 import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_OFFSET;
@@ -87,6 +88,7 @@
 import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST;
 import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS;
 import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_SECONDS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ETHER_OUR_SRC_MAC;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
@@ -94,6 +96,8 @@
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
+import static android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK;
+import static android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
 import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
@@ -106,6 +110,7 @@
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
@@ -124,8 +129,10 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 
+import android.annotation.ChecksSdkIntAtLeast;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -138,6 +145,11 @@
 import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.BaseApfGenerator.IllegalInstructionException;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.nsd.NsdManager;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
+import android.os.Build;
+import android.os.Handler;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.stats.connectivity.NetworkQuirkEvent;
@@ -158,14 +170,13 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.PacketReader;
 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.FileDescriptor;
-import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -186,16 +197,8 @@
  * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
  * filter out redundant duplicate ones.
  * <p>
- * Threading model:
- * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
- * know what RAs to filter for, thus generating APF programs is dependent on mRas.
- * mRas can be accessed by multiple threads:
- * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
- * - callers of:
- *    - setMulticastFilter(), which can cause an APF program to be generated.
- *    - dump(), which dumps mRas among other things.
- *    - shutdown(), which clears mRas.
- * So access to mRas is synchronized.
+ * Threading model: this class is not thread-safe and can only be accessed from IpClient's
+ * handler thread.
  *
  * @hide
  */
@@ -215,39 +218,27 @@
         public boolean hasClatInterface;
         public boolean shouldHandleArpOffload;
         public boolean shouldHandleNdOffload;
+        public boolean shouldHandleMdnsOffload;
     }
 
-    // Thread to listen for RAs.
-    @VisibleForTesting
-    public class ReceiveThread extends Thread {
-        private final byte[] mPacket = new byte[1514];
-        private final FileDescriptor mSocket;
 
-        private volatile boolean mStopped;
+    private class RaPacketReader extends PacketReader {
+        private static final int RECEIVE_BUFFER_SIZE = 1514;
+        private final int mIfIndex;
 
-        public ReceiveThread(FileDescriptor socket) {
-            mSocket = socket;
-        }
-
-        public void halt() {
-            mStopped = true;
-            // Interrupts the read() call the thread is blocked in.
-            SocketUtils.closeSocketQuietly(mSocket);
+        RaPacketReader(Handler handler, int ifIndex) {
+            super(handler, RECEIVE_BUFFER_SIZE);
+            mIfIndex = ifIndex;
         }
 
         @Override
-        public void run() {
-            log("begin monitoring");
-            while (!mStopped) {
-                try {
-                    int length = Os.read(mSocket, mPacket, 0, mPacket.length);
-                    processRa(mPacket, length);
-                } catch (IOException|ErrnoException e) {
-                    if (!mStopped) {
-                        Log.e(TAG, "Read error", e);
-                    }
-                }
-            }
+        protected FileDescriptor createFd() {
+            return mDependencies.createPacketReaderSocket(mIfIndex);
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            processRa(recvbuf, length);
         }
     }
 
@@ -267,8 +258,8 @@
     @VisibleForTesting
     @NonNull
     public final byte[] mHardwareAddress;
-    @VisibleForTesting
-    public ReceiveThread mReceiveThread;
+    private final RaPacketReader mRaPacketReader;
+    private final Handler mHandler;
     @GuardedBy("this")
     private long mUniqueCounter;
     @GuardedBy("this")
@@ -305,10 +296,15 @@
     private final int mAcceptRaMinLft;
     private final boolean mShouldHandleArpOffload;
     private final boolean mShouldHandleNdOffload;
+    private final boolean mShouldHandleMdnsOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
     private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
+    private final NsdManager mNsdManager;
+    @VisibleForTesting
+    final List<OffloadServiceInfo> mOffloadServiceInfos = new ArrayList<>();
+    private OffloadEngine mOffloadEngine;
 
     private static boolean isDeviceIdleModeChangedAction(Intent intent) {
         return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction());
@@ -339,16 +335,21 @@
     private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            final PowerManager powerManager = context.getSystemService(PowerManager.class);
-            if (isDeviceIdleModeChangedAction(intent)
-                    || isDeviceLightIdleModeChangedAction(intent)) {
-                final boolean deviceIdle = powerManager.isDeviceIdleMode()
-                        || isDeviceLightIdleMode(powerManager);
-                setDozeMode(deviceIdle);
-            }
+            mHandler.post(() -> {
+                if (mIsApfShutdown) return;
+                final PowerManager powerManager = context.getSystemService(PowerManager.class);
+                if (isDeviceIdleModeChangedAction(intent)
+                        || isDeviceLightIdleModeChangedAction(intent)) {
+                    final boolean deviceIdle = powerManager.isDeviceIdleMode()
+                            || isDeviceLightIdleMode(powerManager);
+                    setDozeMode(deviceIdle);
+                }
+            });
         }
     };
 
+    private boolean mIsApfShutdown;
+
     // Our IPv4 address, if we have just one, otherwise null.
     @GuardedBy("this")
     private byte[] mIPv4Address;
@@ -375,16 +376,64 @@
 
     private final Dependencies mDependencies;
 
-    public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics) {
-        this(context, config, ifParams, ipClientCallback, networkQuirkMetrics,
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private void registerOffloadEngine() {
+        if (mOffloadEngine != null) {
+            Log.wtf(TAG,
+                    "registerOffloadEngine called twice without calling unregisterOffloadEngine");
+            return;
+        }
+        mOffloadEngine = new OffloadEngine() {
+            @Override
+            public void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info) {
+                mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+                mOffloadServiceInfos.add(info);
+            }
+
+            @Override
+            public void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info) {
+                mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+            }
+        };
+        mNsdManager.registerOffloadEngine(mInterfaceParams.name,
+                OFFLOAD_TYPE_REPLY,
+                OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK,
+                mHandler::post, mOffloadEngine);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private void unregisterOffloadEngine() {
+        if (mOffloadEngine != null) {
+            mNsdManager.unregisterOffloadEngine(mOffloadEngine);
+            mOffloadServiceInfos.clear();
+            mOffloadEngine = null;
+        }
+    }
+
+    public ApfFilter(Handler handler, Context context, ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            NetworkQuirkMetrics networkQuirkMetrics) {
+        this(handler, context, config, ifParams, ipClientCallback, networkQuirkMetrics,
                 new Dependencies(context));
     }
 
+    private synchronized void maybeCleanUpApfRam() {
+        // Clear the APF memory to reset all counters upon connecting to the first AP
+        // in an SSID. This is limited to APFv3 devices because this large write triggers
+        // a crash on some older devices (b/78905546).
+        if (hasDataAccess(mApfVersionSupported)) {
+            byte[] zeroes = new byte[mApfRamSize];
+            if (!mIpClientCallback.installPacketFilter(zeroes)) {
+                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+            }
+        }
+    }
+
     @VisibleForTesting
-    public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-            Dependencies dependencies) {
+    public ApfFilter(Handler handler, Context context, ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            NetworkQuirkMetrics networkQuirkMetrics, Dependencies dependencies) {
+        mHandler = handler;
         mApfVersionSupported = config.apfVersionSupported;
         mApfRamSize = config.apfRamSize;
         mInstallableProgramSizeClamp = config.installableProgramSizeClamp;
@@ -406,6 +455,7 @@
         mAcceptRaMinLft = config.acceptRaMinLft;
         mShouldHandleArpOffload = config.shouldHandleArpOffload;
         mShouldHandleNdOffload = config.shouldHandleNdOffload;
+        mShouldHandleMdnsOffload = config.shouldHandleMdnsOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
@@ -414,6 +464,8 @@
         mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
         mHasClat = config.hasClatInterface;
 
+        mIsApfShutdown = false;
+
         // Now fill the black list from the passed array
         mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
 
@@ -429,31 +481,23 @@
         mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
         // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
         synchronized (this) {
-            // Clear the APF memory to reset all counters upon connecting to the first AP
-            // in an SSID. This is limited to APFv3 devices because this large write triggers
-            // a crash on some older devices (b/78905546).
-            if (hasDataAccess(mApfVersionSupported)) {
-                byte[] zeroes = new byte[mApfRamSize];
-                if (!mIpClientCallback.installPacketFilter(zeroes)) {
-                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-                }
-            }
-
+            maybeCleanUpApfRam();
             // Install basic filters
             installNewProgramLocked();
         }
-        FileDescriptor socket = mDependencies.createRaReaderSocket(mInterfaceParams.index);
-        if (socket != null) {
-            mReceiveThread = new ReceiveThread(socket);
-            mReceiveThread.start();
+
+        mRaPacketReader = new RaPacketReader(mHandler, mInterfaceParams.index);
+        // The class constructor must be called from the IpClient's handler thread
+        if (!mRaPacketReader.start()) {
+            Log.wtf(TAG, "Failed to start RaPacketReader");
         }
 
         // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
         mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
 
-        mDependencies.onApfFilterCreated(this);
-        if (mReceiveThread != null) {
-            mDependencies.onThreadCreated(mReceiveThread);
+        mNsdManager = context.getSystemService(NsdManager.class);
+        if (shouldEnableMdnsOffload()) {
+            registerOffloadEngine();
         }
     }
 
@@ -471,10 +515,10 @@
          * Create a socket to read RAs.
          */
         @Nullable
-        public FileDescriptor createRaReaderSocket(int ifIndex) {
+        public FileDescriptor createPacketReaderSocket(int ifIndex) {
             FileDescriptor socket;
             try {
-                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
                 NetworkStackUtils.attachRaFilter(socket);
                 SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, ifIndex);
                 Os.bind(socket, addr);
@@ -1011,6 +1055,7 @@
                     case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE:
                     case ICMP6_MTU_OPTION_TYPE:
                     case ICMP6_PREF64_OPTION_TYPE:
+                    case ICMP6_RA_FLAGS_EXTENSION_OPTION_TYPE:
                         addMatchSection(optionLength);
                         break;
                     case ICMP6_CAPTIVE_PORTAL_OPTION_TYPE: // unlikely to ever change.
@@ -1679,7 +1724,14 @@
             // Check 1) it's not a fragment. 2) it's UDP.
             // Load 16 bit frag flags/offset field, 8 bit ttl, 8 bit protocol
             gen.addLoad32(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
-            gen.addAnd(0x3FFF00FF);
+            // Mask out the reserved and don't fragment bits, plus the TTL field.
+            // Because:
+            //   IPV4_FRAGMENT_OFFSET_MASK = 0x1fff
+            //   IPV4_FRAGMENT_MORE_FRAGS_MASK = 0x2000
+            // hence this constant ends up being 0x3FFF00FF.
+            // We want the more flag bit and offset to be 0 (ie. not a fragment),
+            // so after this masking we end up with just the ip protocol (hopefully UDP).
+            gen.addAnd((IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK) << 16 | 0xFF);
             gen.addCountAndDropIfR0NotEquals(IPPROTO_UDP, Counter.DROPPED_IPV4_NON_DHCP4);
             // Check it's addressed to DHCP client port.
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
@@ -1697,7 +1749,8 @@
             // Check 1) it's not a fragment. 2) it's UDP.
             // Load 16 bit frag flags/offset field, 8 bit ttl, 8 bit protocol
             gen.addLoad32(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
-            gen.addAnd(0x3FFF00FF);
+            // see above for explanation of this constant
+            gen.addAnd((IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK) << 16 | 0xFF);
             gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
             // Check it's addressed to DHCP client port.
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
@@ -1993,7 +2046,7 @@
         // if there is a hop-by-hop option present (e.g. MLD query)
         //   pass
         // if we're dropping multicast
-        //   if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+        //   if it's not ICMPv6 or it's ICMPv6 but we're in doze mode:
         //     if it's multicast:
         //       drop
         //     pass
@@ -2275,6 +2328,8 @@
 
         // Here's a basic summary of what the initial program does:
         //
+        // if it is a loopback (src mac is nic's primary mac) packet
+        //    pass
         // if it's a 802.3 Frame (ethtype < 0x0600):
         //    drop or pass based on configurations
         // if it has a ether-type that belongs to the black list
@@ -2289,6 +2344,9 @@
         //   pass
         // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
 
+        gen.addLoadImmediate(R0, ETHER_SRC_ADDR_OFFSET);
+        gen.addCountAndPassIfBytesAtR0Equal(mHardwareAddress, PASSED_ETHER_OUR_SRC_MAC);
+
         gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
         if (SdkLevel.isAtLeastV()) {
             // IPv4, ARP, IPv6, EAPOL, WAPI
@@ -2371,9 +2429,10 @@
         final byte[] program;
         int programMinLft = Integer.MAX_VALUE;
 
-        // Ensure the entire APF program uses the same time base.
-        int timeSeconds = secondsSinceBoot();
         try {
+            // Ensure the entire APF program uses the same time base.
+            final int timeSeconds = secondsSinceBoot();
+            mLastTimeInstalledProgram = timeSeconds;
             // Step 1: Determine how many RA filters we can fit in the program.
             ApfV4GeneratorBase<?> gen = emitPrologueLocked();
 
@@ -2422,7 +2481,6 @@
                 sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
             }
         }
-        mLastTimeInstalledProgram = timeSeconds;
         mLastInstalledProgramMinLifetime = programMinLft;
         mLastInstalledProgram = program;
         mNumProgramUpdates++;
@@ -2532,7 +2590,7 @@
      * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet
      * filtering using APF programs.
      */
-    public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
+    public static ApfFilter maybeCreate(Handler handler, Context context, ApfConfiguration config,
             InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
             NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
@@ -2544,7 +2602,8 @@
             return null;
         }
 
-        return new ApfFilter(context, config, ifParams, ipClientCallback, networkQuirkMetrics);
+        return new ApfFilter(handler, context, config, ifParams, ipClientCallback,
+                networkQuirkMetrics);
     }
 
     private synchronized void collectAndSendMetrics() {
@@ -2579,13 +2638,14 @@
 
     public synchronized void shutdown() {
         collectAndSendMetrics();
-        if (mReceiveThread != null) {
-            log("shutting down");
-            mReceiveThread.halt();  // Also closes socket.
-            mReceiveThread = null;
-        }
+        // The shutdown() must be called from the IpClient's handler thread
+        mRaPacketReader.stop();
         mRas.clear();
         mDependencies.removeBroadcastReceiver(mDeviceIdleReceiver);
+        mIsApfShutdown = true;
+        if (shouldEnableMdnsOffload()) {
+            unregisterOffloadEngine();
+        }
     }
 
     public synchronized void setMulticastFilter(boolean isEnabled) {
@@ -2683,6 +2743,13 @@
         return shouldUseApfV6Generator() && mShouldHandleNdOffload;
     }
 
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */, codename =
+            "VanillaIceCream")
+    @Override
+    public boolean shouldEnableMdnsOffload() {
+        return shouldUseApfV6Generator() && mShouldHandleMdnsOffload;
+    }
+
     private boolean shouldUseApfV6Generator() {
         return SdkLevel.isAtLeastV() && ApfV6Generator.supportsVersion(mApfVersionSupported);
     }
@@ -2742,12 +2809,12 @@
     }
 
     public synchronized void dump(IndentingPrintWriter pw) {
+        // TODO: use HandlerUtils.runWithScissors() to dump APF on the handler thread.
         pw.println(String.format(
                 "Capabilities: { apfVersionSupported: %d, maximumApfProgramSize: %d }",
                 mApfVersionSupported, mApfRamSize));
         pw.println("InstallableProgramSizeClamp: " + mInstallableProgramSizeClamp);
         pw.println("Filter update status: " + (mIsRunning ? "RUNNING" : "PAUSED"));
-        pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
         pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
         pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec);
         pw.println("Interface MAC address: " + MacAddress.fromBytes(mHardwareAddress));
@@ -2906,6 +2973,11 @@
 
     /** Resume ApfFilter updates for testing purposes. */
     public void resume() {
+        maybeCleanUpApfRam();
+        // Since the resume() function and cleanup process invalidate previous counter
+        // snapshots, the ApfCounterTracker needs to be reset to maintain reliable, incremental
+        // counter tracking.
+        mApfCounterTracker.clearCounters();
         mIsRunning = true;
     }
 
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
index 51fb428..c071a1b 100644
--- a/src/android/net/ip/ConnectivityPacketTracker.java
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -16,12 +16,15 @@
 
 package android.net.ip;
 
+import static android.net.util.SocketUtils.closeSocket;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.system.OsConstants.AF_PACKET;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
 import android.net.util.ConnectivityPacketSummary;
 import android.os.Handler;
 import android.os.SystemClock;
@@ -30,7 +33,12 @@
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
+import android.util.LruCache;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.TokenBucket;
 import com.android.net.module.util.InterfaceParams;
@@ -39,6 +47,8 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
 
 
 /**
@@ -58,6 +68,50 @@
  * @hide
  */
 public class ConnectivityPacketTracker {
+    /**
+     * Dependencies class for testing.
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static class Dependencies {
+        private final LocalLog mLog;
+        public Dependencies(final LocalLog log) {
+            mLog = log;
+        }
+
+        /**
+         * Create a socket to read RAs.
+         */
+        @Nullable
+        public FileDescriptor createPacketReaderSocket(int ifIndex) {
+            FileDescriptor socket = null;
+            try {
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+                NetworkStackUtils.attachControlPacketFilter(socket);
+                Os.bind(socket, makePacketSocketAddress(ETH_P_ALL, ifIndex));
+            } catch (ErrnoException | IOException e) {
+                final String msg = "Failed to create packet tracking socket: ";
+                Log.e(TAG, msg, e);
+                mLog.log(msg + e);
+                closeFd(socket);
+                return null;
+            }
+            return socket;
+        }
+
+        public int getMaxCapturePktSize() {
+            return MAX_CAPTURE_PACKET_SIZE;
+        }
+
+        private void closeFd(FileDescriptor fd) {
+            try {
+                closeSocket(fd);
+            } catch (IOException e) {
+                Log.e(TAG, "failed to close socket");
+            }
+        }
+    }
+
+    public static final int MAX_CAPTURE_TIME_MS = 300000;
     private static final String TAG = ConnectivityPacketTracker.class.getSimpleName();
     private static final boolean DBG = false;
     private static final String MARK_START = "--- START ---";
@@ -67,21 +121,56 @@
     // Use a TokenBucket to limit CPU usage of logging packets in steady state.
     private static final int TOKEN_FILL_RATE = 50;   // Maximum one packet every 20ms.
     private static final int MAX_BURST_LENGTH = 100; // Maximum burst 100 packets.
+    private static final int MAX_CAPTURE_PACKET_SIZE = 100; // Maximum capture packet size
 
     private final String mTag;
     private final LocalLog mLog;
     private final PacketReader mPacketListener;
     private final TokenBucket mTokenBucket = new TokenBucket(TOKEN_FILL_RATE, MAX_BURST_LENGTH);
+    // store packet hex string in uppercase as key, receive packet count as value
+    private final LruCache<String, Integer> mPacketCache;
+    private final Dependencies mDependencies;
     private long mLastRateLimitLogTimeMs = 0;
     private boolean mRunning;
+    private boolean mCapturing;
     private String mDisplayName;
 
     public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
-        if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+        this(h, ifParams, log, new Dependencies(log));
+    }
 
-        mTag = TAG + "." + ifParams.name;
-        mLog = log;
-        mPacketListener = new PacketListener(h, ifParams);
+    /**
+     * Sets the capture state.
+     *
+     * <p>This method controls whether packet capture is enabled. If capture is disabled,
+     * the internal packet map is cleared.</p>
+     *
+     * @param isCapture {@code true} to enable capture, {@code false} to disable capture
+     */
+    public void setCapture(boolean isCapture) {
+        // prevent configure the duplicated events
+        if (mCapturing == isCapture) {
+            return;
+        }
+
+        mCapturing = isCapture;
+        if (!isCapture) {
+            mPacketCache.evictAll();
+        }
+    }
+
+    /**
+     * Gets the count of matched packets for a given pattern.
+     *
+     * <p>This method searches the internal packet map for packets matching the specified pattern
+     * and returns the count of such packets.</p>
+     *
+     * @param packet The hex string pattern to match against
+     * @return The count of packets matching the pattern, or 0 if no matches are found
+     */
+    public int getMatchedPacketCount(String packet) {
+        final Integer count = mPacketCache.get(packet);
+        return (count != null) ? count : 0;
     }
 
     public void start(String displayName) {
@@ -96,6 +185,28 @@
         mDisplayName = null;
     }
 
+    public boolean isCapturing() {
+        return mCapturing;
+    }
+
+    @VisibleForTesting(visibility = PRIVATE)
+    public int getTotalCapturePacketCount() {
+        return mPacketCache.size();
+    }
+
+    @VisibleForTesting(visibility = PRIVATE)
+    public ConnectivityPacketTracker(
+            @NonNull Handler handler,
+            @NonNull InterfaceParams ifParams,
+            @NonNull LocalLog log,
+            @NonNull Dependencies dependencies) {
+        mTag = TAG + "." + Objects.requireNonNull(ifParams).name;
+        mLog = log;
+        mPacketListener = new PacketListener(handler, ifParams);
+        mDependencies = dependencies;
+        mPacketCache = new LruCache<>(mDependencies.getMaxCapturePktSize());
+    }
+
     private final class PacketListener extends PacketReader {
         private final InterfaceParams mInterface;
 
@@ -106,21 +217,13 @@
 
         @Override
         protected FileDescriptor createFd() {
-            FileDescriptor s = null;
-            try {
-                s = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
-                NetworkStackUtils.attachControlPacketFilter(s);
-                Os.bind(s, makePacketSocketAddress(ETH_P_ALL, mInterface.index));
-            } catch (ErrnoException | IOException e) {
-                logError("Failed to create packet tracking socket: ", e);
-                closeFd(s);
-                return null;
-            }
-            return s;
+            return mDependencies.createPacketReaderSocket(mInterface.index);
         }
 
         @Override
         protected void handlePacket(byte[] recvbuf, int length) {
+            capturePacket(recvbuf, length);
+
             if (!mTokenBucket.get()) {
                 // Rate limited. Log once every second so the user knows packets are missing.
                 final long now = SystemClock.elapsedRealtime();
@@ -171,5 +274,21 @@
         private void addLogEntry(String entry) {
             mLog.log(entry);
         }
+
+        private void capturePacket(byte[] recvbuf, int length) {
+            if (!mCapturing) {
+                return;
+            }
+
+            byte[] pkt = Arrays.copyOfRange(
+                    recvbuf, 0, Math.min(recvbuf.length, length));
+            final String pktHexString = HexDump.toHexString(pkt);
+            final Integer pktCnt = mPacketCache.get(pktHexString);
+            if (pktCnt == null) {
+                mPacketCache.put(pktHexString, 1);
+            } else {
+                mPacketCache.put(pktHexString, pktCnt + 1);
+            }
+        }
     }
 }
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 9bc13bd..8d60804 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -26,6 +26,7 @@
 import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
 import static android.net.ip.IpClient.IpClientCommands.CMD_ADDRESSES_CLEARED;
 import static android.net.ip.IpClient.IpClientCommands.CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF;
+import static android.net.ip.IpClient.IpClientCommands.CMD_APF_STOP_CAPTURE;
 import static android.net.ip.IpClient.IpClientCommands.CMD_COMPLETE_PRECONNECTION;
 import static android.net.ip.IpClient.IpClientCommands.CMD_CONFIRM;
 import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_RUNNING_TO_STOPPING;
@@ -55,6 +56,7 @@
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_HEURISTIC_TRIGGERED;
+import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_PD_PROVISIONED;
 import static android.system.OsConstants.AF_PACKET;
 import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.ETH_P_ARP;
@@ -159,6 +161,7 @@
 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.HandlerUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.SharedLog;
@@ -595,6 +598,8 @@
         static final int CMD_UPDATE_APF_CAPABILITIES = 19;
         static final int EVENT_IPV6_AUTOCONF_TIMEOUT = 20;
         static final int CMD_UPDATE_APF_DATA_SNAPSHOT = 21;
+        // Triggered by ApfShellCommand
+        static final int CMD_APF_STOP_CAPTURE = 22;
         // Internal commands to use instead of trying to call transitionTo() inside
         // a given State's enter() method. Calling transitionTo() from enter/exit
         // encounters a Log.wtf() that can cause trouble on eng builds.
@@ -770,6 +775,7 @@
     private final boolean mPopulateLinkAddressLifetime;
     private final boolean mApfShouldHandleArpOffload;
     private final boolean mApfShouldHandleNdOffload;
+    private final boolean mApfShouldHandleMdnsOffload;
 
     private InterfaceParams mInterfaceParams;
 
@@ -785,6 +791,7 @@
     private String mTcpBufferSizes;
     private ProxyInfo mHttpProxy;
     private AndroidPacketFilter mApfFilter;
+    private ConnectivityPacketTracker mPacketTracker;
     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
@@ -922,12 +929,13 @@
          * APF programs.
          * @see ApfFilter#maybeCreate
          */
-        public AndroidPacketFilter maybeCreateApfFilter(Context context,
+        public AndroidPacketFilter maybeCreateApfFilter(Handler handler, Context context,
                 ApfFilter.ApfConfiguration config, InterfaceParams ifParams,
                 IpClientCallbacksWrapper cb, NetworkQuirkMetrics networkQuirkMetrics,
                 boolean useNewApfFilter) {
             if (useNewApfFilter) {
-                return ApfFilter.maybeCreate(context, config, ifParams, cb, networkQuirkMetrics);
+                return ApfFilter.maybeCreate(handler, context, config, ifParams, cb,
+                        networkQuirkMetrics);
             } else {
                 return LegacyApfFilter.maybeCreate(context, config, ifParams, cb,
                         networkQuirkMetrics);
@@ -1020,6 +1028,8 @@
                 mContext, APF_HANDLE_ARP_OFFLOAD);
         mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
                 mContext, APF_HANDLE_ND_OFFLOAD);
+        // TODO: turn on APF mDNS offload.
+        mApfShouldHandleMdnsOffload = false;
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
 
@@ -1438,7 +1448,16 @@
                     pw.print("TIMEOUT: DUMPING STALE APF SNAPSHOT");
                 }
             }
-            apfFilter.dump(pw);
+            final Handler handler = getHandler();
+            if (handler == null) {
+                // This situation is unexpected. The getHandler() function should not return null
+                // unless the IpClient has stopped running. When the IpClient exits the RunningState
+                // , it should have already set apfFilter to null.
+                pw.println("ApfFilter is not null even if IpClient is not running.");
+            } else {
+                HandlerUtils.runWithScissorsForDump(handler, () -> apfFilter.dump(pw),
+                        10_000 /* ms */);
+            }
             pw.println("APF log:");
             pw.println("mApfDebug: " + mApfDebug);
             mApfLog.dump(fd, pw, args);
@@ -1512,8 +1531,8 @@
         }
 
         final CompletableFuture<String> result = new CompletableFuture<>();
-
-        getHandler().post(() -> {
+        final Handler handler = getHandler();
+        handler.post(() -> {
             try {
                 if (mApfFilter == null) {
                     // IpClient has either stopped or the interface does not support APF.
@@ -1551,6 +1570,42 @@
                         Objects.requireNonNull(snapshot, "No data snapshot recorded.");
                         result.complete(snapshot);
                         break;
+                    case "start-capture":
+                        if (mPacketTracker.isCapturing()) {
+                            result.complete("PacketTracker is already capturing");
+                        } else {
+                            // remove scheduled stop event if it already in the queue
+                            if (handler.hasMessages(IpClientCommands.CMD_APF_STOP_CAPTURE)) {
+                                handler.removeMessages(IpClientCommands.CMD_APF_STOP_CAPTURE);
+                            }
+
+                            mPacketTracker.setCapture(true);
+                            // capture up to 300 sec and stop capturing
+                            sendMessageDelayed(
+                                    IpClientCommands.CMD_APF_STOP_CAPTURE,
+                                    ConnectivityPacketTracker.MAX_CAPTURE_TIME_MS
+                            );
+                            result.complete("success");
+                        }
+                        break;
+                    case "matched-packet-counts":
+                        Objects.requireNonNull(optarg, "No packet pattern provided");
+                        int pktCnt = mPacketTracker.getMatchedPacketCount(optarg);
+                        result.complete(String.valueOf(pktCnt));
+                        break;
+                    case "stop-capture":
+                        if (!mPacketTracker.isCapturing()) {
+                            result.complete("PacketTracker already stop capturing");
+                        } else {
+                            // remove scheduled stop event if it already in the queue
+                            if (handler.hasMessages(IpClientCommands.CMD_APF_STOP_CAPTURE)) {
+                                handler.removeMessages(IpClientCommands.CMD_APF_STOP_CAPTURE);
+                            }
+
+                            sendMessage(IpClientCommands.CMD_APF_STOP_CAPTURE);
+                            result.complete("success");
+                        }
+                        break;
                     default:
                         throw new IllegalArgumentException("Invalid apf command: " + cmd);
                 }
@@ -2250,6 +2305,13 @@
         // the gratuitous NA to update the first-hop router's neighbor cache entry.
         maybeSendMulticastNSes(newLp);
 
+        final boolean gainedV6 = !mLinkProperties.isIpv6Provisioned() && newLp.isIpv6Provisioned();
+        // mDelegatedPrefixes is updated as part of the call to assembleLinkProperties() above.
+        if (gainedV6 && !mDelegatedPrefixes.isEmpty()) {
+            mNetworkQuirkMetrics.setEvent(QE_DHCP6_PD_PROVISIONED);
+            mNetworkQuirkMetrics.statsWrite();
+        }
+
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
         mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
@@ -2645,10 +2707,11 @@
         }
         apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
         apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
+        apfConfig.shouldHandleMdnsOffload = mApfShouldHandleMdnsOffload;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
-        return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
-                mCallback, mNetworkQuirkMetrics, mUseNewApfFilter);
+        return mDependencies.maybeCreateApfFilter(getHandler(), mContext, apfConfig,
+                mInterfaceParams, mCallback, mNetworkQuirkMetrics, mUseNewApfFilter);
     }
 
     private boolean handleUpdateApfCapabilities(@NonNull final ApfCapabilities apfCapabilities) {
@@ -3129,7 +3192,6 @@
     }
 
     class RunningState extends State {
-        private ConnectivityPacketTracker mPacketTracker;
         private boolean mDhcpActionInFlight;
 
         @Override
@@ -3614,7 +3676,9 @@
                     mCallback.startReadPacketFilter("polling");
                     sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
                     break;
-
+                case CMD_APF_STOP_CAPTURE:
+                    mPacketTracker.setCapture(false);
+                    break;
                 default:
                     return NOT_HANDLED;
             }
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 4e8185e..462de90 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -30,6 +30,7 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.IIpMemoryStore;
 import android.net.INetd;
 import android.net.LinkProperties;
 import android.net.RouteInfo;
@@ -837,4 +838,25 @@
                 return INVALID_REACHABILITY_LOSS_TYPE;
         }
     }
+
+    /**
+     * Convert the NUD critical failure event type to NETWORK_EVENT constant defined in
+     * IIpMemoryStore.
+     */
+    public static int nudEventTypeToNetworkEvent(final NudEventType type) {
+        switch (type) {
+            case NUD_POST_ROAMING_FAILED_CRITICAL:
+                return IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
+            case NUD_CONFIRM_FAILED_CRITICAL:
+                return IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
+            case NUD_ORGANIC_FAILED_CRITICAL:
+                return IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
+            case NUD_POST_ROAMING_MAC_ADDRESS_CHANGED:
+            case NUD_CONFIRM_MAC_ADDRESS_CHANGED:
+            case NUD_ORGANIC_MAC_ADDRESS_CHANGED:
+                return IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
+            default:
+                return INVALID_REACHABILITY_LOSS_TYPE;
+        }
+    }
 }
diff --git a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
index 9e7df82..00026a0 100644
--- a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
@@ -35,6 +35,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.net.InetAddress;
@@ -153,7 +155,8 @@
         // Update this whenever changing the schema.
         // DO NOT CHANGE without solid testing for downgrades, and checking onDowngrade
         // below: b/171340630
-        private static final int SCHEMA_VERSION = 4;
+        @VisibleForTesting
+        static final int SCHEMA_VERSION = 4;
         private static final String DATABASE_FILENAME = "IpMemoryStore.db";
         private static final String TRIGGER_NAME = "delete_cascade_to_private";
 
@@ -162,6 +165,12 @@
             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
         }
 
+        @VisibleForTesting
+        DbHelper(@NonNull final Context context, int schemaVersion) {
+            super(context, DATABASE_FILENAME, null, schemaVersion);
+            setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
+        }
+
         /** Called when the database is created */
         @Override
         public void onCreate(@NonNull final SQLiteDatabase db) {
@@ -208,11 +217,6 @@
             // Downgrades always nuke all data and recreate an empty table.
             db.execSQL(NetworkAttributesContract.DROP_TABLE);
             db.execSQL(PrivateDataContract.DROP_TABLE);
-            // TODO: add test for downgrades. Triggers should already be dropped
-            // when the table is dropped, so this may be a bug.
-            // Note that fixing this code does not affect how older versions
-            // will handle downgrades.
-            db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
             onCreate(db);
         }
 
@@ -749,6 +753,19 @@
         }
     }
 
+    static int storeNetworkEvent(@NonNull final SQLiteDatabase db, @NonNull final String cluster,
+            final long timestamp, final long expiry, final int eventType) {
+        // TODO: implement this.
+        return Status.SUCCESS;
+    }
+
+    static int[] retrieveNetworkEventCount(@NonNull final SQLiteDatabase db,
+            @NonNull final String cluster, @NonNull final long[] sinceTimes,
+            @NonNull final int[] eventTypes) {
+        // TODO: implement this.
+        return new int[0];
+    }
+
     // Helper methods
     private static String getString(final Cursor cursor, final String columnName) {
         final int columnIndex = cursor.getColumnIndex(columnName);
diff --git a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java
index 76ed56c..a05c5cb 100644
--- a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java
+++ b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java
@@ -32,6 +32,7 @@
 import android.net.ipmemorystore.IOnBlobRetrievedListener;
 import android.net.ipmemorystore.IOnL2KeyResponseListener;
 import android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener;
+import android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener;
 import android.net.ipmemorystore.IOnSameL3NetworkResponseListener;
 import android.net.ipmemorystore.IOnStatusAndCountListener;
 import android.net.ipmemorystore.IOnStatusListener;
@@ -484,6 +485,95 @@
         });
     }
 
+    /**
+     * Retrieve the specific network event counts for a given cluster and event type since one or
+     * more timestamps in the past.
+     *
+     * @param cluster The cluster to query.
+     * @param sinceTimes An array of timestamps in the past. The query will return an array of
+     *                   equal size. Each element in the array will contain the number of network
+     *                   events between the corresponding timestamp and the current time, e.g. query
+     *                   since the last week and/or the last day.
+     * @param eventTypes An array of network event types to query, which can be one or more of the
+     *                   above NETWORK_EVENT constants.
+     * @param listener The listener that will be invoked to return the answer.
+     * returns (through the listener) The event counts associated with the query, or an empty array
+     *                                if the query failed.
+     */
+    @Override
+    public void retrieveNetworkEventCount(@NonNull final String cluster,
+            @NonNull final long[] sinceTimes,
+            @NonNull final int[] eventTypes,
+            @Nullable final IOnNetworkEventCountRetrievedListener listener) {
+        if (null == listener) return;
+        mExecutor.execute(() -> {
+            try {
+                if (null == cluster) {
+                    listener.onNetworkEventCountRetrieved(
+                            makeStatus(ERROR_ILLEGAL_ARGUMENT), new int[0] /* counts */);
+                    return;
+                }
+                if (null == mDb) {
+                    listener.onNetworkEventCountRetrieved(
+                            makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), new int[0] /* counts */);
+                    return;
+                }
+                try {
+                    final int[] counts = IpMemoryStoreDatabase.retrieveNetworkEventCount(mDb,
+                            cluster, sinceTimes, eventTypes);
+                    listener.onNetworkEventCountRetrieved(makeStatus(SUCCESS), counts);
+                } catch (final Exception e) {
+                    listener.onNetworkEventCountRetrieved(makeStatus(ERROR_GENERIC),
+                            new int[0] /* counts */);
+                }
+            } catch (final RemoteException e) {
+                // Client at the other end died
+            }
+        });
+    }
+
+    /**
+     * Store a specific network event to database for a given cluster.
+     *
+     * @param cluster The cluster representing a notion of network group (e.g., BSSIDs with the
+     *                same SSID).
+     * @param timestamp The timestamp {@link System.currentTimeMillis} when a specific network
+     *                  event occurred.
+     * @param expiry The timestamp {@link System.currentTimeMillis} when a specific network
+     *               event stored in the database expires, e.g. it might be one week from now.
+     * @param eventType One of the NETWORK_EVENT constants above.
+     * @param listener A listener that will be invoked to inform of the completion of this call.
+     * returns (through the listener) A status to indicate success or failure.
+     */
+    @Override
+    public void storeNetworkEvent(@NonNull final String cluster,
+            final long timestamp,
+            final long expiry,
+            final int eventType,
+            @Nullable final IOnStatusListener listener) {
+        mExecutor.execute(() -> {
+            try {
+                if (null == cluster) {
+                    listener.onComplete(makeStatus(ERROR_ILLEGAL_ARGUMENT));
+                    return;
+                }
+                if (null == mDb) {
+                    listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED));
+                    return;
+                }
+                try {
+                    final int code = IpMemoryStoreDatabase.storeNetworkEvent(mDb, cluster,
+                            timestamp, expiry, eventType);
+                    if (null != listener) listener.onComplete(makeStatus(code));
+                } catch (final Exception e) {
+                    if (null != listener) listener.onComplete(makeStatus(ERROR_GENERIC));
+                }
+            } catch (final RemoteException e) {
+                // Client at the other end died
+            }
+        });
+    }
+
     /** Get db size threshold. */
     @VisibleForTesting
     protected int getDbSizeThreshold() {
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 33a8308..0c1eb39 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -276,6 +276,7 @@
 
     /**
      * Experiment flag to enable Discovery of Designated Resolvers (DDR).
+     * This flag requires networkmonitor_async_privdns_resolution flag.
      */
     public static final String DNS_DDR_VERSION = "dns_ddr_version";
 
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index 4d10c3b..780642f 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -627,6 +627,14 @@
                 pw.println("        Format: <apfVersion>,<maxProgramSize>,<packetFormat>");
                 pw.println("      read");
                 pw.println("        reads and returns the current state of APF memory.");
+                pw.println("      start-capture");
+                pw.println("        start capture packets in the received buffer.");
+                pw.println("        The capture is up to 300 sec, then it will stop.");
+                pw.println("      stop-capture");
+                pw.println("        stop capture packets and clear the received buffer.");
+                pw.println("      matched-packet-counts <pkt-hex-string>");
+                pw.println("        the <pkt-hex-string> starts from ether header.");
+                pw.println("        Expect to do full packet match.");
             }
         }
 
diff --git a/src/com/android/server/connectivity/DdrTracker.java b/src/com/android/server/connectivity/DdrTracker.java
index af86914..81d881d 100644
--- a/src/com/android/server/connectivity/DdrTracker.java
+++ b/src/com/android/server/connectivity/DdrTracker.java
@@ -16,25 +16,59 @@
 
 package com.android.server.connectivity;
 
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.CollectionUtils.isEmpty;
 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OFF;
 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.DnsResolver;
+import android.net.LinkProperties;
+import android.net.Network;
 import android.net.shared.PrivateDnsConfig;
+import android.os.CancellationSignal;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsPacket;
+import com.android.net.module.util.DnsSvcbPacket;
+import com.android.net.module.util.SharedLog;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
- * A class to perform DDR on a given network (to be implemented).
+ * A class to perform DDR on a given network.
  *
+ * Caller can use startSvcbLookup() to perform DNS SVCB lookup asynchronously. The result of the
+ * lookup will be passed to callers through the callback onSvcbLookupComplete(). If the result is
+ * stale, the callback won't be invoked. A result becomes stale once there's a new call to
+ * startSvcbLookup().
+ *
+ * Threading:
+ *
+ * 1. DdrTracker is not thread-safe. All public methods must be executed on the same thread to
+ *    guarantee that all DdrTracker members are synchronized.
+ * 2. In DdrTracker constructor, an Executor is provided as the execution thread on which the
+ *    callback onSvcbLookupComplete() will be executed. The execution thread must be the same
+ *    as the thread mentioned in 1.
  */
 class DdrTracker {
+    private static final String TAG = "DDR";
+    private static final boolean DBG  = true;
+
     @IntDef(prefix = { "PRIVATE_DNS_MODE_" }, value = {
         PRIVATE_DNS_MODE_OFF,
         PRIVATE_DNS_MODE_OPPORTUNISTIC,
@@ -43,12 +77,71 @@
     @Retention(RetentionPolicy.SOURCE)
     private @interface PrivateDnsMode {}
 
+    @VisibleForTesting
+    static final String DDR_HOSTNAME = "_dns.resolver.arpa";
+
+    private static final String ALPN_DOH3 = "h3";
+
+    interface Callback {
+        /**
+         * Called on a given execution thread `mExecutor` when a SVCB lookup finishes, unless
+         * the lookup result is stale.
+         * The parameter `result` contains the aggregated result that contains both DoH and DoT
+         * information.
+         */
+        void onSvcbLookupComplete(@NonNull PrivateDnsConfig result);
+    }
+
+    @NonNull
+    private final Network mCleartextDnsNetwork;
+    @NonNull
+    private final DnsResolver mDnsResolver;
+    @NonNull
+    private final Callback mCallback;
+
+    // The execution thread the callback will be executed on.
+    @NonNull
+    private final Executor mExecutor;
+
     // Stores the DNS information that is synced with current DNS configuration.
     @NonNull
     private DnsInfo mDnsInfo;
 
-    DdrTracker() {
-        mDnsInfo = new DnsInfo(new PrivateDnsConfig(false /* useTls */));
+    // Stores the DoT servers discovered from strict mode hostname resolution.
+    @NonNull
+    private final List<InetAddress> mDotServers;
+
+    // Stores the result of latest SVCB lookup.
+    // It is set to null if the result is invalid, for example, lookup timeout or invalid
+    // SVCB responses.
+    @Nullable
+    private DnsSvcbPacket mLatestSvcbPacket = null;
+
+    // Used to check whether a DDR result is stale.
+    // Given the Threading section documented near the beginning of this file, `mTokenId` ensures
+    // that mLatestSvcbRecord is always fresh.
+    @NonNull
+    private int mTokenId;
+
+    // Used to cancel the in-progress SVCB lookup.
+    @NonNull
+    CancellationSignal mCancelSignal;
+
+    private final SharedLog mValidationLogs;
+
+    DdrTracker(@NonNull Network cleartextDnsNetwork, @NonNull DnsResolver dnsResolver,
+            @NonNull Executor executor, @NonNull Callback callback, SharedLog validationLog) {
+        mCleartextDnsNetwork = cleartextDnsNetwork;
+        mDnsResolver = dnsResolver;
+        mExecutor = executor;
+        mCallback = callback;
+        final PrivateDnsConfig privateDnsDisabled = new PrivateDnsConfig(PRIVATE_DNS_MODE_OFF,
+                null /* hostname */, null /* ips */, true /* ddrEnabled */, null /* dohName */,
+                null /* dohIps */, null /* dohPath */, -1 /* dohPort */);
+        mDnsInfo = new DnsInfo(privateDnsDisabled, new ArrayList<>());
+        mDotServers = new ArrayList<>();
+        mCancelSignal = new CancellationSignal();
+        mValidationLogs = validationLog.forSubComponent(TAG);
     }
 
     /**
@@ -57,12 +150,44 @@
      * returns false.
      */
     boolean notifyPrivateDnsSettingsChanged(@NonNull PrivateDnsConfig cfg) {
-        if (arePrivateDnsSettingsEquals(cfg, mDnsInfo.cfg)) return false;
+        if (mDnsInfo.cfg.areSettingsSameAs(cfg)) return false;
 
-        mDnsInfo = new DnsInfo(cfg);
+        ++mTokenId;
+        mDnsInfo = new DnsInfo(cfg, getDnsServers());
+        resetStrictModeHostnameResolutionResult();
         return true;
     }
 
+    /**
+     * If the unencrypted DNS server list on the network has changed (even if only the order has
+     * changed), this function updates the DnsInfo and returns true; otherwise, the DnsInfo remains
+     * unchanged and this function returns false.
+     *
+     * The reason that this method returns true even if only the order has changed is that
+     * DnsResolver returns a DNS answer to app side as soon as it receives a DNS response from
+     * a DNS server. Therefore, the DNS response from the first DNS server that supports DDR
+     * determines the DDR result.
+     */
+    boolean notifyLinkPropertiesChanged(@NonNull LinkProperties lp) {
+        final List<InetAddress> servers = lp.getDnsServers();
+
+        if (servers.equals(getDnsServers())) return false;
+
+        ++mTokenId;
+        mDnsInfo = new DnsInfo(mDnsInfo.cfg, servers);
+        return true;
+    }
+
+    void setStrictModeHostnameResolutionResult(@NonNull InetAddress[] ips) {
+        resetStrictModeHostnameResolutionResult();
+        mDotServers.addAll(Arrays.asList(ips));
+    }
+
+    void resetStrictModeHostnameResolutionResult() {
+        mDotServers.clear();
+    }
+
+    @VisibleForTesting
     @PrivateDnsMode int getPrivateDnsMode() {
         return mDnsInfo.cfg.mode;
     }
@@ -75,22 +200,263 @@
     }
 
     @VisibleForTesting
-    private static boolean arePrivateDnsSettingsEquals(@NonNull PrivateDnsConfig a,
-            @NonNull PrivateDnsConfig b) {
-        return a.mode == b.mode && TextUtils.equals(a.hostname, b.hostname);
+    @NonNull
+    List<InetAddress> getDnsServers() {
+        return mDnsInfo.dnsServers;
+    }
+
+    private boolean hasSvcbAnswer(@NonNull String alpn) {
+        return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.isSupported(alpn) : false;
+    }
+
+    @Nullable
+    private String getTargetNameFromSvcbAnswer(@NonNull String alpn) {
+        return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getTargetName(alpn) : null;
+    }
+
+    // Returns a list of IP addresses for the target name from the latest SVCB packet.
+    // These may be either from the A/AAAA records in the additional section or from the
+    // ipv4hint/ipv6hint keys in the SVCB record.
+    private List<InetAddress> getServersFromSvcbAnswer(@NonNull String alpn) {
+        return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getAddresses(alpn)
+                : Collections.emptyList();
+    }
+
+    private int getPortFromSvcbAnswer(@NonNull String alpn) {
+        return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getPort(alpn) : -1;
+    }
+
+    @Nullable
+    private String getDohPathFromSvcbAnswer(@NonNull String alpn) {
+        return (mLatestSvcbPacket != null) ? mLatestSvcbPacket.getDohPath(alpn) : null;
+    }
+
+    @NonNull
+    private String createHostnameForSvcbQuery() {
+        final String hostname = getStrictModeHostname();
+        if (!TextUtils.isEmpty(hostname)) {
+            return "_dns." + hostname;
+        }
+        return DDR_HOSTNAME;
+    }
+
+    /** Performs a DNS SVCB Lookup asynchronously. */
+    void startSvcbLookup() {
+        if (getPrivateDnsMode() == PRIVATE_DNS_MODE_OFF) {
+            // Ensure getResultForReporting returns reasonable results.
+            mLatestSvcbPacket = null;
+            // We do not need to increment the token. The token is used to ignore stale results.
+            // But there can only be lookups in flight if the mode was previously on. Because the
+            // mode is now off,  that means the mode changed, and that incremented the token.
+            return;
+        }
+        // There are some cases where startSvcbLookup() is called twice in a row that
+        // are likely to lead to the same result, for example:
+        //   1. A network is connected when private DNS mode is strict mode.
+        //   2. Private DNS mode is switched to strict mode.
+        // To avoid duplicate lookups, cancel the in-progress SVCB lookup (if any).
+        //
+        // Note that cancelling is not currently very useful because the DNS resolver still
+        // continues to retry until the query completes or fails. It does prevent the query callback
+        // from being called, but that's not necessary because the token will not match.
+        // We still do attempt to cancel the query so future improvements to the DNS resolver could
+        // use that to do less work.
+        mCancelSignal.cancel();
+        mCancelSignal = new CancellationSignal();
+
+        // Increment the token ID to stale all in-flight lookups.
+        // This is for network revalidation in strict mode that a SVCB lookup can be performed
+        // and its result can be accepted even if there is no DNS configuration change.
+        final int token = ++mTokenId;
+        final String hostname = createHostnameForSvcbQuery();
+        final DnsResolver.Callback<byte[]> callback = new DnsResolver.Callback<byte[]>() {
+            boolean isResultFresh() {
+                return token == mTokenId;
+            }
+
+            void updateSvcbAnswerAndInvokeUserCallback(@Nullable DnsSvcbPacket result) {
+                mLatestSvcbPacket = result;
+                mCallback.onSvcbLookupComplete(getResultForReporting());
+            }
+
+            @Override
+            public void onAnswer(@NonNull byte[] answer, int rcode) {
+                if (!isResultFresh()) {
+                    validationLog("Ignoring stale SVCB answer");
+                    return;
+                }
+
+                if (rcode != 0 || answer.length == 0) {
+                    validationLog("Ignoring invalid SVCB answer: rcode=" + rcode
+                            + " len=" + answer.length);
+                    updateSvcbAnswerAndInvokeUserCallback(null);
+                    return;
+                }
+
+                final DnsSvcbPacket pkt;
+                try {
+                    pkt = DnsSvcbPacket.fromResponse(answer);
+                } catch (DnsPacket.ParseException e) {
+                    validationLog("Ignoring malformed SVCB answer: " + e);
+                    updateSvcbAnswerAndInvokeUserCallback(null);
+                    return;
+                }
+
+                validationLog("Processing SVCB response: " + pkt);
+                updateSvcbAnswerAndInvokeUserCallback(pkt);
+            }
+
+            @Override
+            public void onError(@NonNull DnsResolver.DnsException e) {
+                validationLog("DNS error resolving SVCB record for " + hostname + ": " + e);
+                if (isResultFresh()) {
+                    updateSvcbAnswerAndInvokeUserCallback(null);
+                }
+            }
+        };
+        sendDnsSvcbQuery(hostname, mCancelSignal, callback);
+    }
+
+    /**
+     * Returns candidate IP addresses to use for DoH.
+     *
+     * These can come from A/AAAA records returned by strict mode hostname resolution, from A/AAAA
+     * records in the additional section of the SVCB response, or from the ipv4hint/ipv6hint keys in
+     * the H3 ALPN of the SVCB record itself.
+     *
+     * RFC 9460 §7.3 says that if A and AAAA records for TargetName are locally available, the
+     * client SHOULD ignore the hints.
+     *
+     * - In opportunistic mode, strict name hostname resolution does not happen, so always use the
+     *   addresses in the SVCB response
+     * - In strict mode:
+     *   - If the target name in the H3 ALPN matches the strict mode hostname, prefer the result of
+     *     strict mode hostname resolution.
+     *   - If not, prefer the addresses from the SVCB response, but fall back to A/AAAA records if
+     *     there are none. This ensures that:
+     *     - If the strict mode hostname has A/AAAA addresses, those are used even if there are no
+     *       addresses in the SVCB record.
+     *
+     * Note that in strict mode, this class always uses the user-specified hostname and ignores the
+     * target hostname in the SVCB record (see getResultForReporting). In this case, preferring the
+     * addresses in the SVCB record at ensures that those addresses are used, even if the target
+     * hostname is not.
+     */
+    private List<InetAddress> getTargetNameIpAddresses(@NonNull String alpn) {
+        final List<InetAddress> serversFromSvcbAnswer = getServersFromSvcbAnswer(alpn);
+        final String hostname = getStrictModeHostname();
+        if (TextUtils.isEmpty(hostname)) {
+            return serversFromSvcbAnswer;
+        }
+        // Strict mode can use either A/AAAA records coming from strict mode resolution or the
+        // addresses from the SVCB response (which could be A/AAAA records in the additional section
+        // or the hints in the SVCB record itself).
+        final String targetName = getTargetNameFromSvcbAnswer(alpn);
+        if (TextUtils.equals(targetName, hostname) && !mDotServers.isEmpty()) {
+            return mDotServers;
+        }
+        if (isEmpty(serversFromSvcbAnswer)) {
+            return mDotServers;
+        }
+        return serversFromSvcbAnswer;
+    }
+
+    /**
+     * To follow the design of private DNS opportunistic mode, which is similar to RFC 9462 §4.3,
+     * don't use a designated resolver if its IP address differs from all the unencrypted resolvers'
+     * IP addresses.
+     *
+     * TODO: simplify the code by merging this method with getTargetNameIpAddresses above.
+     */
+    private InetAddress[] getDohServers(@NonNull String alpn) {
+        final List<InetAddress> candidates = getTargetNameIpAddresses(alpn);
+        if (isEmpty(candidates)) return null;
+        if (getPrivateDnsMode() == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) return toArray(candidates);
+
+        candidates.retainAll(getDnsServers());
+        return toArray(candidates);
+    }
+
+    /**
+     * Returns the aggregated private DNS discovery result as a PrivateDnsConfig.
+     * getResultForReporting() is called in the following cases:
+     * 1. when the hostname lookup completes.
+     * 2. when the SVCB lookup completes.
+     *
+     * There is no guarantee which lookup will complete first. Therefore, depending on the private
+     * DNS mode and the SVCB answer, the return PrivateDnsConfig might be set with DoT, DoH,
+     * DoT+DoH, or even no servers.
+     */
+    @NonNull
+    PrivateDnsConfig getResultForReporting() {
+        final String strictModeHostname = getStrictModeHostname();
+        final InetAddress[] dotIps = toArray(mDotServers);
+        final PrivateDnsConfig candidateResultWithDotOnly =
+                new PrivateDnsConfig(getPrivateDnsMode(), strictModeHostname, dotIps,
+                        true /* ddrEnabled */, null /* dohName */, null /* dohIps */,
+                        null /* dohPath */, -1 /* dohPort */);
+
+        if (!hasSvcbAnswer(ALPN_DOH3)) {
+            // TODO(b/240259333): Consider not invoking notifyPrivateDnsConfigResolved() if
+            // DoT server list is empty.
+            return candidateResultWithDotOnly;
+        }
+
+        // The SVCB answer should be fresh.
+
+        final String dohName = (getPrivateDnsMode() == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)
+                ? strictModeHostname : getTargetNameFromSvcbAnswer(ALPN_DOH3);
+        final InetAddress[] dohIps = getDohServers(ALPN_DOH3);
+        final String dohPath = getDohPathFromSvcbAnswer(ALPN_DOH3);
+        final int dohPort = getPortFromSvcbAnswer(ALPN_DOH3);
+
+        return new PrivateDnsConfig(getPrivateDnsMode(), strictModeHostname, dotIps, true,
+                dohName, dohIps, dohPath, dohPort);
+    }
+
+    private void validationLog(String s) {
+        log(s);
+        mValidationLogs.log(s);
+    }
+
+    private void log(String s) {
+        if (DBG) Log.d(TAG + "/" + mCleartextDnsNetwork.toString(), s);
+    }
+
+    /**
+     * A non-blocking call doing DNS SVCB lookup.
+     */
+    private void sendDnsSvcbQuery(String host, @NonNull CancellationSignal cancelSignal,
+            @NonNull DnsResolver.Callback<byte[]> callback) {
+        // Note: the even though this code does not pass FLAG_NO_CACHE_LOOKUP, the query is
+        // currently not cached, because the DNS resolver cache does not cache SVCB records.
+        // TODO: support caching SVCB records in the DNS resolver cache.
+        // This should just work but will need testing.
+        mDnsResolver.rawQuery(mCleartextDnsNetwork, host, CLASS_IN, TYPE_SVCB, 0 /* flags */,
+                mExecutor, cancelSignal, callback);
+    }
+
+    private static InetAddress[] toArray(List<InetAddress> list) {
+        if (list == null) {
+            return null;
+        }
+        return list.toArray(new InetAddress[0]);
     }
 
     /**
      * A class to store current DNS configuration. Only the information relevant to DDR is stored.
      *   1. Private DNS setting.
-     *   2. A list of Unencrypted DNS servers (to be implemented)
+     *   2. A list of Unencrypted DNS servers.
      */
     private static class DnsInfo {
         @NonNull
         public final PrivateDnsConfig cfg;
+        @NonNull
+        public final List<InetAddress> dnsServers;
 
-        DnsInfo(@NonNull PrivateDnsConfig cfg) {
+        DnsInfo(@NonNull PrivateDnsConfig cfg, @NonNull List<InetAddress> dnsServers) {
             this.cfg = cfg;
+            this.dnsServers = dnsServers;
         }
     }
 }
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 05103d9..60bb927 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -65,6 +65,8 @@
 import static com.android.net.module.util.CollectionUtils.isEmpty;
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
+import static com.android.net.module.util.FeatureVersions.FEATURE_DDR_IN_CONNECTIVITY;
+import static com.android.net.module.util.FeatureVersions.FEATURE_DDR_IN_DNSRESOLVER;
 import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL;
 import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static com.android.net.module.util.NetworkStackConstants.TEST_URL_EXPIRATION_TIME;
@@ -682,7 +684,10 @@
                 context, NetworkStackUtils.REEVALUATE_WHEN_RESUME);
         mAsyncPrivdnsResolutionEnabled = deps.isFeatureEnabled(context,
                 NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION);
-        mDdrEnabled = deps.isFeatureEnabled(context, NetworkStackUtils.DNS_DDR_VERSION);
+        mDdrEnabled = mAsyncPrivdnsResolutionEnabled
+                && deps.isFeatureEnabled(context, NetworkStackUtils.DNS_DDR_VERSION)
+                && deps.isFeatureSupported(mContext, FEATURE_DDR_IN_CONNECTIVITY)
+                && deps.isFeatureSupported(mContext, FEATURE_DDR_IN_DNSRESOLVER);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
@@ -719,7 +724,16 @@
         mNetworkCapabilities = new NetworkCapabilities(null);
         mNetworkAgentConfig = NetworkAgentConfigShimImpl.newInstance(null);
 
-        mDdrTracker = new DdrTracker();
+        // For DdrTracker that can safely update SVCB lookup results itself when the lookup
+        // completes. The callback is called inline from onAnswer, which is already posted to
+        // the handler. This ensures that strict mode hostname resolution (which calls
+        // onQueryDone when processing CMD_PRIVATE_DNS_PROBE_COMPLETED) and SVCB lookup (which calls
+        // DdrTracker#updateSvcbAnswerAndInvokeUserCallback by posting onAnswer to the Runnable)
+        // run in order: both of them post exactly once to the handler.
+        mDdrTracker = new DdrTracker(mCleartextDnsNetwork, mDependencies.getDnsResolver(),
+                getHandler()::post,
+                result -> notifyPrivateDnsConfigResolved(result),  // Run inline on handler.
+                mValidationLogs);
     }
 
     /**
@@ -1093,6 +1107,7 @@
                     if (mDdrEnabled) {
                         mDdrTracker.notifyPrivateDnsSettingsChanged(cfg);
                     }
+
                     if (!isPrivateDnsValidationRequired() || !cfg.inStrictMode()) {
                         // No DNS resolution required.
                         //
@@ -1167,6 +1182,12 @@
                     if (tst != null) {
                         tst.setLinkProperties(mLinkProperties);
                     }
+                    final boolean dnsInfoUpdated = mDdrEnabled
+                            && mDdrTracker.notifyLinkPropertiesChanged(mLinkProperties);
+                    if (dnsInfoUpdated) {
+                        removeMessages(CMD_EVALUATE_PRIVATE_DNS);
+                        sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+                    }
                     break;
                 case EVENT_NETWORK_CAPABILITIES_CHANGED:
                     handleCapabilitiesChanged((NetworkCapabilities) message.obj,
@@ -1650,12 +1671,15 @@
 
     private class EvaluatingPrivateDnsState extends State {
         private int mPrivateDnsReevalDelayMs;
-        private PrivateDnsConfig mPrivateDnsConfig;
+        private PrivateDnsConfig mSyncOnlyPrivateDnsConfig;
 
         @Override
         public void enter() {
             mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
-            mPrivateDnsConfig = null;
+            mSyncOnlyPrivateDnsConfig = null;
+            if (mDdrEnabled) {
+                mDdrTracker.resetStrictModeHostnameResolutionResult();
+            }
             sendMessage(CMD_EVALUATE_PRIVATE_DNS);
         }
 
@@ -1663,6 +1687,10 @@
         public boolean processMessage(Message msg) {
             switch (msg.what) {
                 case CMD_EVALUATE_PRIVATE_DNS: {
+                    if (mDdrEnabled) {
+                        mDdrTracker.startSvcbLookup();
+                    }
+
                     if (mAsyncPrivdnsResolutionEnabled) {
                         // Cancel any previously scheduled retry attempt
                         removeMessages(CMD_EVALUATE_PRIVATE_DNS);
@@ -1678,12 +1706,13 @@
                         break;
                     }
 
+                    // Async resolution not enabled, do a blocking DNS lookup.
                     if (inStrictMode()) {
-                        if (!isStrictModeHostnameResolved(mPrivateDnsConfig)) {
+                        if (!isStrictModeHostnameResolved(mSyncOnlyPrivateDnsConfig)) {
                             resolveStrictModeHostname();
 
-                            if (isStrictModeHostnameResolved(mPrivateDnsConfig)) {
-                                notifyPrivateDnsConfigResolved(mPrivateDnsConfig);
+                            if (isStrictModeHostnameResolved(mSyncOnlyPrivateDnsConfig)) {
+                                notifyPrivateDnsConfigResolved(mSyncOnlyPrivateDnsConfig);
                             } else {
                                 handlePrivateDnsEvaluationFailure();
                                 // The private DNS probe fails-fast if the server hostname cannot
@@ -1742,9 +1771,9 @@
                 final InetAddress[] ips = DnsUtils.getAllByName(mDependencies.getDnsResolver(),
                         mCleartextDnsNetwork, mPrivateDnsProviderHostname, getDnsProbeTimeout(),
                         str -> validationLog("Strict mode hostname resolution " + str));
-                mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
+                mSyncOnlyPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
             } catch (UnknownHostException uhe) {
-                mPrivateDnsConfig = null;
+                mSyncOnlyPrivateDnsConfig = null;
             }
         }
 
@@ -1969,11 +1998,16 @@
 
             if (!answer.isEmpty()) {
                 final InetAddress[] ips = answer.toArray(new InetAddress[0]);
-                final PrivateDnsConfig config =
-                        new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
-                notifyPrivateDnsConfigResolved(config);
+                if (mDdrEnabled) {
+                    mDdrTracker.setStrictModeHostnameResolutionResult(ips);
+                    notifyPrivateDnsConfigResolved(mDdrTracker.getResultForReporting());
+                } else {
+                    notifyPrivateDnsConfigResolved(
+                            new PrivateDnsConfig(mPrivateDnsProviderHostname, ips));
+                }
 
-                validationLog("Strict mode hostname resolution " + elapsedNanos + "ns OK "
+                validationLog("Strict mode hostname resolution "
+                        + TimeUnit.NANOSECONDS.toMillis(elapsedNanos) + "ms OK "
                         + answer + " for " + mPrivateDnsProviderHostname);
                 transitionTo(mProbingForPrivateDnsState);
             } else {
@@ -2036,7 +2070,7 @@
 
             final String strIps = Objects.toString(answer);
             validationLog(PROBE_PRIVDNS, queryName,
-                    String.format("%dus: %s", elapsedNanos / 1000, strIps));
+                    String.format("%dms: %s", TimeUnit.NANOSECONDS.toMillis(elapsedNanos), strIps));
 
             mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, success);
             if (success) {
@@ -3727,6 +3761,10 @@
             return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
         }
 
+        boolean isFeatureSupported(@NonNull Context context, long feature) {
+            return DeviceConfigUtils.isFeatureSupported(context, feature);
+        }
+
         /**
          * Collect data stall detection level information for each transport type. Write metrics
          * data to statsd pipeline.
diff --git a/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt b/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
index 3bba529..ac78b7a 100644
--- a/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
+++ b/tests/integration/common/android/net/networkstack/TestNetworkStackServiceClient.kt
@@ -23,6 +23,7 @@
 import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY
 import android.net.INetworkStackConnector
 import android.os.IBinder
+import android.os.UserHandle
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import kotlin.test.fail
@@ -82,7 +83,15 @@
     private fun init() {
         val bindIntent = Intent(testNetworkStackServiceAction)
         bindIntent.component = getNetworkStackComponent(bindIntent.action)
-        context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)
+        // Use UserHandle.SYSTEM to bind to the test network stack service as user 0. Otherwise on a
+        // multi-user device where current user is not user 0, this intent will start another test
+        // service for the current user which is not expected.
+        context.bindServiceAsUser(
+            bindIntent,
+            serviceConnection,
+            Context.BIND_AUTO_CREATE,
+            UserHandle.SYSTEM
+        )
     }
 
     fun disconnect() {
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt
index 715e27d..2c7ce17 100644
--- a/tests/integration/root/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -16,6 +16,7 @@
 
 package android.net.ip
 
+import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_DEVICE_CONFIG
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
@@ -83,7 +84,7 @@
             // Connect to the NetworkStack only once, as it is relatively expensive (~50ms plus any
             // polling time waiting for the test UID to be allowed), and there should be minimal
             // side-effects between tests compared to reconnecting every time.
-            automation.adoptShellPermissionIdentity(NETWORK_SETTINGS)
+            automation.adoptShellPermissionIdentity(NETWORK_SETTINGS, INTERACT_ACROSS_USERS_FULL)
             try {
                 automation.executeShellCommand("su root service call network_stack " +
                         "$ALLOW_TEST_UID_INDEX i32 " + Process.myUid())
diff --git a/tests/unit/jni/apf_jni.cpp b/tests/unit/jni/apf_jni.cpp
index 279e339..873b217 100644
--- a/tests/unit/jni/apf_jni.cpp
+++ b/tests/unit/jni/apf_jni.cpp
@@ -36,7 +36,7 @@
                                uint32_t program_len, uint32_t ram_len,
                                const uint8_t* packet, uint32_t packet_len,
                                uint32_t filter_age) {
-  if (apf_version == 4) {
+  if (apf_version <= 4) {
     return accept_packet((uint8_t*)program, program_len, ram_len, packet, packet_len,
                          filter_age);
   } else {
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
index 0aed28f..15ff224 100644
--- a/tests/unit/src/android/net/apf/ApfFilterTest.kt
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -19,6 +19,8 @@
 import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.MacAddress
+import android.net.NattKeepalivePacketDataParcelable
+import android.net.TcpKeepalivePacketDataParcelable
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_NON_IPV4
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_OTHER_HOST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REPLY_SPA_NO_HOST
@@ -29,14 +31,18 @@
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_GARP_REPLY
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_ADDR
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_NET
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_MULTICAST
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_KEEPALIVE_ACK
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_L2_BROADCAST
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_MULTICAST
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NATT_KEEPALIVE
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_TCP_PORT7_UNICAST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_NA
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NON_ICMP_MULTICAST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
+import android.net.apf.ApfCounterTracker.Counter.PASSED_ETHER_OUR_SRC_MAC
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_BROADCAST_REPLY
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNICAST_REPLY
@@ -50,17 +56,25 @@
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_UNICAST_NON_ICMP
 import android.net.apf.ApfCounterTracker.Counter.PASSED_MLD
 import android.net.apf.ApfFilter.Dependencies
+import android.net.apf.ApfTestHelpers.Companion.TIMEOUT_MS
 import android.net.apf.ApfTestHelpers.Companion.consumeInstalledProgram
 import android.net.apf.ApfTestHelpers.Companion.verifyProgramRun
 import android.net.apf.BaseApfGenerator.APF_VERSION_3
 import android.net.apf.BaseApfGenerator.APF_VERSION_6
 import android.net.ip.IpClient.IpClientCallbacksWrapper
+import android.net.nsd.NsdManager
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.os.SystemClock
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
 import android.system.OsConstants.IFA_F_TENTATIVE
+import android.system.OsConstants.SOCK_STREAM
 import androidx.test.filters.SmallTest
 import com.android.internal.annotations.GuardedBy
 import com.android.net.module.util.HexDump
@@ -81,10 +95,13 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.quitResources
+import com.android.testutils.waitForIdle
+import java.io.FileDescriptor
 import java.net.Inet6Address
 import java.net.InetAddress
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
+import libcore.io.IoUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -92,6 +109,9 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.doAnswer
@@ -111,6 +131,7 @@
 class ApfFilterTest {
     companion object {
         private const val THREAD_QUIT_MAX_RETRY_COUNT = 3
+        private const val TAG = "ApfFilterTest"
     }
 
     @get:Rule
@@ -124,6 +145,7 @@
     @Mock private lateinit var dependencies: Dependencies
 
     @Mock private lateinit var ipClientCallback: IpClientCallbacksWrapper
+    @Mock private lateinit var nsdManager: NsdManager
 
     @GuardedBy("mApfFilterCreated")
     private val mApfFilterCreated = ArrayList<AndroidPacketFilter>()
@@ -177,6 +199,12 @@
         intArrayOf(0x33, 0x33, 0xff, 0xbb, 0xcc, 0xdd).map { it.toByte() }.toByteArray(),
     )
 
+    private val handlerThread by lazy {
+        HandlerThread("$TAG handler thread").apply { start() }
+    }
+    private val handler by lazy { Handler(handlerThread.looper) }
+    private var writerSocket = FileDescriptor()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -194,6 +222,10 @@
             }
         }.`when`(dependencies).onApfFilterCreated(any())
         doReturn(SystemClock.elapsedRealtime()).`when`(dependencies).elapsedRealtime()
+        val readSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writerSocket, readSocket)
+        doReturn(readSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        doReturn(nsdManager).`when`(context).getSystemService(NsdManager::class.java)
     }
 
     private fun shutdownApfFilters() {
@@ -204,7 +236,7 @@
                 return@quitResources ret
             }
         }, { apf: AndroidPacketFilter ->
-            apf.shutdown()
+            handler.post { apf.shutdown() }
         })
 
         synchronized(mApfFilterCreated) {
@@ -218,9 +250,13 @@
 
     @After
     fun tearDown() {
+        IoUtils.closeQuietly(writerSocket)
         shutdownApfFilters()
+        handler.waitForIdle(TIMEOUT_MS)
         Mockito.framework().clearInlineMocks()
         ApfJniUtils.resetTransmittedPacketMemory()
+        handlerThread.quitSafely()
+        handlerThread.join()
     }
 
     private fun getDefaultConfig(apfVersion: Int = APF_VERSION_6): ApfFilter.ApfConfiguration {
@@ -237,16 +273,22 @@
     }
 
     private fun getApfFilter(
-        apfCfg: ApfFilter.ApfConfiguration = getDefaultConfig(APF_VERSION_6)
+            apfCfg: ApfFilter.ApfConfiguration = getDefaultConfig(APF_VERSION_6)
     ): ApfFilter {
-        return ApfFilter(
-            context,
-            apfCfg,
-            ifParams,
-            ipClientCallback,
-            metrics,
-            dependencies
-        )
+        lateinit var apfFilter: ApfFilter
+        handler.post {
+            apfFilter = ApfFilter(
+                    handler,
+                    context,
+                    apfCfg,
+                    ifParams,
+                    ipClientCallback,
+                    metrics,
+                    dependencies
+            )
+        }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        return apfFilter
     }
 
     private fun doTestEtherTypeAllowListFilter(apfFilter: ApfFilter) {
@@ -497,6 +539,26 @@
     }
 
     @Test
+    fun testLoopbackFilter() {
+        val apfConfig = getDefaultConfig()
+        val apfFilter = getApfFilter(apfConfig)
+        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        // Using scapy to generate echo-ed broadcast packet:
+        //   ether = Ether(src=${ifParams.macAddr}, dst='ff:ff:ff:ff:ff:ff')
+        //   ip = IP(src='192.168.1.1', dst='255.255.255.255', proto=21)
+        //   pkt = ether/ip
+        val nonDhcpBcastPkt = """
+            ffffffffffff020304050607080045000014000100004015b92bc0a80101ffffffff
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+                apfFilter.mApfVersionSupported,
+                program,
+                HexDump.hexStringToByteArray(nonDhcpBcastPkt),
+                PASSED_ETHER_OUR_SRC_MAC
+        )
+    }
+
+    @Test
     fun testIPv4MulticastPacketFilter() {
         val apfConfig = getDefaultConfig()
         apfConfig.multicastFilter = true
@@ -638,46 +700,267 @@
     }
 
     @Test
-    fun testIPv6MulticastPacketFilter() {
+    fun testIPv4TcpKeepaliveFilter() {
+        val srcAddr = byteArrayOf(10, 0, 0, 5)
+        val dstAddr = byteArrayOf(10, 0, 0, 6)
+        val srcPort = 12345
+        val dstPort = 54321
+        val seqNum = 2123456789
+        val ackNum = 1234567890
+
+        // src: 10.0.0.5:12345
+        // dst: 10.0.0.6:54321
+        val parcel = TcpKeepalivePacketDataParcelable()
+        parcel.srcAddress = InetAddress.getByAddress(srcAddr).address
+        parcel.srcPort = srcPort
+        parcel.dstAddress = InetAddress.getByAddress(dstAddr).address
+        parcel.dstPort = dstPort
+        parcel.seq = seqNum
+        parcel.ack = ackNum
+
         val apfConfig = getDefaultConfig()
         apfConfig.multicastFilter = true
+        apfConfig.ieee802_3Filter = true
         val apfFilter = getApfFilter(apfConfig)
         consumeInstalledProgram(ipClientCallback, installCnt = 2)
-        val lp = LinkProperties()
-        for (addr in hostIpv6Addresses) {
-            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
-        }
-        apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        apfFilter.addTcpKeepalivePacketFilter(1, parcel)
+        var program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
 
-        // Using scapy to generate non ICMPv6 sent to ff00::/8 (multicast prefix) packet:
+        // Drop IPv4 keepalive ack
+        // Using scapy to generate IPv4 TCP keepalive ack packet with seq + 1:
         // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff00::1", nh=59)
-        // pkt = eth/ip6
-        val nonIcmpv6McastPkt = """
-            ffffffffffff00112233445586dd6000000000003b4020010000000000000200001a11223344ff00000
-            0000000000000000000000000
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // tcp = TCP(sport=54321, dport=12345, flags="A", seq=1234567890, ack=2123456790)
+        // pkt = eth/ip/tcp
+        val keepaliveAckPkt = """
+            01020304050600010203040508004500002800010000400666c50a0000060a000005d4313039499602d2
+            7e916116501020004b4f0000
         """.replace("\\s+".toRegex(), "").trim()
         verifyProgramRun(
             APF_VERSION_6,
             program,
-            HexDump.hexStringToByteArray(nonIcmpv6McastPkt),
-            DROPPED_IPV6_NON_ICMP_MULTICAST
+            HexDump.hexStringToByteArray(keepaliveAckPkt),
+            DROPPED_IPV4_KEEPALIVE_ACK
         )
 
-        // Using scapy to generate non ICMPv6 unicast packet:
+        // Pass IPv4 non-keepalive ack from the same source address
+        // Using scapy to generate IPv4 TCP non-keepalive ack from the same source address:
         // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:5566", nh=59)
-        // pkt = eth/ip6
-        val nonIcmpv6UcastPkt = """
-            ffffffffffff00112233445586dd6000000000003b4020010000000000000200001a112233442001000
-            0000000000200001a33445566
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // tcp = TCP(sport=54321, dport=12345, flags="A", seq=1234567990, ack=2123456789)
+        // pkt = eth/ip/tcp
+        val nonKeepaliveAckPkt1 = """
+            01020304050600010203040508004500002800010000400666c50a0000060a000005d431303949960336
+            7e916115501020004aec0000
         """.replace("\\s+".toRegex(), "").trim()
         verifyProgramRun(
             APF_VERSION_6,
             program,
-            HexDump.hexStringToByteArray(nonIcmpv6UcastPkt),
-            PASSED_IPV6_UNICAST_NON_ICMP
+            HexDump.hexStringToByteArray(nonKeepaliveAckPkt1),
+            PASSED_IPV4_UNICAST
+        )
+
+        // Pass IPv4 non-keepalive ack from the same source address
+        // Using scapy to generate IPv4 TCP non-keepalive ack from the same source address:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // tcp = TCP(sport=54321, dport=12345, flags="A", seq=1234567890, ack=2123456790)
+        // payload = Raw(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09')
+        // pkt = eth/ip/tcp/payload
+        val nonKeepaliveAckPkt2 = """
+            01020304050600010203040508004500003200010000400666bb0a0000060a000005d4313039499602d27
+            e91611650102000372c000000010203040506070809
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(nonKeepaliveAckPkt2),
+            PASSED_IPV4_UNICAST
+        )
+
+        // Pass IPv4 keepalive ack from another address
+        // Using scapy to generate IPv4 TCP keepalive ack from another address:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.7', dst='10.0.0.5')
+        // tcp = TCP(sport=23456, dport=65432, flags="A", seq=2123456780, ack=1123456789)
+        // pkt = eth/ip/tcp
+        val otherSrcKeepaliveAck = """
+            01020304050600010203040508004500002800010000400666c40a0000070a0000055ba0ff987e91610c4
+            2f697155010200066e60000
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(otherSrcKeepaliveAck),
+            PASSED_IPV4_UNICAST
+        )
+
+        // test IPv4 packets when TCP keepalive filter is removed
+        apfFilter.removeKeepalivePacketFilter(1)
+        program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(keepaliveAckPkt),
+            PASSED_IPV4_UNICAST
+        )
+
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(otherSrcKeepaliveAck),
+            PASSED_IPV4_UNICAST
+        )
+    }
+
+    @Test
+    fun testIPv4NattKeepaliveFilter() {
+        val srcAddr = byteArrayOf(10, 0, 0, 5)
+        val dstAddr = byteArrayOf(10, 0, 0, 6)
+        val srcPort = 1024
+        val dstPort = 4500
+
+        // src: 10.0.0.5:1024
+        // dst: 10.0.0.6:4500
+        val parcel = NattKeepalivePacketDataParcelable()
+        parcel.srcAddress = InetAddress.getByAddress(srcAddr).address
+        parcel.srcPort = srcPort
+        parcel.dstAddress = InetAddress.getByAddress(dstAddr).address
+        parcel.dstPort = dstPort
+
+        val apfConfig = getDefaultConfig()
+        apfConfig.multicastFilter = true
+        apfConfig.ieee802_3Filter = true
+        val apfFilter = getApfFilter(apfConfig)
+        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfFilter.addNattKeepalivePacketFilter(1, parcel)
+        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+
+        // Drop IPv4 keepalive response packet
+        // Using scapy to generate IPv4 NAT-T keepalive ack packet with payload 0xff:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // udp = UDP(sport=4500, dport=1024)
+        // payload = NAT_KEEPALIVE(nat_keepalive=0xff)
+        // pkt = eth/ip/udp/payload
+        val validNattPkt = """
+            01020304050600010203040508004500001d00010000401166c50a0000060a000005119404000009d73cff
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(validNattPkt),
+            DROPPED_IPV4_NATT_KEEPALIVE
+        )
+
+        // Pass IPv4 keepalive response packet with 0xfe payload
+        // Using scapy to generate IPv4 NAT-T keepalive ack packet with payload 0xfe:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // udp = UDP(sport=4500, dport=1024)
+        // payload = NAT_KEEPALIVE(nat_keepalive=0xfe)
+        // pkt = eth/ip/udp/payload
+        val invalidNattPkt = """
+            01020304050600010203040508004500001d00010000401166c50a0000060a000005119404000009d83cfe
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(invalidNattPkt),
+            PASSED_IPV4_UNICAST
+        )
+
+        // Pass IPv4 non-keepalive response packet from the same source address
+        // Using scapy to generate IPv4 NAT-T keepalive ack packet with payload 0xfe:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // udp = UDP(sport=4500, dport=1024)
+        // payload = Raw(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09')
+        // pkt = eth/ip/udp/payload
+        val nonNattPkt = """
+            01020304050600010203040508004500002600010000401166bc0a0000060a000005119404000012c2120
+            0010203040506070809
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(nonNattPkt),
+            PASSED_IPV4_UNICAST
+        )
+
+        // Pass IPv4 non-keepalive response packet from other source address
+        // Using scapy to generate IPv4 NAT-T keepalive ack packet with payload 0xfe:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.7', dst='10.0.0.5')
+        // udp = UDP(sport=4500, dport=1024)
+        // payload = Raw(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09')
+        // pkt = eth/ip/udp/payload
+        val otherSrcNonNattPkt = """
+            01020304050600010203040508004500002600010000401166bb0a0000070a000005119404000012c2110
+            0010203040506070809
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(otherSrcNonNattPkt),
+            PASSED_IPV4_UNICAST
+        )
+    }
+
+    @Test
+    fun testIPv4TcpPort7Filter() {
+        val apfFilter = getApfFilter()
+        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+
+        // Drop IPv4 TCP port 7 packet
+        // Using scapy to generate IPv4 TCP port 7 packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5')
+        // tcp = TCP(dport=7)
+        // pkt = eth/ip/tcp
+        val tcpPort7Pkt = """
+            01020304050600010203040508004500002800010000400666c50a0000060a00000500140007000000000
+            0000000500220007bbd0000
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(tcpPort7Pkt),
+            DROPPED_IPV4_TCP_PORT7_UNICAST
+        )
+
+        // Pass IPv4 TCP initial fragment packet
+        // Using scapy to generate IPv4 TCP initial fragment packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5', flags=1, frag=0)
+        // tcp = TCP()
+        // pkt = eth/ip/tcp
+        val initialFragmentTcpPkt = """
+            01020304050600010203040508004500002800012000400646c50a0000060a00000500140050000000000
+            0000000500220007b740000
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(initialFragmentTcpPkt),
+            PASSED_IPV4
+        )
+
+        // Pass IPv4 TCP fragment packet
+        // Using scapy to generate IPv4 TCP fragment packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // ip = IP(src='10.0.0.6', dst='10.0.0.5', flags=1, frag=100)
+        // tcp = TCP()
+        // pkt = eth/ip/tcp
+        val fragmentTcpPkt = """
+            01020304050600010203040508004500002800012064400646610a0000060a00000500140050000000000
+            0000000500220007b740000
+        """.replace("\\s+".toRegex(), "").trim()
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(fragmentTcpPkt),
+            PASSED_IPV4
         )
     }
 
@@ -1739,6 +2022,63 @@
         }
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testRegisterOffloadEngine() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.shouldHandleMdnsOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
+        verify(nsdManager).registerOffloadEngine(
+                eq(ifParams.name),
+                anyLong(),
+                anyLong(),
+                any(),
+                captor.capture()
+        )
+        val offloadEngine = captor.value
+        val info1 = OffloadServiceInfo(
+                OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+                listOf(),
+                "Android_test.local",
+                byteArrayOf(0x01, 0x02, 0x03, 0x04),
+                0,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        val info2 = OffloadServiceInfo(
+                OffloadServiceInfo.Key("TestServiceName2", "_advertisertest._tcp"),
+                listOf(),
+                "Android_test.local",
+                byteArrayOf(0x01, 0x02, 0x03, 0x04),
+                0,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        val updatedInfo1 = OffloadServiceInfo(
+                OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+                listOf(),
+                "Android_test.local",
+                byteArrayOf(),
+                0,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        handler.post { offloadEngine.onOffloadServiceUpdated(info1) }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        assertContentEquals(listOf(info1), apfFilter.mOffloadServiceInfos)
+        handler.post { offloadEngine.onOffloadServiceUpdated(info2) }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        assertContentEquals(listOf(info1, info2), apfFilter.mOffloadServiceInfos)
+        handler.post { offloadEngine.onOffloadServiceUpdated(updatedInfo1) }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        assertContentEquals(listOf(info2, updatedInfo1), apfFilter.mOffloadServiceInfos)
+        handler.post { offloadEngine.onOffloadServiceRemoved(updatedInfo1) }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        assertContentEquals(listOf(info2), apfFilter.mOffloadServiceInfos)
+
+        handler.post { apfFilter.shutdown() }
+        handlerThread.waitForIdle(TIMEOUT_MS)
+        verify(nsdManager).unregisterOffloadEngine(any())
+    }
+
     @Test
     fun testApfProgramUpdate() {
         val apfFilter = getApfFilter()
@@ -1794,4 +2134,13 @@
         val program = programCaptor.allValues.first()
         assertContentEquals(ByteArray(4096) { 0 }, program)
     }
+
+    @Test
+    fun testApfFilterResumeWillCleanUpTheApfMemoryRegion() {
+        val apfFilter = getApfFilter()
+        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfFilter.resume()
+        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        assertContentEquals(ByteArray(4096) { 0 }, program)
+    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
index 60f0ca8..98b2a42 100644
--- a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
+++ b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
@@ -512,7 +512,7 @@
                 program
         )
         assertContentEquals(
-                listOf("0: drop        counter=46"),
+                listOf("0: drop        counter=47"),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
@@ -561,14 +561,14 @@
                 byteArrayOf(
                         encodeInstruction(opcode = 14, immLength = 2, register = 1), 1, 0
                 ) + largeByteArray + byteArrayOf(
-                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 13
+                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 9
                 ),
                 program
         )
         assertContentEquals(
                 listOf(
                         "0: data        256, " + "01".repeat(256),
-                        "259: debugbuf    size=1549"
+                        "259: debugbuf    size=1545"
                 ),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
@@ -854,7 +854,7 @@
                 .generate()
         assertContentEquals(listOf(
                 "0: data        9, 112233445566778899",
-                "12: debugbuf    size=1776",
+                "12: debugbuf    size=1772",
                 "16: allocate    18",
                 "20: datacopy    src=3, len=6",
                 "23: datacopy    src=4, len=3",
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 3db7a7f..14e2122 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -17,6 +17,7 @@
 package android.net.apf;
 
 import static android.net.apf.ApfCounterTracker.Counter.getCounterEnumFromOffset;
+import static android.net.apf.ApfTestHelpers.TIMEOUT_MS;
 import static android.net.apf.ApfTestHelpers.consumeInstalledProgram;
 import static android.net.apf.ApfTestHelpers.DROP;
 import static android.net.apf.ApfTestHelpers.MIN_PKT_SIZE;
@@ -41,7 +42,6 @@
 import static android.system.OsConstants.ETH_P_IPV6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_IPV6;
-import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_STREAM;
 
@@ -57,12 +57,11 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -72,14 +71,14 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 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.BaseApfGenerator.IllegalInstructionException;
 import android.net.ip.IpClient;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.stats.connectivity.NetworkQuirkEvent;
@@ -109,6 +108,7 @@
 import com.android.testutils.ConcurrentUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -130,6 +130,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -142,6 +143,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Tests for APF program generator and interpreter.
@@ -178,10 +180,9 @@
     @Mock private IpClient.IpClientCallbacksWrapper mIpClientCb;
     @GuardedBy("mApfFilterCreated")
     private final ArrayList<AndroidPacketFilter> mApfFilterCreated = new ArrayList<>();
-    @GuardedBy("mThreadsToBeCleared")
-    private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
-
     private FileDescriptor mWriteSocket;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
     private long mCurrentTimeMs;
 
     @Before
@@ -190,39 +191,22 @@
         doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class);
         doReturn(mApfSessionInfoMetrics).when(mDependencies).getApfSessionInfoMetrics();
         doReturn(mIpClientRaInfoMetrics).when(mDependencies).getIpClientRaInfoMetrics();
+        FileDescriptor readSocket = new FileDescriptor();
+        mWriteSocket = new FileDescriptor();
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
+        doReturn(readSocket).when(mDependencies).createPacketReaderSocket(anyInt());
+        mCurrentTimeMs = SystemClock.elapsedRealtime();
+        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
+        doReturn(true).when(mIpClientCb).installPacketFilter(any());
         doAnswer((invocation) -> {
             synchronized (mApfFilterCreated) {
                 mApfFilterCreated.add(invocation.getArgument(0));
             }
             return null;
         }).when(mDependencies).onApfFilterCreated(any());
-        doAnswer((invocation) -> {
-            synchronized (mThreadsToBeCleared) {
-                mThreadsToBeCleared.add(invocation.getArgument(0));
-            }
-            return null;
-        }).when(mDependencies).onThreadCreated(any());
-        FileDescriptor readSocket = new FileDescriptor();
-        mWriteSocket = new FileDescriptor();
-        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
-        doReturn(readSocket).when(mDependencies).createRaReaderSocket(anyInt());
-        mCurrentTimeMs = SystemClock.elapsedRealtime();
-        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
-        doReturn(true).when(mIpClientCb).installPacketFilter(any());
-    }
-
-    private void quitThreads() throws Exception {
-        ConcurrentUtils.quitThreads(
-                THREAD_QUIT_MAX_RETRY_COUNT,
-                false /* interrupt */,
-                TIMEOUT_MS,
-                () -> {
-                    synchronized (mThreadsToBeCleared) {
-                        final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared);
-                        mThreadsToBeCleared.clear();
-                        return ret;
-                    }
-                });
+        mHandlerThread = new HandlerThread("ApfTestThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
     }
 
     private void shutdownApfFilters() throws Exception {
@@ -233,26 +217,22 @@
                 mApfFilterCreated.clear();
                 return ret;
             }
-        }, (apf) -> {
-            apf.shutdown();
-        });
+        }, (apf) -> mHandler.post(apf::shutdown));
         synchronized (mApfFilterCreated) {
             assertEquals("ApfFilters did not fully shutdown.",
                     0, mApfFilterCreated.size());
         }
-        // It's necessary to wait until all ReceiveThreads have finished running because
-        // clearInlineMocks clears all Mock objects, including some privilege frameworks
-        // required by logStats, at the end of ReceiveThread#run.
-        quitThreads();
     }
 
     @After
     public void tearDown() throws Exception {
+        IoUtils.closeQuietly(mWriteSocket);
         shutdownApfFilters();
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
         // Clear mocks to prevent from stubs holding instances and cause memory leaks.
         Mockito.framework().clearInlineMocks();
-        IoUtils.closeQuietly(mWriteSocket);
-        mWriteSocket = null;
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
     }
 
     private static final String TAG = "ApfTest";
@@ -267,7 +247,6 @@
     private static final int MIN_RDNSS_LIFETIME_SEC = 0;
     private static final int MIN_METRICS_SESSION_DURATIONS_MS = 300_000;
 
-    private static final int TIMEOUT_MS = 1000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 500;
     private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3;
 
@@ -1035,10 +1014,19 @@
     }
 
     private void pretendPacketReceived(byte[] packet)
-            throws IOException, ErrnoException {
+            throws InterruptedIOException, ErrnoException {
         Os.write(mWriteSocket, packet, 0, packet.length);
     }
 
+    private ApfFilter getApfFilter(ApfFilter.ApfConfiguration config) {
+        AtomicReference<ApfFilter> apfFilter = new AtomicReference<>();
+        mHandler.post(() ->
+                apfFilter.set(new ApfFilter(mHandler, mContext, config, TEST_PARAMS,
+                        mIpClientCb, mNetworkQuirkMetrics, mDependencies)));
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
+        return apfFilter.get();
+    }
+
     /**
      * Generate APF program, run pcap file though APF filter, then check all the packets in the file
      * should be dropped.
@@ -1056,8 +1044,7 @@
         config.apfRamSize = 1700;
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS,
-                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
         apfFilter.setLinkProperties(lp);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
@@ -1084,25 +1071,12 @@
     private static final int IP_HEADER_OFFSET = ETH_HEADER_LEN;
 
     private static final int IPV4_HEADER_LEN          = 20;
-    private static final int IPV4_TOTAL_LENGTH_OFFSET = IP_HEADER_OFFSET + 2;
     private static final int IPV4_PROTOCOL_OFFSET     = IP_HEADER_OFFSET + 9;
-    private static final int IPV4_SRC_ADDR_OFFSET     = IP_HEADER_OFFSET + 12;
     private static final int IPV4_DEST_ADDR_OFFSET    = IP_HEADER_OFFSET + 16;
 
-    private static final int IPV4_TCP_HEADER_LEN           = 20;
     private static final int IPV4_TCP_HEADER_OFFSET        = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
-    private static final int IPV4_TCP_SRC_PORT_OFFSET      = IPV4_TCP_HEADER_OFFSET + 0;
-    private static final int IPV4_TCP_DEST_PORT_OFFSET     = IPV4_TCP_HEADER_OFFSET + 2;
-    private static final int IPV4_TCP_SEQ_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 4;
-    private static final int IPV4_TCP_ACK_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 8;
-    private static final int IPV4_TCP_HEADER_LENGTH_OFFSET = IPV4_TCP_HEADER_OFFSET + 12;
-    private static final int IPV4_TCP_HEADER_FLAG_OFFSET   = IPV4_TCP_HEADER_OFFSET + 13;
 
     private static final int IPV4_UDP_HEADER_OFFSET    = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
-    private static final int IPV4_UDP_SRC_PORT_OFFSET  = IPV4_UDP_HEADER_OFFSET + 0;
-    private static final int IPV4_UDP_DEST_PORT_OFFSET = IPV4_UDP_HEADER_OFFSET + 2;
-    private static final int IPV4_UDP_LENGTH_OFFSET    = IPV4_UDP_HEADER_OFFSET + 4;
-    private static final int IPV4_UDP_PAYLOAD_OFFSET   = IPV4_UDP_HEADER_OFFSET + 8;
     private static final byte[] IPV4_BROADCAST_ADDRESS =
             {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
 
@@ -1112,10 +1086,6 @@
     private static final int IPV6_SRC_ADDR_OFFSET        = IP_HEADER_OFFSET + 8;
     private static final int IPV6_DEST_ADDR_OFFSET       = IP_HEADER_OFFSET + 24;
     private static final int IPV6_PAYLOAD_OFFSET = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
-    private static final int IPV6_TCP_SRC_PORT_OFFSET    = IPV6_PAYLOAD_OFFSET + 0;
-    private static final int IPV6_TCP_DEST_PORT_OFFSET   = IPV6_PAYLOAD_OFFSET + 2;
-    private static final int IPV6_TCP_SEQ_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 4;
-    private static final int IPV6_TCP_ACK_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 8;
     // The IPv6 all nodes address ff02::1
     private static final byte[] IPV6_ALL_NODES_ADDRESS   =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
@@ -1129,25 +1099,18 @@
     private static final int ICMP6_TYPE_OFFSET           = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
     private static final int ICMP6_ROUTER_SOLICITATION   = 133;
     private static final int ICMP6_ROUTER_ADVERTISEMENT  = 134;
-    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
     private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
 
     private static final int ICMP6_RA_HEADER_LEN = 16;
     private static final int ICMP6_RA_CHECKSUM_OFFSET =
             IP_HEADER_OFFSET + IPV6_HEADER_LEN + 2;
-    private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 6;
     private static final int ICMP6_RA_REACHABLE_TIME_OFFSET =
             IP_HEADER_OFFSET + IPV6_HEADER_LEN + 8;
-    private static final int ICMP6_RA_RETRANSMISSION_TIMER_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 12;
     private static final int ICMP6_RA_OPTION_OFFSET =
             IP_HEADER_OFFSET + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
 
     private static final int ICMP6_PREFIX_OPTION_TYPE                      = 3;
     private static final int ICMP6_PREFIX_OPTION_LEN                       = 32;
-    private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET     = 4;
-    private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
 
     // From RFC6106: Recursive DNS Server option
     private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
@@ -1158,8 +1121,6 @@
     private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
     // Above three options all have the same format:
     private static final int ICMP6_4_BYTE_OPTION_LEN      = 8;
-    private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
-    private static final int ICMP6_4_BYTE_LIFETIME_LEN    = 4;
 
     private static final int UDP_HEADER_LEN              = 8;
     private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22;
@@ -1197,7 +1158,6 @@
     private static final byte[] IPV4_MDNS_MULTICAST_ADDR = {(byte) 224, 0, 0, (byte) 251};
     private static final byte[] IPV6_MDNS_MULTICAST_ADDR =
             {(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfb};
-    private static final int IPV6_UDP_DEST_PORT_OFFSET = IPV6_PAYLOAD_OFFSET + 2;
     private static final int MDNS_UDP_PORT = 5353;
 
     private static void setIpv4VersionFields(ByteBuffer packet) {
@@ -1232,8 +1192,7 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS,
-                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
@@ -1288,8 +1247,7 @@
     @Test
     public void testApfFilterIPv6() throws Exception {
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty IPv6 packet is passed
@@ -1702,8 +1660,7 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
@@ -1762,10 +1719,9 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         clearInvocations(mIpClientCb);
-        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter2 = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-        apfFilter.setLinkProperties(lp);
+        apfFilter2.setLinkProperties(lp);
         program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
@@ -1790,8 +1746,7 @@
 
     private void doTestApfFilterMulticastPingWhileDozing(boolean isLightDozing) throws Exception {
         final ApfConfiguration configuration = getDefaultConfig();
-        final ApfFilter apfFilter = new ApfFilter(mContext, configuration, TEST_PARAMS,
-                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(configuration);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
@@ -1847,8 +1802,7 @@
     @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testApfFilter802_3() throws Exception {
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
@@ -1866,8 +1820,7 @@
 
         // Now turn on the filter
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        apfFilter = getApfFilter(config);
         program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IEEE802.3 frame is dropped
@@ -1892,8 +1845,7 @@
         final int[] ipv4Ipv6BlackList = {ETH_P_IP, ETH_P_IPV6};
 
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
@@ -1911,8 +1863,7 @@
 
         // Now add IPv4 to the black list
         config.ethTypeBlackList = ipv4BlackList;
-        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        apfFilter = getApfFilter(config);
         program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
@@ -1925,8 +1876,7 @@
 
         // Now let us have both IPv4 and IPv6 in the black list
         config.ethTypeBlackList = ipv4Ipv6BlackList;
-        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        apfFilter = getApfFilter(config);
         program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
@@ -1964,8 +1914,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
@@ -2012,281 +1961,9 @@
         return packet.array();
     }
 
-    private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 5};
-    private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 6};
-    private static final byte[] IPV4_ANOTHER_ADDR = {10, 0 , 0, 7};
-    private static final byte[] IPV6_KEEPALIVE_SRC_ADDR =
-            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf1};
-    private static final byte[] IPV6_KEEPALIVE_DST_ADDR =
-            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf2};
     private static final byte[] IPV6_ANOTHER_ADDR =
             {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf5};
 
-    @Test
-    public void testApfFilterKeepaliveAck() throws Exception {
-        final ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-        byte[] program;
-        final int srcPort = 12345;
-        final int dstPort = 54321;
-        final int seqNum = 2123456789;
-        final int ackNum = 1234567890;
-        final int anotherSrcPort = 23456;
-        final int anotherDstPort = 65432;
-        final int anotherSeqNum = 2123456780;
-        final int anotherAckNum = 1123456789;
-        final int slot1 = 1;
-        final int slot2 = 2;
-        final int window = 14480;
-        final int windowScale = 4;
-
-        // src: 10.0.0.5, port: 12345
-        // dst: 10.0.0.6, port: 54321
-        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
-        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
-
-        final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
-        parcel.srcAddress = srcAddr.getAddress();
-        parcel.srcPort = srcPort;
-        parcel.dstAddress = dstAddr.getAddress();
-        parcel.dstPort = dstPort;
-        parcel.seq = seqNum;
-        parcel.ack = ackNum;
-
-        apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-
-        // Verify IPv4 keepalive ack packet is dropped
-        // src: 10.0.0.6, port: 54321
-        // dst: 10.0.0.5, port: 12345
-        assertDrop(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-        // Verify IPv4 non-keepalive ack packet from the same source address is passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 10 /* dataLength */));
-        // Verify IPv4 packet from another address is passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                        anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-
-        // Remove IPv4 keepalive filter
-        apfFilter.removeKeepalivePacketFilter(slot1);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-
-        try {
-            // src: 2404:0:0:0:0:0:faf1, port: 12345
-            // dst: 2404:0:0:0:0:0:faf2, port: 54321
-            srcAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_SRC_ADDR);
-            dstAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_DST_ADDR);
-
-            final TcpKeepalivePacketDataParcelable ipv6Parcel =
-                    new TcpKeepalivePacketDataParcelable();
-            ipv6Parcel.srcAddress = srcAddr.getAddress();
-            ipv6Parcel.srcPort = srcPort;
-            ipv6Parcel.dstAddress = dstAddr.getAddress();
-            ipv6Parcel.dstPort = dstPort;
-            ipv6Parcel.seq = seqNum;
-            ipv6Parcel.ack = ackNum;
-
-            apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
-            program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-
-            // Verify IPv6 keepalive ack packet is dropped
-            // src: 2404:0:0:0:0:0:faf2, port: 54321
-            // dst: 2404:0:0:0:0:0:faf1, port: 12345
-            assertDrop(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1));
-            // Verify IPv6 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum));
-            // Verify IPv6 packet from another address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum));
-
-            // Remove IPv6 keepalive filter
-            apfFilter.removeKeepalivePacketFilter(slot1);
-
-            // Verify multiple filters
-            apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-            apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
-            program = consumeInstalledProgram(mIpClientCb, 3 /* installCnt */);
-
-            // Verify IPv4 keepalive ack packet is dropped
-            // src: 10.0.0.6, port: 54321
-            // dst: 10.0.0.5, port: 12345
-            assertDrop(program,
-                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-            // Verify IPv4 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
-            // Verify IPv4 packet from another address is passed
-            assertPass(program,
-                    ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-
-            // Verify IPv6 keepalive ack packet is dropped
-            // src: 2404:0:0:0:0:0:faf2, port: 54321
-            // dst: 2404:0:0:0:0:0:faf1, port: 12345
-            assertDrop(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1));
-            // Verify IPv6 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum));
-            // Verify IPv6 packet from another address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum));
-
-            // Remove keepalive filters
-            apfFilter.removeKeepalivePacketFilter(slot1);
-            apfFilter.removeKeepalivePacketFilter(slot2);
-        } catch (UnsupportedOperationException e) {
-            // TODO: support V6 packets
-        }
-
-        // Verify IPv4, IPv6 packets are passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-        assertPass(program,
-                ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1));
-        assertPass(program,
-                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, srcPort,
-                        dstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-        assertPass(program,
-                ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, srcPort,
-                        dstPort, anotherSeqNum, anotherAckNum));
-    }
-
-    private static byte[] ipv4TcpPacket(byte[] sip, byte[] dip, int sport,
-            int dport, int seq, int ack, int dataLength) {
-        final int totalLength = dataLength + IPV4_HEADER_LEN + IPV4_TCP_HEADER_LEN;
-
-        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
-
-        // Ethertype and IPv4 header
-        setIpv4VersionFields(packet);
-        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_TCP);
-        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
-        packet.putShort(IPV4_TCP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV4_TCP_DEST_PORT_OFFSET, (short) dport);
-        packet.putInt(IPV4_TCP_SEQ_NUM_OFFSET, seq);
-        packet.putInt(IPV4_TCP_ACK_NUM_OFFSET, ack);
-
-        // TCP header length 5(20 bytes), reserved 3 bits, NS=0
-        packet.put(IPV4_TCP_HEADER_LENGTH_OFFSET, (byte) 0x50);
-        // TCP flags: ACK set
-        packet.put(IPV4_TCP_HEADER_FLAG_OFFSET, (byte) 0x10);
-        return packet.array();
-    }
-
-    private static byte[] ipv6TcpPacket(byte[] sip, byte[] tip, int sport,
-            int dport, int seq, int ack) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        setIpv6VersionFields(packet);
-        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_TCP);
-        put(packet, IPV6_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV6_DEST_ADDR_OFFSET, tip);
-        packet.putShort(IPV6_TCP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV6_TCP_DEST_PORT_OFFSET, (short) dport);
-        packet.putInt(IPV6_TCP_SEQ_NUM_OFFSET, seq);
-        packet.putInt(IPV6_TCP_ACK_NUM_OFFSET, ack);
-        return packet.array();
-    }
-
-    @Test
-    public void testApfFilterNattKeepalivePacket() throws Exception {
-        final ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-        byte[] program;
-        final int srcPort = 1024;
-        final int dstPort = 4500;
-        final int slot1 = 1;
-        // NAT-T keepalive
-        final byte[] kaPayload = {(byte) 0xff};
-        final byte[] nonKaPayload = {(byte) 0xfe};
-
-        // src: 10.0.0.5, port: 1024
-        // dst: 10.0.0.6, port: 4500
-        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
-        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
-
-        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
-        parcel.srcAddress = srcAddr.getAddress();
-        parcel.srcPort = srcPort;
-        parcel.dstAddress = dstAddr.getAddress();
-        parcel.dstPort = dstPort;
-
-        apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-
-        // Verify IPv4 keepalive packet is dropped
-        // src: 10.0.0.6, port: 4500
-        // dst: 10.0.0.5, port: 1024
-        byte[] pkt = ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR,
-                    IPV4_KEEPALIVE_SRC_ADDR, dstPort, srcPort, 1 /* dataLength */);
-        System.arraycopy(kaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, kaPayload.length);
-        assertDrop(program, pkt);
-
-        // Verify a packet with payload length 1 byte but it is not 0xff will pass the filter.
-        System.arraycopy(nonKaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, nonKaPayload.length);
-        assertPass(program, pkt);
-
-        // Verify IPv4 non-keepalive response packet from the same source address is passed
-        assertPass(program,
-                ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, 10 /* dataLength */));
-
-        // Verify IPv4 non-keepalive response packet from other source address is passed
-        assertPass(program,
-                ipv4UdpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, 10 /* dataLength */));
-
-        apfFilter.removeKeepalivePacketFilter(slot1);
-    }
-
-    private static byte[] ipv4UdpPacket(byte[] sip, byte[] dip, int sport,
-            int dport, int dataLength) {
-        final int totalLength = dataLength + IPV4_HEADER_LEN + UDP_HEADER_LEN;
-        final int udpLength = UDP_HEADER_LEN + dataLength;
-        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
-
-        // Ethertype and IPv4 header
-        setIpv4VersionFields(packet);
-        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_UDP);
-        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
-        packet.putShort(IPV4_UDP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV4_UDP_DEST_PORT_OFFSET, (short) dport);
-        packet.putShort(IPV4_UDP_LENGTH_OFFSET, (short) udpLength);
-
-        return packet.array();
-    }
-
     private static class RaPacketBuilder {
         final ByteArrayOutputStream mPacket = new ByteArrayOutputStream();
         int mFlowLabel = 0x12345;
@@ -2467,8 +2144,7 @@
     @Test
     public void testRaToString() throws Exception {
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
 
         byte[] packet = buildLargeRa();
         ApfFilter.Ra ra = apfFilter.new Ra(packet, packet.length);
@@ -2541,8 +2217,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int ROUTER_LIFETIME = 1000;
@@ -2626,8 +2301,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
@@ -2667,8 +2341,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
@@ -2740,8 +2413,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2761,8 +2433,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2776,8 +2447,7 @@
 
     @Test
     public void testMatchedRaUpdatesLifetime() throws Exception {
-        final ApfFilter apfFilter = new ApfFilter(mContext, getDefaultConfig(), TEST_PARAMS,
-                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(getDefaultConfig());
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an RA and build an APF program
@@ -2801,8 +2471,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         // Template packet:
         // Frame 1: 150 bytes on wire (1200 bits), 150 bytes captured (1200 bits)
@@ -2866,8 +2535,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -2895,8 +2563,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -2931,8 +2598,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -2960,8 +2626,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -2997,8 +2662,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -3030,8 +2694,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -3069,8 +2732,7 @@
         // configure accept_ra_min_lft
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
@@ -3142,8 +2804,8 @@
     public void testInstallPacketFilterFailure() throws Exception {
         doReturn(false).when(mIpClientCb).installPacketFilter(any());
         final ApfConfiguration config = getDefaultConfig();
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
+
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
         verify(mNetworkQuirkMetrics).statsWrite();
         reset(mNetworkQuirkMetrics);
@@ -3160,8 +2822,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.apfVersionSupported = 2;
         config.apfRamSize = 512;
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final byte[] ra = buildLargeRa();
         pretendPacketReceived(ra);
@@ -3172,12 +2833,14 @@
     }
 
     @Test
-    public void testGenerateApfProgramException() throws Exception {
+    public void testGenerateApfProgramException() {
         final ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = spy(new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies));
+        ApfFilter apfFilter = getApfFilter(config);
+        // Simulate exception during installNewProgramLocked() by mocking
+        // mDependencies.elapsedRealtime() to throw an exception (this method doesn't throw in
+        // real-world scenarios).
+        doThrow(new IllegalStateException("test exception")).when(mDependencies).elapsedRealtime();
         synchronized (apfFilter) {
-            when(apfFilter.emitPrologueLocked()).thenThrow(new IllegalStateException("test"));
             apfFilter.installNewProgramLocked();
         }
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
@@ -3192,8 +2855,7 @@
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         byte[] program = consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
         int maxProgramSize = 0;
         int numProgramUpdated = 0;
@@ -3238,7 +2900,9 @@
 
         // Write metrics data to statsd pipeline when shutdown.
         doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
-        apfFilter.shutdown();
+        mHandler.post(apfFilter::shutdown);
+        IoUtils.closeQuietly(mWriteSocket);
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
         verify(mApfSessionInfoMetrics).setVersion(4);
         verify(mApfSessionInfoMetrics).setMemorySize(4096);
 
@@ -3270,8 +2934,7 @@
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
@@ -3327,7 +2990,9 @@
 
         // Write metrics data to statsd pipeline when shutdown.
         doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
-        apfFilter.shutdown();
+        mHandler.post(apfFilter::shutdown);
+        IoUtils.closeQuietly(mWriteSocket);
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
 
         // Verify each metric fields in IpClientRaInfoMetrics.
         verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(6);
@@ -3348,20 +3013,20 @@
 
         // Verify no metrics data written to statsd for duration less than durationTimeMs.
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
-        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = getApfFilter(config);
         doReturn(startTimeMs + durationTimeMs - 1).when(mDependencies).elapsedRealtime();
-        apfFilter.shutdown();
+        mHandler.post(apfFilter::shutdown);
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
         verify(mApfSessionInfoMetrics, never()).statsWrite();
         verify(mIpClientRaInfoMetrics, never()).statsWrite();
 
         // Verify metrics data written to statsd for duration greater than or equal to
         // durationTimeMs.
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
-        final ApfFilter apfFilter2 = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
-                mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter2 = getApfFilter(config);
         doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
-        apfFilter2.shutdown();
+        mHandler.post(apfFilter2::shutdown);
+        HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
         verify(mApfSessionInfoMetrics).statsWrite();
         verify(mIpClientRaInfoMetrics).statsWrite();
     }
diff --git a/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
new file mode 100644
index 0000000..1ee0ca5
--- /dev/null
+++ b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.ip
+
+import android.net.MacAddress
+import android.net.ip.ConnectivityPacketTracker.Dependencies
+import android.os.Handler
+import android.os.HandlerThread
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.system.OsConstants.SOCK_STREAM
+import android.util.LocalLog
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.InterfaceParams
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.waitForIdle
+import java.io.FileDescriptor
+import java.io.InterruptedIOException
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import libcore.io.IoUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.MockitoAnnotations
+
+/**
+ * Test for ConnectivityPacketTracker.
+ */
+@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class ConnectivityPacketTrackerTest {
+    companion object {
+        private const val TIMEOUT_MS: Long = 10000
+        private const val SLEEP_TIMEOUT_MS: Long = 500
+        private const val TEST_MAX_CAPTURE_PKT_SIZE: Int = 100
+        private const val TAG = "ConnectivityPacketTrackerTest"
+    }
+
+    private val loInterfaceParams = InterfaceParams.getByName("lo")
+    private val ifParams =
+        InterfaceParams(
+            "lo",
+            loInterfaceParams.index,
+            MacAddress.fromBytes(byteArrayOf(2, 3, 4, 5, 6, 7)),
+            loInterfaceParams.defaultMtu
+        )
+    private val writeSocket = FileDescriptor()
+    private val handlerThread by lazy {
+        HandlerThread("$TAG-handler-thread").apply { start() }
+    }
+    private val handler by lazy { Handler(handlerThread.looper) }
+    @Mock private lateinit var mDependencies: Dependencies
+    @Mock private lateinit var localLog: LocalLog
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val readSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM or SOCK_NONBLOCK, 0, writeSocket, readSocket)
+        doReturn(readSocket).`when`(mDependencies).createPacketReaderSocket(anyInt())
+        doReturn(TEST_MAX_CAPTURE_PKT_SIZE).`when`(mDependencies).maxCapturePktSize
+    }
+
+    @After
+    fun tearDown() {
+        IoUtils.closeQuietly(writeSocket)
+        handler.waitForIdle(10000)
+        Mockito.framework().clearInlineMocks()
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    @Test
+    fun testCapturePacket() {
+        val packetTracker = getConnectivityPacketTracker()
+        // Using scapy to generate ARP request packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // arp = ARP()
+        // pkt = eth/arp
+        val arpPkt = """
+            010203040506000102030405080600010800060400015c857e3c74e1c0a8012200000000000000000000
+        """.replace("\\s+".toRegex(), "").trim().uppercase()
+        val arpPktByteArray = HexDump.hexStringToByteArray(arpPkt)
+        assertEquals(0, getTotalCapturePacketCount(packetTracker))
+        assertEquals(0, getMatchedPacketCount(packetTracker, arpPkt))
+
+        // start capture packet
+        setCapture(packetTracker, true)
+
+        for (i in 1..5) {
+            pretendPacketReceive(arpPktByteArray)
+            Thread.sleep(SLEEP_TIMEOUT_MS)
+        }
+
+        assertEquals(1, getTotalCapturePacketCount(packetTracker))
+        assertEquals(5, getMatchedPacketCount(packetTracker, arpPkt))
+
+        // stop capture packet
+        setCapture(packetTracker, false)
+        assertEquals(0, getTotalCapturePacketCount(packetTracker))
+        assertEquals(0, getMatchedPacketCount(packetTracker, arpPkt))
+    }
+
+    @Test
+    fun testMaxCapturePacketSize() {
+        doReturn(3).`when`(mDependencies).maxCapturePktSize
+        val packetTracker = getConnectivityPacketTracker(mDependencies)
+
+        // Using scapy to generate ARP request packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // arp = ARP()
+        // pkt = eth/arp
+        val arpPkt = """
+            010203040506000102030405080600010800060400015c857e3c74e1c0a8012200000000000000000000
+        """.replace("\\s+".toRegex(), "").trim().uppercase()
+        val arpPktByteArray = HexDump.hexStringToByteArray(arpPkt)
+        // start capture packet
+        setCapture(packetTracker, true)
+        val pktCnt = 5
+        val pktList = ArrayList<String>()
+        for (i in 0..<pktCnt) {
+            // modify the original packet's last byte
+            val modPkt = arpPktByteArray.copyOf()
+            modPkt[modPkt.size - 1] = i.toByte()
+            pretendPacketReceive(modPkt)
+            pktList.add(HexDump.toHexString(modPkt))
+            Thread.sleep(SLEEP_TIMEOUT_MS)
+        }
+
+        // The old packets are evicted due to LruCache size
+        pktList.take(2).forEach {
+            assertEquals(0, getMatchedPacketCount(packetTracker, it))
+        }
+
+        pktList.drop(2).forEach {
+            assertEquals(1, getMatchedPacketCount(packetTracker, it))
+        }
+
+        assertEquals(mDependencies.maxCapturePktSize, getTotalCapturePacketCount(packetTracker))
+    }
+
+    @Throws(InterruptedIOException::class, ErrnoException::class)
+    private fun pretendPacketReceive(packet: ByteArray) {
+        Os.write(writeSocket, packet, 0, packet.size)
+    }
+
+    private fun getConnectivityPacketTracker(
+        dependencies: Dependencies = mDependencies
+    ): ConnectivityPacketTracker {
+        val result = CompletableFuture<ConnectivityPacketTracker>()
+        handler.post {
+            try {
+                val tracker = ConnectivityPacketTracker(handler, ifParams, localLog, dependencies)
+                tracker.start(TAG)
+                result.complete(tracker)
+            } catch (e: Exception) {
+                result.completeExceptionally(e)
+            }
+        }
+
+        return result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun setCapture(
+        packetTracker: ConnectivityPacketTracker,
+        isCapturing: Boolean
+    ) {
+        val result = CompletableFuture<Unit>()
+        handler.post {
+            try {
+                packetTracker.setCapture(isCapturing)
+                result.complete(Unit)
+            } catch (e: Exception) {
+                result.completeExceptionally(e)
+            }
+        }
+
+        result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun getMatchedPacketCount(
+        packetTracker: ConnectivityPacketTracker,
+        packet: String
+    ): Int {
+        val result = CompletableFuture<Int>()
+        handler.post {
+            try {
+                result.complete(packetTracker.getMatchedPacketCount(packet))
+            } catch (e: Exception) {
+                result.completeExceptionally(e)
+            }
+        }
+
+        return result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    private fun getTotalCapturePacketCount(
+        packetTracker: ConnectivityPacketTracker
+    ): Int {
+        val result = CompletableFuture<Int>()
+        handler.post {
+            try {
+                val totalCnt = packetTracker.totalCapturePacketCount
+                result.complete(totalCnt)
+            } catch (e: Exception) {
+                result.completeExceptionally(e)
+            }
+        }
+
+        return result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 71f7ebf..0df8a31 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -850,10 +850,10 @@
                 ApfConfiguration.class);
         if (isApfSupported) {
             verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                    any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                    any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         } else {
             verify(mDependencies, never()).maybeCreateApfFilter(
-                    any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                    any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         }
 
         return isApfSupported ? configCaptor.getValue() : null;
@@ -922,7 +922,7 @@
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
                 ApfConfiguration.class);
         verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         final ApfConfiguration actual = configCaptor.getValue();
         assertNotNull(actual);
         assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, actual.apfVersionSupported);
@@ -957,7 +957,7 @@
         ipc.updateApfCapabilities(newApfCapabilities);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
-                anyBoolean());
+                any(), anyBoolean());
         verifyShutdown(ipc);
     }
 
@@ -973,15 +973,15 @@
         ipc.updateApfCapabilities(null /* apfCapabilities */);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
-                anyBoolean());
+                any(), anyBoolean());
         verifyShutdown(ipc);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadDisabledWhenApfV6Supported() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
-                .thenReturn(mApfFilter);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
+                anyBoolean())).thenReturn(mApfFilter);
         when(mApfFilter.supportNdOffload()).thenReturn(true);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -1006,8 +1006,8 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadEnabledWhenApfV6NotSupported() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
-                .thenReturn(mApfFilter);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
+                anyBoolean())).thenReturn(mApfFilter);
         when(mApfFilter.supportNdOffload()).thenReturn(false);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -1030,8 +1030,8 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadDisabledWhenApfCapabilitiesUpdated() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
-                .thenReturn(mApfFilter);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
+                anyBoolean())).thenReturn(mApfFilter);
         when(mApfFilter.supportNdOffload()).thenReturn(true);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -1053,8 +1053,8 @@
 
     @Test
     public void testLinkPropertiesUpdate_callSetLinkPropertiesOnApfFilter() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
-                .thenReturn(mApfFilter);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
+                anyBoolean())).thenReturn(mApfFilter);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         verifyApfFilterCreatedOnStart(ipc, true /* isApfSupported */);
         onInterfaceAddressUpdated(
diff --git a/tests/unit/src/android/net/shared/PrivateDnsConfigTest.java b/tests/unit/src/android/net/shared/PrivateDnsConfigTest.java
index 94f04d5..2320470 100644
--- a/tests/unit/src/android/net/shared/PrivateDnsConfigTest.java
+++ b/tests/unit/src/android/net/shared/PrivateDnsConfigTest.java
@@ -22,6 +22,8 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.net.PrivateDnsConfigParcel;
 
@@ -55,6 +57,7 @@
         assertEquals(a.mode, b.mode);
         assertEquals(a.hostname, b.hostname);
         assertArrayEquals(a.ips, b.ips);
+        assertEquals(a.ddrEnabled, b.ddrEnabled);
         assertEquals(a.dohName, b.dohName);
         assertArrayEquals(a.dohIps, b.dohIps);
         assertEquals(a.dohPath, b.dohPath);
@@ -65,6 +68,7 @@
         assertEquals(parcel.privateDnsMode, cfg.mode);
         assertEquals(parcel.hostname, cfg.hostname);
         assertArrayEquals(parcel.ips, toStringArray(cfg.ips));
+        assertEquals(parcel.ddrEnabled, cfg.ddrEnabled);
         assertEquals(parcel.dohName, cfg.dohName);
         assertEquals(parcel.dohPath, cfg.dohPath);
         assertEquals(parcel.dohPort, cfg.dohPort);
@@ -102,19 +106,23 @@
         //                  String dohName, InetAddress[] dohIps, String dohPath, int dohPort)
         for (int mode : new int[] { OFF_MODE, OPPORTUNISTIC_MODE, STRICT_MODE }) {
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, null, null,
-                    null, null, null, -1));
+                    false, null, null, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", null,
-                    null, null, null, -1));
+                    false, null, null, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
-                    null, null, null, -1));
+                    false, null, null, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
-                    "doh.com", null, null, -1));
+                    true, null, null, null, -1));
+            testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", null,
+                    true, null, null, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
-                    "doh.com", TEST_ADDRS, null, -1));
+                    true, "doh.com", null, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
-                    "doh.com", TEST_ADDRS, "dohpath=/some-path{?dns}", -1));
+                    true, "doh.com", TEST_ADDRS, null, -1));
             testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
-                    "doh.com", TEST_ADDRS, "dohpath=/some-path{?dns}", 443));
+                    true, "doh.com", TEST_ADDRS, "dohpath=/some-path{?dns}", -1));
+            testPrivateDnsConfigConversion(new PrivateDnsConfig(mode, "dns.com", TEST_ADDRS,
+                    true, "doh.com", TEST_ADDRS, "dohpath=/some-path{?dns}", 443));
         }
     }
 
@@ -123,11 +131,30 @@
         final InetAddress ip = InetAddress.parseNumericAddress("1.2.3.4");
         final InetAddress[] ipArray = new InetAddress[] { ip };
         final PrivateDnsConfig cfg = new PrivateDnsConfig(OPPORTUNISTIC_MODE, null /* hostname */,
-                ipArray /* ips */, null /* dohName */, ipArray /* dohIps */, null /* dohPath */,
-                -1 /* dohPort */);
+                ipArray /* ips */, false /* ddrEnabled */, null /* dohName */, ipArray /* dohIps */,
+                null /* dohPath */, -1 /* dohPort */);
 
         ipArray[0] = InetAddress.parseNumericAddress("2001:db8::2");
         assertArrayEquals(new InetAddress[] { ip }, cfg.ips);
         assertArrayEquals(new InetAddress[] { ip }, cfg.dohIps);
     }
+
+    @Test
+    public void testSettingsComparison() {
+        final PrivateDnsConfig off = new PrivateDnsConfig(false);
+        final PrivateDnsConfig opportunistic = new PrivateDnsConfig(true);
+        final PrivateDnsConfig strict = new PrivateDnsConfig("dns.com", null);
+
+        assertFalse(opportunistic.areSettingsSameAs(off));
+        assertFalse(opportunistic.areSettingsSameAs(strict));
+        assertTrue(opportunistic.areSettingsSameAs(new PrivateDnsConfig(
+                OPPORTUNISTIC_MODE, null, TEST_ADDRS, false, null, null, null, -1)));
+
+        assertFalse(strict.areSettingsSameAs(off));
+        assertFalse(strict.areSettingsSameAs(opportunistic));
+        assertTrue(strict.areSettingsSameAs(new PrivateDnsConfig(
+                STRICT_MODE, "dns.com", TEST_ADDRS, false, null, null, null, -1)));
+        assertFalse(strict.areSettingsSameAs(new PrivateDnsConfig(
+                STRICT_MODE, "foo.com", TEST_ADDRS, false, null, null, null, -1)));
+    }
 }
diff --git a/tests/unit/src/android/net/testutils/NetworkStatsUtilsTest.kt b/tests/unit/src/android/net/testutils/NetworkStatsUtilsTest.kt
deleted file mode 100644
index 2e76c52..0000000
--- a/tests/unit/src/android/net/testutils/NetworkStatsUtilsTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.testutils
-
-import android.net.NetworkStats
-import android.net.NetworkStats.DEFAULT_NETWORK_NO
-import android.net.NetworkStats.METERED_NO
-import android.net.NetworkStats.ROAMING_NO
-import android.net.NetworkStats.SET_DEFAULT
-import android.net.NetworkStats.TAG_NONE
-import android.os.Build
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.orderInsensitiveEquals
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-
-private const val TEST_IFACE = "test0"
-private const val TEST_IFACE2 = "test2"
-private const val TEST_START = 1194220800000L
-
-@RunWith(JUnit4::class)
-class NetworkStatsUtilsTest {
-    // This is a unit test for a test utility that uses R APIs
-    @Rule @JvmField
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
-    @Test
-    fun testOrderInsensitiveEquals() {
-        val testEntry = arrayOf(
-                NetworkStats.Entry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
-                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L),
-                NetworkStats.Entry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
-                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L))
-
-        // Verify equals of empty stats regardless of initial capacity.
-        val red = NetworkStats(TEST_START, 0)
-        val blue = NetworkStats(TEST_START, 1)
-        assertTrue(orderInsensitiveEquals(red, blue))
-        assertTrue(orderInsensitiveEquals(blue, red))
-
-        // Verify not equal.
-        red.combineValues(testEntry[1])
-        blue.combineValues(testEntry[0]).combineValues(testEntry[1])
-        assertFalse(orderInsensitiveEquals(red, blue))
-        assertFalse(orderInsensitiveEquals(blue, red))
-
-        // Verify equals even if the order of entries are not the same.
-        red.combineValues(testEntry[0])
-        assertTrue(orderInsensitiveEquals(red, blue))
-        assertTrue(orderInsensitiveEquals(blue, red))
-    }
-}
\ No newline at end of file
diff --git a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
index d192425..7d11f80 100644
--- a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
+++ b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.ipmemorystore;
 
+import static com.android.networkstack.ipmemorystore.IpMemoryStoreDatabase.DbHelper.SCHEMA_VERSION;
 import static com.android.networkstack.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance;
 
 import static org.junit.Assert.assertEquals;
@@ -29,6 +30,8 @@
 
 import android.app.job.JobScheduler;
 import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import android.net.ipmemorystore.Blob;
 import android.net.ipmemorystore.IOnBlobRetrievedListener;
 import android.net.ipmemorystore.IOnL2KeyResponseListener;
@@ -99,8 +102,9 @@
     private static final long UNIX_TIME_MS_2100_01_01 = 4102412400000L;
     private static final int MTU_NULL = -1;
     private static final String[] FAKE_KEYS;
-    private static final byte[] TEST_BLOB_DATA = new byte[] { -3, 6, 8, -9, 12,
-            -128, 0, 89, 112, 91, -34 };
+    private static final byte[] TEST_BLOB_DATA = new byte[]{-3, 6, 8, -9, 12,
+            -128, 0, 89, 112, 91, -34};
+
     static {
         FAKE_KEYS = new String[FAKE_KEY_COUNT];
         for (int i = 0; i < FAKE_KEYS.length; ++i) {
@@ -235,6 +239,7 @@
     private interface OnBlobRetrievedListener {
         void onBlobRetrieved(Status status, String l2Key, String name, byte[] data);
     }
+
     private IOnBlobRetrievedListener onBlobRetrieved(final OnBlobRetrievedListener functor) {
         return new IOnBlobRetrievedListener() {
             @Override
@@ -262,9 +267,10 @@
     }
 
     /** Helper method to make an IOnNetworkAttributesRetrievedListener */
-    private interface OnNetworkAttributesRetrievedListener  {
+    private interface OnNetworkAttributesRetrievedListener {
         void onNetworkAttributesRetrieved(Status status, String l2Key, NetworkAttributes attr);
     }
+
     private IOnNetworkAttributesRetrievedListener onNetworkAttributesRetrieved(
             final OnNetworkAttributesRetrievedListener functor) {
         return new IOnNetworkAttributesRetrievedListener() {
@@ -297,6 +303,7 @@
     private interface OnSameL3NetworkResponseListener {
         void onSameL3NetworkResponse(Status status, SameL3NetworkResponse answer);
     }
+
     private IOnSameL3NetworkResponseListener onSameResponse(
             final OnSameL3NetworkResponseListener functor) {
         return new IOnSameL3NetworkResponseListener() {
@@ -329,6 +336,7 @@
     private interface OnL2KeyResponseListener {
         void onL2KeyResponse(Status status, String key);
     }
+
     private IOnL2KeyResponseListener onL2KeyResponse(final OnL2KeyResponseListener functor) {
         return new IOnL2KeyResponseListener() {
             @Override
@@ -376,6 +384,7 @@
     private NetworkAttributes storeAttributes(final String l2Key, final NetworkAttributes na) {
         return storeAttributes("Did not complete storing attributes", l2Key, na);
     }
+
     private NetworkAttributes storeAttributes(final String timeoutMessage, final String l2Key,
             final NetworkAttributes na) {
         doLatched(timeoutMessage, latch -> mService.storeNetworkAttributes(l2Key, na.toParcelable(),
@@ -390,6 +399,7 @@
     private void storeBlobOrFail(final String l2Key, final Blob b, final byte[] data) {
         storeBlobOrFail("Did not complete storing private data", l2Key, b, data);
     }
+
     private void storeBlobOrFail(final String timeoutMessage, final String l2Key, final Blob b,
             final byte[] data) {
         b.data = data;
@@ -409,8 +419,7 @@
      * 2. Comment out "mDbFile.delete()" in tearDown() method.
      * 3. Run "atest IpMemoryStoreServiceTest#testGenerateDB".
      * 4. Run "adb root; adb pull /data/data/com.android.server.networkstack.tests/files/test.db
-     *    $YOUR_CODE_BASE/package/module/NetworkStack/tests/unit/res/raw/test.db".
-     *
+     * $YOUR_CODE_BASE/package/module/NetworkStack/tests/unit/res/raw/test.db".
      */
     private void generateFakeData() {
         final int fakeDataCount = 1000;
@@ -473,7 +482,7 @@
 
         final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder();
         na.setDnsAddresses(Arrays.asList(
-                new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
+                new InetAddress[]{Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
         final NetworkAttributes attributes2 = na2.build();
         storeAttributes("Did not complete storing attributes 2", l2Key, attributes2);
 
@@ -587,13 +596,13 @@
         stored.add(storeAttributes(FAKE_KEYS[0], na.build()));
 
         na.setDnsAddresses(Arrays.asList(
-                new InetAddress[] {Inet6Address.getByName("8D56:9AF1::08EE:20F1")}));
+                new InetAddress[]{Inet6Address.getByName("8D56:9AF1::08EE:20F1")}));
         na.setMtu(208);
         stored.add(storeAttributes(FAKE_KEYS[1], na.build()));
         na.setMtu(null);
         na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
         na.setDnsAddresses(Arrays.asList(
-                new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
+                new InetAddress[]{Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
         na.setCluster("cluster1");
         stored.add(storeAttributes(FAKE_KEYS[2], na.build()));
         na.setMtu(219);
@@ -922,13 +931,13 @@
 
     private final List<Pair<String, byte[]>> mByteArrayTests = List.of(
             new Pair<>("null", null),
-            new Pair<>("[]", new byte[] {}),
+            new Pair<>("[]", new byte[]{}),
             new Pair<>("[0102030405060708090A0B0C]",
-                    new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }),
-            new Pair<>("[0F1080FF]", new byte[] { 15, 16, -128, -1 }),
+                    new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}),
+            new Pair<>("[0F1080FF]", new byte[]{15, 16, -128, -1}),
             new Pair<>("[0102030405060708090A0B0C0D0E0F10...15161718191A1B1C]",
-                    new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
-                            17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28 })
+                    new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+                            17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28})
     );
 
     @Test
@@ -976,4 +985,29 @@
         //db is null, the db size could not over the threshold.
         assertFalse(ipMemoryStoreService.isDbSizeOverThreshold());
     }
+
+    private void doTestDowngradeAndUpgrade(int downgradeVersion) {
+        SQLiteOpenHelper dbHelper = new IpMemoryStoreDatabase.DbHelper(
+                mMockContext, downgradeVersion);
+        SQLiteDatabase db = dbHelper.getWritableDatabase();
+        assertEquals(downgradeVersion, db.getVersion());
+        db.close();
+
+        dbHelper = new IpMemoryStoreDatabase.DbHelper(mMockContext, SCHEMA_VERSION);
+        db = dbHelper.getWritableDatabase();
+        assertEquals(SCHEMA_VERSION, db.getVersion());
+        db.close();
+    }
+
+    @Test
+    public void testDowngradeClearsTablesAndriggers() {
+        final String l2Key = FAKE_KEYS[0];
+        final Blob b = new Blob();
+        storeBlobOrFail(l2Key, b, TEST_BLOB_DATA);
+        mService.shutdown();
+
+        for (int version = SCHEMA_VERSION - 1; version >= 1; version--) {
+            doTestDowngradeAndUpgrade(version);
+        }
+    }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
index dcc4291..77630c7 100644
--- a/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
+++ b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
@@ -20,20 +20,28 @@
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.annotation.NonNull;
+import android.net.DnsResolver;
+import android.net.LinkProperties;
+import android.net.Network;
 import android.net.shared.PrivateDnsConfig;
 
+import com.android.net.module.util.SharedLog;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
+import java.util.concurrent.Executor;
 
 @RunWith(JUnit4.class)
 public final class DdrTrackerTest {
@@ -42,11 +50,22 @@
     private static final int STRICT_MODE = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 
     private DdrTracker mDdrTracker;
+    private @Mock DnsResolver mDnsResolver;
+    private @Mock Network mCleartextDnsNetwork;
+    private @Mock TestCallback mCallback;
+    private @Mock Executor mExecutor;
+    private @Mock SharedLog mValidationLogger;
+
+    // TODO(b/240259333): Fake DNS responses on mDnsResolver to test startSvcbLookup.
+    private static class TestCallback implements DdrTracker.Callback {
+        @Override
+        public void onSvcbLookupComplete(@NonNull PrivateDnsConfig result) {}
+    }
 
     private static class PrivateDnsConfigBuilder {
         private int mMode = OFF_MODE;
         private String mHostname = null;
-        private final InetAddress[] mIps = null;
+        private InetAddress[] mIps = null;
         private final String mDohName = null;
         private final InetAddress[] mDohIps = null;
         private final String mDohPath = null;
@@ -60,18 +79,24 @@
             mHostname = value;
             return this;
         }
+        PrivateDnsConfigBuilder setIps(InetAddress[] value) {
+            mIps = value;
+            return this;
+        }
         PrivateDnsConfig build() {
-            return new PrivateDnsConfig(mMode, mHostname, mIps, mDohName, mDohIps, mDohPath,
-                    mDohPort);
+            return new PrivateDnsConfig(mMode, mHostname, mIps,
+                    false /* ddrEnabled */, mDohName, mDohIps, mDohPath,  mDohPort);
         }
     }
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mDdrTracker = new DdrTracker();
+        mDdrTracker = new DdrTracker(mCleartextDnsNetwork, mDnsResolver, mExecutor, mCallback,
+                mValidationLogger);
     }
 
+    // TODO: check that if DeviceConfigUtils#isFeatureSupported returns false, DDR is disabled.
     private void testNotifyPrivateDnsSettingsChangedHelper(int mode, @NonNull String dnsProvider)
             throws Exception {
         final PrivateDnsConfig cfg =
@@ -87,7 +112,6 @@
     public void testNotifyPrivateDnsSettingsChanged() throws Exception {
         // Tests the initial private DNS setting in DdrTracker.
         assertEquals(OFF_MODE, mDdrTracker.getPrivateDnsMode());
-
         assertEquals("", mDdrTracker.getStrictModeHostname());
         assertFalse(mDdrTracker.notifyPrivateDnsSettingsChanged(new PrivateDnsConfigBuilder()
                 .setMode(OFF_MODE).build()));
@@ -97,4 +121,59 @@
         testNotifyPrivateDnsSettingsChangedHelper(STRICT_MODE, "example2.com");
         testNotifyPrivateDnsSettingsChangedHelper(OFF_MODE, "");
     }
+
+    private void testNotifyLinkPropertiesChangedHelper(InetAddress[] ips) {
+        final LinkProperties lp = new LinkProperties();
+        for (InetAddress ip : ips) {
+            assertTrue(lp.addDnsServer(ip));
+        }
+        assertTrue(mDdrTracker.notifyLinkPropertiesChanged(lp));
+        assertArrayEquals(ips, mDdrTracker.getDnsServers().toArray());
+        assertFalse(mDdrTracker.notifyLinkPropertiesChanged(lp));
+    }
+
+    @Test
+    public void testNotifyLinkPropertiesChanged() throws Exception {
+        final InetAddress ip1 = InetAddress.parseNumericAddress("1.2.3.4");
+        final InetAddress ip2 = InetAddress.parseNumericAddress("2001:db8::1");
+
+        // Tests the initial value in DdrTracker.
+        assertTrue(mDdrTracker.getDnsServers().isEmpty());
+
+        testNotifyLinkPropertiesChangedHelper(new InetAddress[] {ip1});
+        testNotifyLinkPropertiesChangedHelper(new InetAddress[] {ip1, ip2});
+        testNotifyLinkPropertiesChangedHelper(new InetAddress[] {ip2, ip1});
+    }
+
+    private void assertPrivateDnsConfigEquals(PrivateDnsConfig a, PrivateDnsConfig b) {
+        assertEquals(a.mode, b.mode);
+        assertEquals(a.hostname, b.hostname);
+        assertArrayEquals(a.ips, b.ips);
+        assertEquals(a.dohName, b.dohName);
+        assertArrayEquals(a.dohIps, b.dohIps);
+        assertEquals(a.dohPath, b.dohPath);
+        assertEquals(a.dohPort, b.dohPort);
+    }
+
+    @Test
+    public void testSetStrictModeHostnameResolutionResult() throws Exception {
+        final String dnsProvider = "example1.com";
+        final InetAddress[] ips = new InetAddress[] {
+            InetAddress.parseNumericAddress("1.2.3.4"),
+            InetAddress.parseNumericAddress("2001:db8::1"),
+        };
+        final PrivateDnsConfigBuilder builder =
+                new PrivateDnsConfigBuilder().setMode(STRICT_MODE).setHostname(dnsProvider);
+
+        assertTrue(mDdrTracker.notifyPrivateDnsSettingsChanged(builder.build()));
+        assertPrivateDnsConfigEquals(builder.build(), mDdrTracker.getResultForReporting());
+
+        mDdrTracker.setStrictModeHostnameResolutionResult(ips);
+        assertPrivateDnsConfigEquals(builder.setIps(ips).build(),
+                mDdrTracker.getResultForReporting());
+
+        mDdrTracker.resetStrictModeHostnameResolutionResult();
+        assertPrivateDnsConfigEquals(builder.setIps(new InetAddress[0]).build(),
+                mDdrTracker.getResultForReporting());
+    }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index d5d1c90..9a86d9f 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -104,7 +104,6 @@
 
 import static java.lang.System.currentTimeMillis;
 import static java.util.Collections.singletonList;
-import static java.util.stream.Collectors.toList;
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
@@ -121,7 +120,6 @@
 import android.net.DnsResolver;
 import android.net.INetd;
 import android.net.INetworkMonitorCallbacks;
-import android.net.InetAddresses;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkAgentConfig;
@@ -388,11 +386,11 @@
         class DnsEntry {
             final String mHostname;
             final int mType;
-            final AddressSupplier mAddressesSupplier;
-            DnsEntry(String host, int type, AddressSupplier addr) {
+            final AnswerSupplier mAnswerSupplier;
+            DnsEntry(String host, int type, AnswerSupplier answerSupplier) {
                 mHostname = host;
                 mType = type;
-                mAddressesSupplier = addr;
+                mAnswerSupplier = answerSupplier;
             }
             // Full match or partial match that target host contains the entry hostname to support
             // random private dns probe hostname.
@@ -400,22 +398,22 @@
                 return hostname.endsWith(mHostname) && type == mType;
             }
         }
-        interface AddressSupplier {
-            List<InetAddress> get() throws DnsResolver.DnsException;
+        interface AnswerSupplier {
+            List<String> get() throws DnsResolver.DnsException;
         }
 
-        class InstantAddressSupplier implements AddressSupplier {
-            private final List<InetAddress> mAddresses;
-            InstantAddressSupplier(List<InetAddress> addresses) {
-                mAddresses = addresses;
+        class InstantAnswerSupplier implements AnswerSupplier {
+            private final List<String> mAnswers;
+            InstantAnswerSupplier(List<String> answers) {
+                mAnswers = answers;
             }
             @Override
-            public List<InetAddress> get() {
-                return mAddresses;
+            public List<String> get() {
+                return mAnswers;
             }
         }
 
-        private final ArrayList<DnsEntry> mAnswers = new ArrayList<DnsEntry>();
+        private final ArrayList<DnsEntry> mAnswers = new ArrayList<>();
         private boolean mNonBypassPrivateDnsWorking = true;
 
         /** Whether DNS queries on mNonBypassPrivateDnsWorking should succeed. */
@@ -431,29 +429,29 @@
         }
 
         /** Returns the answer for a given name and type on the given mock network. */
-        private CompletableFuture<List<InetAddress>> getAnswer(Network mockNetwork, String hostname,
+        private CompletableFuture<List<String>> getAnswer(Network mockNetwork, String hostname,
                 int type) {
             if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) {
                 return CompletableFuture.completedFuture(null);
             }
 
-            final AddressSupplier answerSupplier;
+            final AnswerSupplier answerSupplier;
 
             synchronized (mAnswers) {
                 answerSupplier = mAnswers.stream()
                         .filter(e -> e.matches(hostname, type))
-                        .map(answer -> answer.mAddressesSupplier).findFirst().orElse(null);
+                        .map(answer -> answer.mAnswerSupplier).findFirst().orElse(null);
             }
             if (answerSupplier == null) {
                 return CompletableFuture.completedFuture(null);
             }
 
-            if (answerSupplier instanceof InstantAddressSupplier) {
+            if (answerSupplier instanceof InstantAnswerSupplier) {
                 // Save latency waiting for a query thread if the answer is hardcoded.
                 return CompletableFuture.completedFuture(
-                        ((InstantAddressSupplier) answerSupplier).get());
+                        ((InstantAnswerSupplier) answerSupplier).get());
             }
-            final CompletableFuture<List<InetAddress>> answerFuture = new CompletableFuture<>();
+            final CompletableFuture<List<String>> answerFuture = new CompletableFuture<>();
             new Thread(() -> {
                 try {
                     answerFuture.complete(answerSupplier.get());
@@ -466,10 +464,11 @@
 
         /** Sets the answer for a given name and type. */
         private void setAnswer(String hostname, String[] answer, int type) {
-            setAnswer(hostname, new InstantAddressSupplier(generateAnswer(answer)), type);
+            setAnswer(hostname, new InstantAnswerSupplier(
+                    (answer == null) ? null : Arrays.asList(answer)), type);
         }
 
-        private void setAnswer(String hostname, AddressSupplier answerSupplier, int type) {
+        private void setAnswer(String hostname, AnswerSupplier answerSupplier, int type) {
             DnsEntry record = new DnsEntry(hostname, type, answerSupplier);
             synchronized (mAnswers) {
                 // Remove the existing one.
@@ -479,9 +478,8 @@
             }
         }
 
-        private List<InetAddress> generateAnswer(String[] answer) {
-            if (answer == null) return new ArrayList<>();
-            return Arrays.stream(answer).map(InetAddresses::parseNumericAddress).collect(toList());
+        private byte[] makeSvcbResponse(String hostname, List<String> answer) {
+            throw new UnsupportedOperationException("Not supported, update this fake");
         }
 
         /** Simulates a getAllByName call for the specified name on the specified mock network. */
@@ -489,8 +487,8 @@
                 throws UnknownHostException {
             final List<InetAddress> answer;
             try {
-                answer = queryAllTypes(mockNetwork, hostname).get(
-                        HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+                answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname).get(
+                        HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             } catch (ExecutionException | InterruptedException | TimeoutException e) {
                 throw new AssertionError("No mock DNS reply within timeout", e);
             }
@@ -501,22 +499,22 @@
         }
 
         // Regardless of the type, depends on what the responses contained in the network.
-        private CompletableFuture<List<InetAddress>> queryAllTypes(
+        private CompletableFuture<List<String>> queryAllTypes(
                 Network mockNetwork, String hostname) {
             if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) {
                 return CompletableFuture.completedFuture(null);
             }
 
-            final CompletableFuture<List<InetAddress>> aFuture =
+            final CompletableFuture<List<String>> aFuture =
                     getAnswer(mockNetwork, hostname, TYPE_A)
                             .exceptionally(e -> Collections.emptyList());
-            final CompletableFuture<List<InetAddress>> aaaaFuture =
+            final CompletableFuture<List<String>> aaaaFuture =
                     getAnswer(mockNetwork, hostname, TYPE_AAAA)
                             .exceptionally(e -> Collections.emptyList());
 
-            final CompletableFuture<List<InetAddress>> combinedFuture = new CompletableFuture<>();
+            final CompletableFuture<List<String>> combinedFuture = new CompletableFuture<>();
             aFuture.thenAcceptBoth(aaaaFuture, (res1, res2) -> {
-                final List<InetAddress> answer = new ArrayList<>();
+                final List<String> answer = new ArrayList<>();
                 if (res1 != null) answer.addAll(res1);
                 if (res2 != null) answer.addAll(res2);
                 combinedFuture.complete(answer);
@@ -544,15 +542,24 @@
             }).when(mDnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any());
         }
 
+        private List<InetAddress> stringsToInetAddresses(List<String> addrs) {
+            if (addrs == null) return null;
+            final List<InetAddress> out = new ArrayList<>();
+            for (String addr : addrs) {
+                out.add(parseNumericAddress(addr));
+            }
+            return out;
+        }
+
         // Mocking queries on DnsResolver#query.
         private Answer mockQuery(InvocationOnMock invocation, int posNetwork, int posHostname,
                 int posExecutor, int posCallback, int posType) {
             String hostname = (String) invocation.getArgument(posHostname);
             Executor executor = (Executor) invocation.getArgument(posExecutor);
-            DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(posCallback);
             Network network = invocation.getArgument(posNetwork);
+            DnsResolver.Callback callback = invocation.getArgument(posCallback);
 
-            final CompletableFuture<List<InetAddress>> answerFuture = posType != -1
+            final CompletableFuture<List<String>> answerFuture = (posType != -1)
                     ? getAnswer(network, hostname, invocation.getArgument(posType))
                     : queryAllTypes(network, hostname);
 
@@ -566,7 +573,18 @@
                         return;
                     }
                     if (answer != null && answer.size() > 0) {
-                        callback.onAnswer(answer, 0);
+                        final int qtype = (posType != -1)
+                                ? invocation.getArgument(posType) : TYPE_AAAA;
+                        switch (qtype) {
+                            // Assume A and AAAA queries use the List<InetAddress> callback.
+                            case TYPE_A:
+                            case TYPE_AAAA:
+                                callback.onAnswer(stringsToInetAddresses(answer), 0);
+                                break;
+                            default:
+                                throw new UnsupportedOperationException(
+                                        "Unsupported qtype: " + qtype + ", update this fake");
+                        }
                     }
                 }));
             });
@@ -2561,7 +2579,7 @@
         final ConditionVariable v4Queried = new ConditionVariable();
         mFakeDns.setAnswer("dns.google", () -> {
             v4Queried.open();
-            return List.of(parseNumericAddress("192.0.2.123"));
+            return List.of("192.0.2.123");
         }, TYPE_A);
         mFakeDns.setAnswer("dns.google", () -> {
             // Make sure the v6 query processing is a bit slower than the v6 one. The small delay
@@ -2570,7 +2588,7 @@
             // not, the test should pass.
             v4Queried.block(HANDLER_TIMEOUT_MS);
             SystemClock.sleep(10L);
-            return List.of(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
+            return List.of("2001:db8::1", "2001:db8::2");
         }, TYPE_AAAA);
 
         notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
@@ -2601,12 +2619,12 @@
         mFakeDns.setAnswer("v1.google", () -> {
             queriedLatch.countDown();
             blockReplies.block(HANDLER_TIMEOUT_MS);
-            return List.of(parseNumericAddress("192.0.2.123"));
+            return List.of("192.0.2.123");
         }, TYPE_A);
         mFakeDns.setAnswer("v1.google", () -> {
             queriedLatch.countDown();
             blockReplies.block(HANDLER_TIMEOUT_MS);
-            return List.of(parseNumericAddress("2001:db8::1"));
+            return List.of("2001:db8::1");
         }, TYPE_AAAA);
 
         notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
@@ -2649,12 +2667,12 @@
         mFakeDns.setAnswer("v1.google", () -> {
             queriedLatch.countDown();
             blockReplies.block(HANDLER_TIMEOUT_MS);
-            return List.of(parseNumericAddress("192.0.2.123"));
+            return List.of("192.0.2.123");
         }, TYPE_A);
         mFakeDns.setAnswer("v1.google", () -> {
             queriedLatch.countDown();
             blockReplies.block(HANDLER_TIMEOUT_MS);
-            return List.of(parseNumericAddress("2001:db8::1"));
+            return List.of("2001:db8::1");
         }, TYPE_AAAA);
 
         notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
@@ -3262,11 +3280,11 @@
         mFakeDns.setAnswer("www.google.com", () -> {
             // Make sure the DNS probes take at least 1ms
             SystemClock.sleep(1);
-            return List.of(parseNumericAddress("2001:db8::443"));
+            return List.of("2001:db8::443");
         }, TYPE_AAAA);
         mFakeDns.setAnswer(PRIVATE_DNS_PROBE_HOST_SUFFIX, () -> {
             SystemClock.sleep(1);
-            return List.of(parseNumericAddress("2001:db8::444"));
+            return List.of("2001:db8::444");
         }, TYPE_AAAA);
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);