Merge "Use the network stack shims in services connectivity"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ffbd1fc..780ba26 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -26,6 +26,9 @@
     },
     {
       "name": "traffic_controller_unit_test"
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "postsubmit": [
@@ -42,6 +45,9 @@
     {
       "name": "traffic_controller_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "mainline-presubmit": [
@@ -61,6 +67,9 @@
     },
     {
       "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "mainline-postsubmit": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 041a3ae..c0e6749 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -53,6 +53,7 @@
         "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi",
+        "framework-bluetooth",
         "unsupportedappusage",
     ],
     plugins: ["java_api_finder"],
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index a3ca90c..fa5af49 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -45,9 +45,8 @@
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        // TODO: remove system/netd/* when all BPF code is moved out of Netd.
-        "//system/netd/libnetdbpf",
         "//system/netd/server",
         "//system/netd/tests",
     ],
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 8577d9d..f0df97b 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -20,7 +20,6 @@
 #include <linux/if_ether.h>
 #include <linux/in.h>
 #include <linux/in6.h>
-#include <netdutils/UidConstants.h>
 
 // This header file is shared by eBPF kernel programs (C) and netd (C++) and
 // some of the maps are also accessed directly from Java mainline module code.
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 72ee431..f0af8b4 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -25,6 +25,7 @@
 #include <linux/ipv6.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
+#include <netdutils/UidConstants.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include "bpf_net_helpers.h"
diff --git a/framework/api/current.txt b/framework/api/current.txt
index ce0c868..a373b71 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -326,6 +326,8 @@
     field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
     field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
     field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
+    field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
+    field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
     field public static final int NET_CAPABILITY_RCS = 8; // 0x8
     field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
     field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index db08c48..a5afbd4 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -146,7 +146,9 @@
   }
 
   public class NetworkRequest implements android.os.Parcelable {
+    method @NonNull public int[] getEnterpriseIds();
     method @NonNull public int[] getForbiddenCapabilities();
+    method public boolean hasEnterpriseId(int);
     method public boolean hasForbiddenCapability(int);
   }
 
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index c98be47..4ae3a06 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -357,6 +357,8 @@
             NET_CAPABILITY_BIP,
             NET_CAPABILITY_HEAD_UNIT,
             NET_CAPABILITY_MMTEL,
+            NET_CAPABILITY_PRIORITIZE_LATENCY,
+            NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
     })
     public @interface NetCapability { }
 
@@ -600,8 +602,18 @@
      */
     public static final int NET_CAPABILITY_MMTEL = 33;
 
+    /**
+     * Indicates that this network should be able to prioritize latency for the internet.
+     */
+    public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34;
+
+    /**
+     * Indicates that this network should be able to prioritize bandwidth for the internet.
+     */
+    public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
+
     private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_MMTEL;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
 
     /**
      * Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -2241,6 +2253,8 @@
             case NET_CAPABILITY_BIP:                  return "BIP";
             case NET_CAPABILITY_HEAD_UNIT:            return "HEAD_UNIT";
             case NET_CAPABILITY_MMTEL:                return "MMTEL";
+            case NET_CAPABILITY_PRIORITIZE_LATENCY:          return "PRIORITIZE_LATENCY";
+            case NET_CAPABILITY_PRIORITIZE_BANDWIDTH:        return "PRIORITIZE_BANDWIDTH";
             default:                                  return Integer.toString(capability);
         }
     }
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index afc76d6..b7a6076 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -725,6 +725,33 @@
     }
 
     /**
+     * Get the enteprise identifiers.
+     *
+     * Get all the enterprise identifiers set on this {@code NetworkCapability}
+     * @return array of all the enterprise identifiers.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public @NonNull @NetworkCapabilities.EnterpriseId int[] getEnterpriseIds() {
+        // No need to make a defensive copy here as NC#getCapabilities() already returns
+        // a new array.
+        return networkCapabilities.getEnterpriseIds();
+    }
+
+    /**
+     * Tests for the presence of an enterprise identifier on this instance.
+     *
+     * @param enterpriseId the enterprise capability identifier to be tested for.
+     * @return {@code true} if set on this instance.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public boolean hasEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        return networkCapabilities.hasEnterpriseId(enterpriseId);
+    }
+
+    /**
      * Gets all the forbidden capabilities set on this {@code NetworkRequest} instance.
      *
      * @return an array of forbidden capability values for this instance.
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
new file mode 100644
index 0000000..bceeefa
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libnetworkstats",
+    vendor_available: false,
+    host_supported: false,
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStats.cpp"
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "libnetworkstats_test",
+    test_suites: ["general-tests"],
+    require_root: true,  // required by setrlimitForTest()
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStatsTest.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    static_libs: ["libgmock"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libnetworkstats",
+        "libutils",
+    ],
+}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
new file mode 100644
index 0000000..4d605ce
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include <inttypes.h>
+#include <net/if.h>
+#include <string.h>
+#include <unordered_set>
+
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "android-base/file.h"
+#include "android-base/strings.h"
+#include "android-base/unique_fd.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+#ifdef LOG_TAG
+#undef LOG_TAG
+#endif
+
+#define LOG_TAG "BpfNetworkStats"
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+
+// The target map for stats reading should be the inactive map, which is opposite
+// from the config value.
+static constexpr char const* STATS_MAP_PATH[] = {STATS_MAP_B_PATH, STATS_MAP_A_PATH};
+
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
+    auto statsEntry = appUidStatsMap.readValue(uid);
+    if (statsEntry.ok()) {
+        stats->rxPackets = statsEntry.value().rxPackets;
+        stats->txPackets = statsEntry.value().txPackets;
+        stats->rxBytes = statsEntry.value().rxBytes;
+        stats->txBytes = statsEntry.value().txBytes;
+    }
+    return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
+                                                                    : -statsEntry.error().code();
+}
+
+int bpfGetUidStats(uid_t uid, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
+
+    if (!appUidStatsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
+        return ret;
+    }
+    return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
+}
+
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    stats->tcpRxPackets = -1;
+    stats->tcpTxPackets = -1;
+    const auto processIfaceStats =
+            [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+                    const uint32_t& key,
+                    const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        if (!iface || !strcmp(iface, ifname)) {
+            Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
+            if (!statsEntry.ok()) {
+                return statsEntry.error();
+            }
+            stats->rxPackets += statsEntry.value().rxPackets;
+            stats->txPackets += statsEntry.value().txPackets;
+            stats->rxBytes += statsEntry.value().rxBytes;
+            stats->txBytes += statsEntry.value().txBytes;
+        }
+        return Result<void>();
+    };
+    auto res = ifaceStatsMap.iterate(processIfaceStats);
+    return res.ok() ? 0 : -res.error().code();
+}
+
+int bpfGetIfaceStats(const char* iface, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    int ret;
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
+                              const char* ifname) {
+    stats_line newLine;
+    strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+    newLine.uid = (int32_t)statsKey.uid;
+    newLine.set = (int32_t)statsKey.counterSet;
+    newLine.tag = (int32_t)statsKey.tag;
+    newLine.rxPackets = statsEntry.rxPackets;
+    newLine.txPackets = statsEntry.txPackets;
+    newLine.rxBytes = statsEntry.rxBytes;
+    newLine.txBytes = statsEntry.txBytes;
+    return newLine;
+}
+
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailUidStats =
+            [lines, &limitIfaces, &limitTag, &limitUid, &unknownIfaceBytesTotal, &ifaceMap](
+                    const StatsKey& key,
+                    const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        std::string ifnameStr(ifname);
+        if (limitIfaces.size() > 0 &&
+            std::find(limitIfaces.begin(), limitIfaces.end(), ifnameStr) == limitIfaces.end()) {
+            // Nothing matched; skip this line.
+            return Result<void>();
+        }
+        if (limitTag != TAG_ALL && uint32_t(limitTag) != key.tag) {
+            return Result<void>();
+        }
+        if (limitUid != UID_ALL && uint32_t(limitUid) != key.uid) {
+            return Result<void>();
+        }
+        Result<StatsValue> statsEntry = statsMap.readValue(key);
+        if (!statsEntry.ok()) {
+            return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
+        }
+        lines->push_back(populateStatsEntry(key, statsEntry.value(), ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterate(processDetailUidStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    // Since eBPF use hash map to record stats, network stats collected from
+    // eBPF will be out of order. And the performance of findIndexHinted in
+    // NetworkStats will also be impacted.
+    //
+    // Furthermore, since the StatsKey contains iface index, the network stats
+    // reported to framework would create items with the same iface, uid, tag
+    // and set, which causes NetworkStats maps wrong item to subtract.
+    //
+    // Thus, the stats needs to be properly sorted and grouped before reported.
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid) {
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+    if (!configurationMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get configuration map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
+    if (!configuration.ok()) {
+        ALOGE("Cannot read the old configuration from map: %s",
+              configuration.error().message().c_str());
+        return -configuration.error().code();
+    }
+    const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+    BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
+    if (!statsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
+        return ret;
+    }
+
+    // It is safe to read and clear the old map now since the
+    // networkStatsFactory should call netd to swap the map in advance already.
+    int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid, statsMap,
+                                                 ifaceIndexNameMap);
+    if (ret) {
+        ALOGE("parse detail network stats failed: %s", strerror(errno));
+        return ret;
+    }
+
+    Result<void> res = statsMap.clear();
+    if (!res.ok()) {
+        ALOGE("Clean up current stats map failed: %s", strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    return 0;
+}
+
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailIfaceStats = [lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+                                             const uint32_t& key, const StatsValue& value,
+                                             const BpfMap<uint32_t, StatsValue>&) {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        StatsKey fakeKey = {
+                .uid = (uint32_t)UID_ALL,
+                .tag = (uint32_t)TAG_NONE,
+                .counterSet = (uint32_t)SET_ALL,
+        };
+        lines->push_back(populateStatsEntry(fakeKey, value, ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
+    int ret = 0;
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+uint64_t combineUidTag(const uid_t uid, const uint32_t tag) {
+    return (uint64_t)uid << 32 | tag;
+}
+
+void groupNetworkStats(std::vector<stats_line>* lines) {
+    if (lines->size() <= 1) return;
+    std::sort(lines->begin(), lines->end());
+
+    // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
+    size_t nextOutput = 0;
+    for (size_t i = 1; i < lines->size(); i++) {
+        if (lines->at(nextOutput) == lines->at(i)) {
+            lines->at(nextOutput) += lines->at(i);
+        } else {
+            nextOutput++;
+            if (nextOutput != i) {
+                lines->at(nextOutput) = lines->at(i);
+            }
+        }
+    }
+
+    if (lines->size() != nextOutput + 1) {
+        lines->resize(nextOutput + 1);
+    }
+}
+
+// True if lhs equals to rhs, only compare iface, uid, tag and set.
+bool operator==(const stats_line& lhs, const stats_line& rhs) {
+    return ((lhs.uid == rhs.uid) && (lhs.tag == rhs.tag) && (lhs.set == rhs.set) &&
+            !strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface)));
+}
+
+// True if lhs is smaller than rhs, only compare iface, uid, tag and set.
+bool operator<(const stats_line& lhs, const stats_line& rhs) {
+    int ret = strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface));
+    if (ret != 0) return ret < 0;
+    if (lhs.uid < rhs.uid) return true;
+    if (lhs.uid > rhs.uid) return false;
+    if (lhs.tag < rhs.tag) return true;
+    if (lhs.tag > rhs.tag) return false;
+    if (lhs.set < rhs.set) return true;
+    if (lhs.set > rhs.set) return false;
+    return false;
+}
+
+stats_line& stats_line::operator=(const stats_line& rhs) {
+    if (this == &rhs) return *this;
+
+    strlcpy(iface, rhs.iface, sizeof(iface));
+    uid = rhs.uid;
+    set = rhs.set;
+    tag = rhs.tag;
+    rxPackets = rhs.rxPackets;
+    txPackets = rhs.txPackets;
+    rxBytes = rhs.rxBytes;
+    txBytes = rhs.txBytes;
+    return *this;
+}
+
+stats_line& stats_line::operator+=(const stats_line& rhs) {
+    rxPackets += rhs.rxPackets;
+    txPackets += rhs.txPackets;
+    rxBytes += rhs.rxBytes;
+    txBytes += rhs.txBytes;
+    return *this;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
new file mode 100644
index 0000000..4974b96
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -0,0 +1,569 @@
+/*
+ * 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.
+ */
+
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID1 = 10086;
+constexpr uid_t TEST_UID2 = 12345;
+constexpr uint32_t TEST_TAG = 42;
+constexpr int TEST_COUNTERSET0 = 0;
+constexpr int TEST_COUNTERSET1 = 1;
+constexpr uint64_t TEST_BYTES0 = 1000;
+constexpr uint64_t TEST_BYTES1 = 2000;
+constexpr uint64_t TEST_PACKET0 = 100;
+constexpr uint64_t TEST_PACKET1 = 200;
+constexpr const char IFACE_NAME1[] = "lo";
+constexpr const char IFACE_NAME2[] = "wlan0";
+constexpr const char IFACE_NAME3[] = "rmnet_data0";
+// A iface name that the size is bigger than IFNAMSIZ
+constexpr const char LONG_IFACE_NAME[] = "wlanWithALongName";
+constexpr const char TRUNCATED_IFACE_NAME[] = "wlanWithALongNa";
+constexpr uint32_t IFACE_INDEX1 = 1;
+constexpr uint32_t IFACE_INDEX2 = 2;
+constexpr uint32_t IFACE_INDEX3 = 3;
+constexpr uint32_t IFACE_INDEX4 = 4;
+constexpr uint32_t UNKNOWN_IFACE = 0;
+
+class BpfNetworkStatsHelperTest : public testing::Test {
+  protected:
+    BpfNetworkStatsHelperTest() {}
+    BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+    BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMap;
+    BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
+    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+
+    void SetUp() {
+        ASSERT_EQ(0, setrlimitForTest());
+
+        mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+
+        mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+
+        mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeStatsMap.getMap());
+
+        mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+
+        mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+    }
+
+    void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
+        auto tagResult = mFakeCookieTagMap.readValue(cookie);
+        EXPECT_RESULT_OK(tagResult);
+        EXPECT_EQ(uid, tagResult.value().uid);
+        EXPECT_EQ(tag, tagResult.value().tag);
+    }
+
+    void populateFakeStats(uid_t uid, uint32_t tag, uint32_t ifaceIndex, uint32_t counterSet,
+                           StatsValue value, BpfMap<StatsKey, StatsValue>& map) {
+        StatsKey key = {
+            .uid = (uint32_t)uid, .tag = tag, .counterSet = counterSet, .ifaceIndex = ifaceIndex};
+        EXPECT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+    }
+
+    void updateIfaceMap(const char* ifaceName, uint32_t ifaceIndex) {
+        IfaceValue iface;
+        strlcpy(iface.name, ifaceName, IFNAMSIZ);
+        EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+    }
+
+    void expectStatsEqual(const StatsValue& target, const Stats& result) {
+        EXPECT_EQ(target.rxPackets, result.rxPackets);
+        EXPECT_EQ(target.rxBytes, result.rxBytes);
+        EXPECT_EQ(target.txPackets, result.txPackets);
+        EXPECT_EQ(target.txBytes, result.txBytes);
+    }
+
+    void expectStatsLineEqual(const StatsValue target, const char* iface, uint32_t uid,
+                              int counterSet, uint32_t tag, const stats_line& result) {
+        EXPECT_EQ(0, strcmp(iface, result.iface));
+        EXPECT_EQ(uid, (uint32_t)result.uid);
+        EXPECT_EQ((uint32_t) counterSet, result.set);
+        EXPECT_EQ(tag, (uint32_t)result.tag);
+        EXPECT_EQ(target.rxPackets, (uint64_t)result.rxPackets);
+        EXPECT_EQ(target.rxBytes, (uint64_t)result.rxBytes);
+        EXPECT_EQ(target.txPackets, (uint64_t)result.txPackets);
+        EXPECT_EQ(target.txBytes, (uint64_t)result.txBytes);
+    }
+};
+
+// TEST to verify the behavior of bpf map when cocurrent deletion happens when
+// iterating the same map.
+TEST_F(BpfNetworkStatsHelperTest, TestIterateMapWithDeletion) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    uint64_t curCookie = 0;
+    auto nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    uint64_t headOfMap = nextCookie.value();
+    curCookie = nextCookie.value();
+    // Find the second entry in the map, then immediately delete it.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Find the entry that is now immediately after headOfMap, then delete that.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Attempting to read an entry that has been deleted fails with ENOENT.
+    curCookie = nextCookie.value();
+    auto tagResult = mFakeCookieTagMap.readValue(curCookie);
+    EXPECT_EQ(ENOENT, tagResult.error().code());
+    // Finding the entry after our deleted entry restarts iteration from the beginning of the map.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_EQ(headOfMap, nextCookie.value());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestBpfIterateMap) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    int totalCount = 0;
+    int totalSum = 0;
+    const auto iterateWithoutDeletion =
+            [&totalCount, &totalSum](const uint64_t& key, const BpfMap<uint64_t, UidTagValue>&) {
+                EXPECT_GE((uint64_t)5, key);
+                totalCount++;
+                totalSum += key;
+                return Result<void>();
+            };
+    EXPECT_RESULT_OK(mFakeCookieTagMap.iterate(iterateWithoutDeletion));
+    EXPECT_EQ(5, totalCount);
+    EXPECT_EQ(1 + 2 + 3 + 4 + 5, totalSum);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUidStatsNoTraffic) {
+    StatsValue value1 = {
+            .rxPackets = 0,
+            .rxBytes = 0,
+            .txPackets = 0,
+            .txBytes = 0,
+    };
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetUidStatsTotal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
+    expectStatsEqual(value2, result2);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME3, TEST_UID2, TEST_COUNTERSET1, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsInternal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value1, result1);
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value2, result2);
+    Stats totalResult = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    StatsValue totalValue = {
+            .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
+            .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
+            .txPackets = TEST_PACKET1 * 2 + TEST_PACKET0,
+            .txBytes = TEST_BYTES1 * 2 + TEST_BYTES0,
+    };
+    expectStatsEqual(totalValue, totalResult);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsWithSkippedIface) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(0, 0, 0, OVERFLOW_COUNTERSET, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, 0, lines.front());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUnknownIfaceError) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 20,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 20,
+    };
+    uint32_t ifaceIndex = UNKNOWN_IFACE;
+    populateFakeStats(TEST_UID1, 0, ifaceIndex, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 40,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 40,
+    };
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    StatsKey curKey = {
+            .uid = TEST_UID1,
+            .tag = 0,
+            .counterSet = TEST_COUNTERSET0,
+            .ifaceIndex = ifaceIndex,
+    };
+    char ifname[IFNAMSIZ];
+    int64_t unknownIfaceBytesTotal = 0;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
+    curKey.ifaceIndex = IFACE_INDEX2;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(-1, unknownIfaceBytesTotal);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    // TODO: find a way to test the total of unknown Iface Bytes go above limit.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    updateIfaceMap(LONG_IFACE_NAME, IFACE_INDEX4);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX4;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    std::vector<stats_line> lines;
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+
+    expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME3, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[2]);
+    ASSERT_EQ(0, strcmp(TRUNCATED_IFACE_NAME, lines[3].iface));
+    expectStatsLineEqual(value2, TRUNCATED_IFACE_NAME, UID_ALL, SET_ALL, TAG_NONE, lines[3]);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortedAndGrouped) {
+    // Create iface indexes with duplicate iface name.
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX3);  // Duplicate!
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    StatsValue value3 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+
+    // Test empty stats.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 0, lines.size());
+    lines.clear();
+
+    // Test 1 line stats.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    lines.clear();
+
+    // These items should not be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET1, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+    lines.clear();
+
+    // These items should be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+
+    // Verify Sorted & Grouped.
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+    lines.clear();
+
+    // Perform test on IfaceStats.
+    uint32_t ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    // This should be grouped.
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 2, lines.size());
+
+    expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    lines.clear();
+}
+
+// Test to verify that subtract overflow will not be triggered by the compare function invoked from
+// sorting. See http:/b/119193941.
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortAndOverflow) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+
+    // Mutate uid, 0 < TEST_UID1 < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(0, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(UINT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MIN, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // Mutate tag, 0 < TEST_TAG < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(TEST_UID1, INT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, INT_MIN, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, UINT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 8, lines.size());
+
+    // Uid 0 first
+    expectStatsLineEqual(value1, IFACE_NAME1, 0, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+
+    // Test uid, mutate tag.
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[1]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX, lines[2]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN, lines[3]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[4]);
+
+    // Mutate uid.
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[5]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN, TEST_COUNTERSET0, TEST_TAG, lines[6]);
+    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[7]);
+    lines.clear();
+}
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
new file mode 100644
index 0000000..8ab7e25
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+#ifndef _BPF_NETWORKSTATS_H
+#define _BPF_NETWORKSTATS_H
+
+#include <bpf/BpfMap.h>
+#include "bpf_shared.h"
+
+namespace android {
+namespace bpf {
+
+// TODO: set this to a proper value based on the map size;
+constexpr int TAG_STATS_MAP_SOFT_LIMIT = 3;
+constexpr int UID_ALL = -1;
+constexpr int TAG_ALL = -1;
+constexpr int TAG_NONE = 0;
+constexpr int SET_ALL = -1;
+constexpr int SET_DEFAULT = 0;
+constexpr int SET_FOREGROUND = 1;
+
+// The limit for stats received by a unknown interface;
+constexpr const int64_t MAX_UNKNOWN_IFACE_BYTES = 100 * 1000;
+
+// This is used by
+// frameworks/base/core/jni/com_android_internal_net_NetworkStatsFactory.cpp
+// make sure it is consistent with the JNI code before changing this.
+struct stats_line {
+    char iface[32];
+    uint32_t uid;
+    uint32_t set;
+    uint32_t tag;
+    int64_t rxBytes;
+    int64_t rxPackets;
+    int64_t txBytes;
+    int64_t txPackets;
+
+    stats_line& operator=(const stats_line& rhs);
+    stats_line& operator+=(const stats_line& rhs);
+};
+
+bool operator==(const stats_line& lhs, const stats_line& rhs);
+bool operator<(const stats_line& lhs, const stats_line& rhs);
+
+// For test only
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+// For test only
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+// For test only
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+// For test only
+int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
+// For test only
+template <class Key>
+int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
+                        const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
+                        const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    auto iface = ifaceMap.readValue(ifaceIndex);
+    if (!iface.ok()) {
+        maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
+        return -ENODEV;
+    }
+    strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
+    return 0;
+}
+
+template <class Key>
+void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+                          const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    // Have we already logged an error?
+    if (*unknownIfaceBytesTotal == -1) {
+        return;
+    }
+
+    // Are we undercounting enough data to be worth logging?
+    auto statsEntry = statsMap.readValue(curKey);
+    if (!statsEntry.ok()) {
+        // No data is being undercounted.
+        return;
+    }
+
+    *unknownIfaceBytesTotal += (statsEntry.value().rxBytes + statsEntry.value().txBytes);
+    if (*unknownIfaceBytesTotal >= MAX_UNKNOWN_IFACE_BYTES) {
+        ALOGE("Unknown name for ifindex %d with more than %" PRId64 " bytes of traffic", ifaceIndex,
+              *unknownIfaceBytesTotal);
+        *unknownIfaceBytesTotal = -1;
+    }
+}
+
+// For test only
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+
+int bpfGetUidStats(uid_t uid, Stats* stats);
+int bpfGetIfaceStats(const char* iface, Stats* stats);
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid);
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>* lines);
+int cleanStatsMap();
+}  // namespace bpf
+}  // namespace android
+
+#endif  // _BPF_NETWORKSTATS_H
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 9603139..b445462 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#define LOG_TAG "jniClatCoordinator"
 
 #include <arpa/inet.h>
 #include <errno.h>
@@ -22,6 +23,8 @@
 #include <log/log.h>
 #include <nativehelper/JNIHelp.h>
 #include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
 #include <string>
 
 #include <netjniutils/netjniutils.h>
@@ -33,9 +36,16 @@
 // Sync from system/netd/include/netid_client.h
 #define MARK_UNSET 0u
 
+// Sync from system/netd/server/NetdConstants.h
+#define __INT_STRLEN(i) sizeof(#i)
+#define _INT_STRLEN(i) __INT_STRLEN(i)
+#define INT32_STRLEN _INT_STRLEN(INT32_MIN)
+
 #define DEVICEPREFIX "v4-"
 
 namespace android {
+static const char* kClatdPath = "/apex/com.android.tethering/bin/for-system/clatd";
+
 static void throwIOException(JNIEnv* env, const char* msg, int error) {
     jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
 }
@@ -282,8 +292,7 @@
     return 0;
 }
 
-// TODO: fork clatd and rename to .._startClatd.
-static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf(
+static jint com_android_server_connectivity_ClatCoordinator_startClatd(
         JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
         jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
     ScopedUtfChars ifaceStr(env, iface);
@@ -291,7 +300,121 @@
     ScopedUtfChars v4Str(env, v4);
     ScopedUtfChars v6Str(env, v6);
 
-    // Start BPF if any
+    int tunFd = netjniutils::GetNativeFileDescriptor(env, tunJavaFd);
+    if (tunFd < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid tun file descriptor");
+        return -1;
+    }
+
+    int readSock = netjniutils::GetNativeFileDescriptor(env, readSockJavaFd);
+    if (readSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid read socket");
+        return -1;
+    }
+
+    int writeSock = netjniutils::GetNativeFileDescriptor(env, writeSockJavaFd);
+    if (writeSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid write socket");
+        return -1;
+    }
+
+    // 1. create a throwaway socket to reserve a file descriptor number
+    int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedTunFd == -1) {
+        throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
+        return -1;
+    }
+    int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockRead == -1) {
+        throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
+        return -1;
+    }
+    int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockWrite == -1) {
+        throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
+        return -1;
+    }
+
+    // these are the FD we'll pass to clatd on the cli, so need it as a string
+    char passedTunFdStr[INT32_STRLEN];
+    char passedSockReadStr[INT32_STRLEN];
+    char passedSockWriteStr[INT32_STRLEN];
+    snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
+    snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
+    snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+
+    // 2. we're going to use this as argv[0] to clatd to make ps output more useful
+    std::string progname("clatd-");
+    progname += ifaceStr.c_str();
+
+    // clang-format off
+    const char* args[] = {progname.c_str(),
+                          "-i", ifaceStr.c_str(),
+                          "-p", pfx96Str.c_str(),
+                          "-4", v4Str.c_str(),
+                          "-6", v6Str.c_str(),
+                          "-t", passedTunFdStr,
+                          "-r", passedSockReadStr,
+                          "-w", passedSockWriteStr,
+                          nullptr};
+    // clang-format on
+
+    // 3. register vfork requirement
+    posix_spawnattr_t attr;
+    if (int ret = posix_spawnattr_init(&attr)) {
+        throwIOException(env, "posix_spawnattr_init failed", ret);
+        return -1;
+    }
+
+    // TODO: use android::base::ScopeGuard.
+    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawnattr_setflags failed", ret);
+        return -1;
+    }
+
+    // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
+    // on the tun fd that we want the child clatd process to inherit
+    // (this will happen after the vfork, and before the execve)
+    posix_spawn_file_actions_t fa;
+    if (int ret = posix_spawn_file_actions_init(&fa)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawn_file_actions_init failed", ret);
+        return -1;
+    }
+
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
+        return -1;
+    }
+
+    // 5. actually perform vfork/dup2/execve
+    pid_t pid;
+    if (int ret = posix_spawn(&pid, kClatdPath, &fa, &attr, (char* const*)args, nullptr)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn failed", ret);
+        return -1;
+    }
+
+    posix_spawnattr_destroy(&attr);
+    posix_spawn_file_actions_destroy(&fa);
+
+    // 5. Start BPF if any
     if (!net::clat::initMaps()) {
         net::clat::ClatdTracker tracker = {};
         if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -300,19 +423,60 @@
         }
     }
 
-    return 0; // TODO: return forked clatd pid.
+    return pid;
 }
 
-// TODO: stop clatd and rename to .._stopClatd.
-static void com_android_server_connectivity_ClatCoordinator_maybeStopBpf(JNIEnv* env, jobject clazz,
-                                                                       jstring iface, jstring pfx96,
-                                                                       jstring v4, jstring v6,
-                                                                       jint pid /* unused */) {
+// Stop clatd process. SIGTERM with timeout first, if fail, SIGKILL.
+// See stopProcess() in system/netd/server/NetdConstants.cpp.
+// TODO: have a function stopProcess(int pid, const char *name) in common location and call it.
+static constexpr int WAITPID_ATTEMPTS = 50;
+static constexpr int WAITPID_RETRY_INTERVAL_US = 100000;
+
+static void stopClatdProcess(int pid) {
+    int err = kill(pid, SIGTERM);
+    if (err) {
+        err = errno;
+    }
+    if (err == ESRCH) {
+        ALOGE("clatd child process %d unexpectedly disappeared", pid);
+        return;
+    }
+    if (err) {
+        ALOGE("Error killing clatd child process %d: %s", pid, strerror(err));
+    }
+    int status = 0;
+    int ret = 0;
+    for (int count = 0; ret == 0 && count < WAITPID_ATTEMPTS; count++) {
+        usleep(WAITPID_RETRY_INTERVAL_US);
+        ret = waitpid(pid, &status, WNOHANG);
+    }
+    if (ret == 0) {
+        ALOGE("Failed to SIGTERM clatd pid=%d, try SIGKILL", pid);
+        // TODO: fix that kill failed or waitpid doesn't return.
+        kill(pid, SIGKILL);
+        ret = waitpid(pid, &status, 0);
+    }
+    if (ret == -1) {
+        ALOGE("Error waiting for clatd child process %d: %s", pid, strerror(errno));
+    } else {
+        ALOGD("clatd process %d terminated status=%d", pid, status);
+    }
+}
+
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jobject clazz,
+                                                                      jstring iface, jstring pfx96,
+                                                                      jstring v4, jstring v6,
+                                                                      jint pid) {
     ScopedUtfChars ifaceStr(env, iface);
     ScopedUtfChars pfx96Str(env, pfx96);
     ScopedUtfChars v4Str(env, v4);
     ScopedUtfChars v6Str(env, v6);
 
+    if (pid <= 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
+        return;
+    }
+
     if (!net::clat::initMaps()) {
         net::clat::ClatdTracker tracker = {};
         if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -320,6 +484,8 @@
             net::clat::maybeStopBpf(tracker);
         }
     }
+
+    stopClatdProcess(pid);
 }
 
 /*
@@ -344,13 +510,13 @@
          (void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt},
         {"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
          (void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket},
-        {"native_maybeStartBpf",
+        {"native_startClatd",
          "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
          "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
-         (void*)com_android_server_connectivity_ClatCoordinator_maybeStartBpf},
-        {"native_maybeStopBpf",
+         (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
+        {"native_stopClatd",
          "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
-          (void*)com_android_server_connectivity_ClatCoordinator_maybeStopBpf},
+         (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
 };
 
 int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index cac545d..97fcc43 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -40,6 +40,7 @@
 #include <android-base/unique_fd.h>
 #include <netdutils/StatusOr.h>
 #include <netdutils/Syscalls.h>
+#include <netdutils/UidConstants.h>
 #include <netdutils/Utils.h>
 #include <private/android_filesystem_config.h>
 
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index ae6fe11..145eade 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -4046,7 +4046,7 @@
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
                         INetd.PERMISSION_NONE,
                         (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
-                        getVpnType(nai), /*excludeLocalRoutes=*/ false);
+                        getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
             } else {
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 5a5e24a..c57983b 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -170,20 +170,20 @@
         }
 
         /**
-         * Maybe start bpf.
+         * Start clatd.
          */
-        public int maybeStartBpf(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
                 @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
                 @NonNull String v4, @NonNull String v6) throws IOException {
-            return native_maybeStartBpf(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
+            return native_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
         }
 
         /**
-         * Maybe stop bpf.
+         * Stop clatd.
          */
-        public void maybeStopBpf(String iface, String pfx96, String v4, String v6, int pid)
+        public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
                 throws IOException {
-            native_maybeStopBpf(iface, pfx96, v4, v6, pid);
+            native_stopClatd(iface, pfx96, v4, v6, pid);
         }
     }
 
@@ -219,6 +219,9 @@
     public String clatStart(final String iface, final int netId,
             @NonNull final IpPrefix nat64Prefix)
             throws IOException {
+        if (mIface != null || mPid != INVALID_PID) {
+            throw new IOException("Clatd is already running on " + mIface + " (pid " + mPid + ")");
+        }
         if (nat64Prefix.getPrefixLength() != 96) {
             throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
         }
@@ -254,6 +257,7 @@
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
         }
 
@@ -270,6 +274,7 @@
         try {
             mNetd.interfaceSetMtu(tunIface, mtu);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
         }
         final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -281,6 +286,7 @@
         try {
             mNetd.interfaceSetCfg(ifConfig);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
                     + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
         }
@@ -290,11 +296,12 @@
         final ParcelFileDescriptor readSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket() because we would
-            // like to use ParcelFileDescriptor to close file descriptor automatically. But ctor
+            // like to use ParcelFileDescriptor to manage file descriptor. But ctor
             // ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
             // descriptor to initialize ParcelFileDescriptor object instead.
             readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
         } catch (IOException e) {
+            tunFd.close();
             throw new IOException("Open packet socket failed: " + e);
         }
 
@@ -305,11 +312,16 @@
             // reason why we use jniOpenPacketSocket6().
             writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
             throw new IOException("Open raw socket failed: " + e);
         }
 
         final int ifaceIndex = mDeps.getInterfaceIndex(iface);
         if (ifaceIndex == INVALID_IFINDEX) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("Fail to get interface index for interface " + iface);
         }
 
@@ -317,6 +329,9 @@
         try {
             mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("add anycast sockopt failed: " + e);
         }
 
@@ -324,31 +339,41 @@
         try {
             mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("configure packet socket failed: " + e);
         }
 
-        // [5] Maybe start bpf.
+        // [5] Start clatd.
         try {
-            mDeps.maybeStartBpf(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+            mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
                     writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
             mIface = iface;
             mNat64Prefix = pfx96;
             mXlatLocalAddress4 = v4;
             mXlatLocalAddress6 = v6;
         } catch (IOException e) {
-            throw new IOException("Error start bpf on " + iface + ": " + e);
+            throw new IOException("Error start clatd on " + iface + ": " + e);
+        } finally {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
         }
 
-        // TODO: start clatd and returns local xlat464 v6 address.
-        return null;
+        return v6;
     }
 
     /**
      * Stop clatd
      */
     public void clatStop() throws IOException {
-        mDeps.maybeStopBpf(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6,
-                mPid /* unused */);
+        if (mPid == INVALID_PID) {
+            throw new IOException("Clatd has not started");
+        }
+        Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+
+        mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
         // TODO: remove setIptablesDropRule
 
         Log.i(TAG, "clatd on " + mIface + " stopped");
@@ -357,6 +382,7 @@
         mNat64Prefix = null;
         mXlatLocalAddress4 = null;
         mXlatLocalAddress6 = null;
+        mPid = INVALID_PID;
     }
 
     private static native String native_selectIpv4Address(String v4addr, int prefixlen)
@@ -372,9 +398,9 @@
             int ifindex) throws IOException;
     private static native void native_configurePacketSocket(FileDescriptor sock, String v6,
             int ifindex) throws IOException;
-    private static native int native_maybeStartBpf(FileDescriptor tunfd, FileDescriptor readsock6,
+    private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
             FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
             throws IOException;
-    private static native void native_maybeStopBpf(String iface, String pfx96, String v4,
-            String v6, int pid) throws IOException;
+    private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
+            int pid) throws IOException;
 }
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index d03629d..bea00a9 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -34,6 +34,8 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
@@ -415,6 +417,31 @@
         assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
     }
 
+    @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    public void testPrioritizeLatencyAndBandwidth() {
+        NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testOemPrivate() {
         NetworkCapabilities nc = new NetworkCapabilities();
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 1d1c18e..153ff51 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -43,6 +43,7 @@
     static_libs: [
         "libbpf_android",
         "libgtest",
+        "libmodules-utils-build",
     ],
 
     // Tag this module as a cts test artifact
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 874bad4..97ecb9e 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -21,6 +21,8 @@
 
 #include <gtest/gtest.h>
 
+#include "android-modules-utils/sdk_level.h"
+
 #include "libbpf_android.h"
 
 using namespace android::bpf;
@@ -33,11 +35,17 @@
   EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
 }
 
-TEST(BpfTest, bpfStructSizeTest) {
+TEST(BpfTest, bpfStructSizeTestPreT) {
+  if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device.";
   doBpfStructSizeTest("/system/etc/bpf/netd.o");
   doBpfStructSizeTest("/system/etc/bpf/clatd.o");
 }
 
+TEST(BpfTest, bpfStructSizeTest) {
+  doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
+  doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+}
+
 int main(int argc, char **argv) {
   testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 80c2db4..53e4ab7 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -168,6 +168,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
@@ -3021,6 +3022,13 @@
         }
     }
 
+    @Test
+    public void testDump() throws Exception {
+        final String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
+                Context.CONNECTIVITY_SERVICE, "--short");
+        assertTrue(dumpOutput, dumpOutput.contains("Active default network"));
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 0bef0e2..cb446f1 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -38,8 +38,8 @@
         "liblog",
         "liblzma",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
         "libnetworkstatsfactorytestjni",
         "libpackagelistparser",
         "libpcre2",
@@ -143,6 +143,7 @@
         "ServiceConnectivityResources",
     ],
     visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+    exclude_kotlinc_generated_files: false,
 }
 
 android_test {
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index ec0420e..1b512f0 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -171,7 +171,7 @@
     fun testBuilder_ratType() {
         // Assert illegal ratTypes cannot make an identity.
         listOf(Integer.MIN_VALUE, NetworkTemplate.NETWORK_TYPE_ALL,
-                TelephonyManager.NETWORK_TYPE_UNKNOWN - 1, Integer.MAX_VALUE)
+                NetworkTemplate.NETWORK_TYPE_5G_NSA - 1, Integer.MAX_VALUE)
                 .forEach {
                     assertFailsWith<IllegalArgumentException> {
                         NetworkIdentity.Builder()
@@ -184,6 +184,7 @@
         // Verify legitimate ratTypes can make an identity.
         TelephonyManager.getAllNetworkTypes().toMutableList().also {
             it.add(TelephonyManager.NETWORK_TYPE_UNKNOWN)
+            it.add(NetworkTemplate.NETWORK_TYPE_5G_NSA)
         }.forEach { rat ->
             NetworkIdentity.Builder()
                     .setType(TYPE_MOBILE)
@@ -199,11 +200,11 @@
         // Assert illegal oemManage values cannot make an identity.
         listOf(Integer.MIN_VALUE, NetworkTemplate.OEM_MANAGED_ALL, NetworkTemplate.OEM_MANAGED_YES,
                 Integer.MAX_VALUE)
-                .forEach {
+                .forEach { oemManaged ->
                     assertFailsWith<IllegalArgumentException> {
                         NetworkIdentity.Builder()
                                 .setType(TYPE_MOBILE)
-                                .setRatType(it)
+                                .setOemManaged(oemManaged)
                                 .build()
                     }
                 }
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 2e82986..c27ee93 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -38,11 +38,13 @@
 import static org.junit.Assert.fail;
 
 import android.content.res.Resources;
+import android.net.NetworkStatsCollection.Key;
 import android.os.Process;
 import android.os.UserHandle;
 import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.RecurrenceRule;
 
 import androidx.test.InstrumentationRegistry;
@@ -73,6 +75,7 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Tests for {@link NetworkStatsCollection}.
@@ -530,6 +533,52 @@
         assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
     }
 
+    @Test
+    public void testBuilder() {
+        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final NetworkIdentitySet ident = new NetworkIdentitySet();
+        final Key key1 = new Key(ident, 0, 0, 0);
+        final Key key2 = new Key(ident, 1, 0, 0);
+        final long bucketDuration = 10;
+
+        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
+                4, 50, 5, 60);
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+                41, 7, 1, 0);
+
+        NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry1)
+                .addEntry(entry2)
+                .build();
+
+        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
+
+        NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+                .addEntry(key1, history1)
+                .addEntry(key2, history2)
+                .build();
+
+        // The builder will omit any entry with empty history. Thus, history2
+        // is not expected in the result collection.
+        expectedEntries.put(key1, history1);
+
+        final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+
+        assertEquals(expectedEntries.size(), actualEntries.size());
+        for (Key expectedKey : expectedEntries.keySet()) {
+            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+
+            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+            assertNotNull(actualHistory);
+
+            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+
+            actualEntries.remove(expectedKey);
+        }
+        assertEquals(0, actualEntries.size());
+    }
+
     /**
      * Copy a {@link Resources#openRawResource(int)} into {@link File} for
      * testing purposes.
@@ -587,6 +636,14 @@
                 actual.txBytes, actual.txPackets, 0L));
     }
 
+    private static void assertEntry(NetworkStatsHistory.Entry expected,
+            NetworkStatsHistory.Entry actual) {
+        assertEntry(new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+                actual.txBytes, actual.txPackets, 0L),
+                new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+                actual.txBytes, actual.txPackets, 0L));
+    }
+
     private static void assertEntry(NetworkStats.Entry expected,
             NetworkStats.Entry actual) {
         assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes);
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
new file mode 100644
index 0000000..84e02ce
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class ClatCoordinatorTest {
+    private static final String BASE_IFACE = "test0";
+    private static final String STACKED_IFACE = "v4-test0";
+    private static final int BASE_IFINDEX = 1000;
+
+    private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+    private static final int GOOGLE_DNS_4 = 0x08080808;  // 8.8.8.8
+    private static final int NETID = 42;
+
+    // The test fwmark means: PERMISSION_SYSTEM (0x2), protectedFromVpn: true,
+    // explicitlySelected: true, netid: 42. For bit field structure definition, see union Fwmark in
+    // system/netd/include/Fwmark.h
+    private static final int MARK = 0xb002a;
+
+    private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
+    private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+    private static final int CLATD_PID = 10483;
+
+    private static final int TUN_FD = 534;
+    private static final int RAW_SOCK_FD = 535;
+    private static final int PACKET_SOCK_FD = 536;
+    private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+
+    @Mock private INetd mNetd;
+    @Spy private TestDependencies mDeps = new TestDependencies();
+
+    /**
+      * The dependency injection class is used to mock the JNI functions and system functions
+      * for clatd coordinator control plane. Note that any testing used JNI functions need to
+      * be overridden to avoid calling native methods.
+      */
+    protected class TestDependencies extends ClatCoordinator.Dependencies {
+        /**
+          * Get netd.
+          */
+        @Override
+        public INetd getNetd() {
+            return mNetd;
+        }
+
+        /**
+         * @see ParcelFileDescriptor#adoptFd(int).
+         */
+        @Override
+        public ParcelFileDescriptor adoptFd(int fd) {
+            switch (fd) {
+                case TUN_FD:
+                    return TUN_PFD;
+                case RAW_SOCK_FD:
+                    return RAW_SOCK_PFD;
+                case PACKET_SOCK_FD:
+                    return PACKET_SOCK_PFD;
+                default:
+                    fail("unsupported arg: " + fd);
+                    return null;
+            }
+        }
+
+        /**
+         * Get interface index for a given interface.
+         */
+        @Override
+        public int getInterfaceIndex(String ifName) {
+            if (BASE_IFACE.equals(ifName)) {
+                return BASE_IFINDEX;
+            }
+            fail("unsupported arg: " + ifName);
+            return -1;
+        }
+
+        /**
+         * Create tun interface for a given interface name.
+         */
+        @Override
+        public int createTunInterface(@NonNull String tuniface) throws IOException {
+            if (STACKED_IFACE.equals(tuniface)) {
+                return TUN_FD;
+            }
+            fail("unsupported arg: " + tuniface);
+            return -1;
+        }
+
+        /**
+         * Pick an IPv4 address for clat.
+         */
+        @Override
+        public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                throws IOException {
+            if (INIT_V4ADDR_STRING.equals(v4addr) && INIT_V4ADDR_PREFIX_LEN == prefixlen) {
+                return XLAT_LOCAL_IPV4ADDR_STRING;
+            }
+            fail("unsupported args: " + v4addr + ", " + prefixlen);
+            return null;
+        }
+
+        /**
+         * Generate a checksum-neutral IID.
+         */
+        @Override
+        public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                @NonNull String prefix64) throws IOException {
+            if (BASE_IFACE.equals(iface) && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && NAT64_PREFIX_STRING.equals(prefix64)) {
+                return XLAT_LOCAL_IPV6ADDR_STRING;
+            }
+            fail("unsupported args: " + iface + ", " + v4 + ", " + prefix64);
+            return null;
+        }
+
+        /**
+         * Detect MTU.
+         */
+        @Override
+        public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                throws IOException {
+            if (NAT64_PREFIX_STRING.equals(platSubnet) && GOOGLE_DNS_4 == platSuffix
+                    && MARK == mark) {
+                return ETHER_MTU;
+            }
+            fail("unsupported args: " + platSubnet + ", " + platSuffix + ", " + mark);
+            return -1;
+        }
+
+        /**
+         * Open IPv6 raw socket and set SO_MARK.
+         */
+        @Override
+        public int openRawSocket6(int mark) throws IOException {
+            if (mark == MARK) {
+                return RAW_SOCK_FD;
+            }
+            fail("unsupported arg: " + mark);
+            return -1;
+        }
+
+        /**
+         * Open packet socket.
+         */
+        @Override
+        public int openPacketSocket() throws IOException {
+            // assume that open socket always successfully because there is no argument to check.
+            return PACKET_SOCK_FD;
+        }
+
+        /**
+         * Add anycast setsockopt.
+         */
+        @Override
+        public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Configure packet socket.
+         */
+        @Override
+        public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Start clatd.
+         */
+        @Override
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+                @NonNull String v4, @NonNull String v6) throws IOException {
+            if (Objects.equals(TUN_PFD.getFileDescriptor(), tunfd)
+                    && Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), readsock6)
+                    && Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), writesock6)
+                    && BASE_IFACE.equals(iface)
+                    && NAT64_PREFIX_STRING.equals(pfx96)
+                    && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)) {
+                return CLATD_PID;
+            }
+            fail("unsupported args: " + tunfd + ", " + readsock6 + ", " + writesock6 + ", "
+                    + ", " + iface + ", " + v4 + ", " + v6);
+            return -1;
+        }
+
+        /**
+         * Stop clatd.
+         */
+        public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
+                @NonNull String v6, int pid) throws IOException {
+            if (pid == -1) {
+                fail("unsupported arg: " + pid);
+            }
+        }
+    };
+
+    @NonNull
+    private ClatCoordinator makeClatCoordinator() throws Exception {
+        final ClatCoordinator coordinator = new ClatCoordinator(mDeps);
+        return coordinator;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private boolean assertContainsFlag(String[] flags, String match) {
+        for (String flag : flags) {
+            if (flag.equals(match)) return true;
+        }
+        fail("Missing flag: " + match);
+        return false;
+    }
+
+    @Test
+    public void testStartStopClatd() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final InOrder inOrder = inOrder(mNetd, mDeps);
+        clearInvocations(mNetd, mDeps);
+
+        // [1] Start clatd.
+        final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+
+        // Pick an IPv4 address.
+        inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
+                eq(INIT_V4ADDR_PREFIX_LEN));
+
+        // Generate a checksum-neutral IID.
+        inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING));
+
+        // Open, configure and bring up the tun interface.
+        inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+        inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+        inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+        inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+        inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+                eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                STACKED_IFACE.equals(cfg.ifName)
+                && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+                && (32 == cfg.prefixLength)
+                && "".equals(cfg.hwAddr)
+                && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
+        // Open and configure 464xlat read/write sockets.
+        inOrder.verify(mDeps).openPacketSocket();
+        inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
+        inOrder.verify(mDeps).openRawSocket6(eq(MARK));
+        inOrder.verify(mDeps).adoptFd(eq(RAW_SOCK_FD));
+        inOrder.verify(mDeps).getInterfaceIndex(eq(BASE_IFACE));
+        inOrder.verify(mDeps).addAnycastSetsockopt(
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+        inOrder.verify(mDeps).configurePacketSocket(
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+
+        // Start clatd.
+        inOrder.verify(mDeps).startClatd(
+                argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+        inOrder.verifyNoMoreInteractions();
+
+        // [2] Start clatd again failed.
+        assertThrows("java.io.IOException: Clatd is already running on test0 (pid 10483)",
+                IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+        // [3] Expect clatd to stop successfully.
+        coordinator.clatStop();
+        inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+        inOrder.verifyNoMoreInteractions();
+
+        // [4] Expect an IO exception while stopping a clatd that doesn't exist.
+        assertThrows("java.io.IOException: Clatd has not started", IOException.class,
+                () -> coordinator.clatStop());
+        inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
+                anyString(), anyInt());
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testGetFwmark() throws Exception {
+        assertEquals(0xb0064, ClatCoordinator.getFwmark(100));
+        assertEquals(0xb03e8, ClatCoordinator.getFwmark(1000));
+        assertEquals(0xb2710, ClatCoordinator.getFwmark(10000));
+        assertEquals(0xbffff, ClatCoordinator.getFwmark(65535));
+    }
+
+    @Test
+    public void testAdjustMtu() throws Exception {
+        // Expected mtu is that IPV6_MIN_MTU(1280) minus MTU_DELTA(28).
+        assertEquals(1252, ClatCoordinator.adjustMtu(-1 /* detect mtu failed */));
+        assertEquals(1252, ClatCoordinator.adjustMtu(500));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1000));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1280));
+
+        // Expected mtu is that the detected mtu minus MTU_DELTA(28).
+        assertEquals(1372, ClatCoordinator.adjustMtu(1400));
+        assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+
+        // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 6a7da9e..66dcf6d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -42,7 +42,6 @@
 import android.net.NetworkStats;
 import android.net.NetworkStatsAccess;
 import android.net.NetworkTemplate;
-import android.net.netstats.IUsageCallback;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
@@ -101,7 +100,7 @@
     private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
 
     @Mock private IBinder mUsageCallbackBinder;
-    @Mock private IUsageCallback mUsageCallback;
+    private TestableUsageCallback mUsageCallback;
 
     @Before
     public void setUp() throws Exception {
@@ -119,20 +118,27 @@
 
         mActiveIfaces = new ArrayMap<>();
         mActiveUidIfaces = new ArrayMap<>();
-        Mockito.when(mUsageCallback.asBinder()).thenReturn(mUsageCallbackBinder);
+        mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
     @Test
     public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
-        long thresholdTooLowBytes = 1L;
-        DataUsageRequest inputRequest = new DataUsageRequest(
+        final long thresholdTooLowBytes = 1L;
+        final DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
-        assertTrue(request.requestId > 0);
-        assertTrue(Objects.equals(sTemplateWifi, request.template));
-        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+        final DataUsageRequest requestByApp = mStatsObservers.register(inputRequest, mUsageCallback,
+                UID_RED, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(requestByApp.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
+        assertEquals(THRESHOLD_BYTES, requestByApp.thresholdInBytes);
+
+        // Verify the threshold requested by system uid won't be overridden.
+        final DataUsageRequest requestBySystem = mStatsObservers.register(inputRequest,
+                mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(requestBySystem.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
+        assertEquals(1, requestBySystem.thresholdInBytes);
     }
 
     @Test
@@ -304,7 +310,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        Mockito.verify(mUsageCallback).onThresholdReached(any());
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -337,7 +343,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        Mockito.verify(mUsageCallback).onThresholdReached(any());
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -402,7 +408,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        Mockito.verify(mUsageCallback).onThresholdReached(any());
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 94a4f3d..ea35c31 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -1292,7 +1292,7 @@
 
 
         // Wait for the caller to invoke expectOnThresholdReached.
-        mUsageCallback.expectOnThresholdReached();
+        mUsageCallback.expectOnThresholdReached(request);
 
         // Allow binder to disconnect
         when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()))
@@ -1302,7 +1302,7 @@
         mService.unregisterUsageRequest(request);
 
         // Wait for the caller to invoke expectOnCallbackReleased.
-        mUsageCallback.expectOnCallbackReleased();
+        mUsageCallback.expectOnCallbackReleased(request);
 
         // Make sure that the caller binder gets disconnected
         verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
diff --git a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
index 44f588c..1917ec3 100644
--- a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
+++ b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
@@ -21,37 +21,34 @@
 import android.os.IBinder
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.TimeUnit
-import kotlin.test.assertEquals
 import kotlin.test.fail
 
 private const val DEFAULT_TIMEOUT_MS = 200L
 
 // TODO: Move the class to static libs once all downstream have IUsageCallback definition.
-open class TestableUsageCallback(private val binder: IBinder) : IUsageCallback.Stub() {
-    sealed class CallbackType {
-        object OnThresholdReached : CallbackType()
-        object OnCallbackReleased : CallbackType()
+class TestableUsageCallback(private val binder: IBinder) : IUsageCallback.Stub() {
+    sealed class CallbackType(val request: DataUsageRequest) {
+        class OnThresholdReached(request: DataUsageRequest) : CallbackType(request)
+        class OnCallbackReleased(request: DataUsageRequest) : CallbackType(request)
     }
 
     // TODO: Change to use ArrayTrackRecord once moved into to the module.
     private val history = LinkedBlockingQueue<CallbackType>()
 
     override fun onThresholdReached(request: DataUsageRequest) {
-        history.add(CallbackType.OnThresholdReached)
+        history.add(CallbackType.OnThresholdReached(request))
     }
 
     override fun onCallbackReleased(request: DataUsageRequest) {
-        history.add(CallbackType.OnCallbackReleased)
+        history.add(CallbackType.OnCallbackReleased(request))
     }
 
-    fun expectOnThresholdReached() {
-        assertEquals(CallbackType.OnThresholdReached,
-                history.poll(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+    fun expectOnThresholdReached(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnThresholdReached>(request, DEFAULT_TIMEOUT_MS)
     }
 
-    fun expectOnCallbackReleased() {
-        assertEquals(CallbackType.OnCallbackReleased,
-                history.poll(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+    fun expectOnCallbackReleased(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnCallbackReleased>(request, DEFAULT_TIMEOUT_MS)
     }
 
     @JvmOverloads
@@ -60,6 +57,22 @@
         cb?.let { fail("Expected no callback but got $cb") }
     }
 
+    // Expects a callback of the specified request on the specified network within the timeout.
+    // If no callback arrives, or a different callback arrives, fail.
+    private inline fun <reified T : CallbackType> expectCallback(
+        expectedRequest: DataUsageRequest,
+        timeoutMs: Long
+    ) {
+        history.poll(timeoutMs, TimeUnit.MILLISECONDS).let {
+            if (it !is T || it.request != expectedRequest) {
+                fail("Unexpected callback : $it," +
+                        " expected ${T::class} with Request[$expectedRequest]")
+            } else {
+                it
+            }
+        }
+    }
+
     override fun asBinder(): IBinder {
         return binder
     }
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index fe971e7..0f71c13 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -24,7 +24,7 @@
         "libbpf_android",
         "liblog",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
     ],
 }