Merge changes from topic "libnetworkstats"

* changes:
  Rename libnetdbpf to libnetworkstats
  Copy libnetdbpf from system/net to tethering module
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 44c57ab..06e9617 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,2 @@
 /bin/for-system 0 1000 0550
-/bin/for-system/clatd 1029 1029 06555
+/bin/for-system/clatd 1029 1029 06755
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index a7155fd..fa5af49 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -42,6 +42,7 @@
         // TODO: remove it when NetworkStatsService is moved into the mainline module and no more
         // calls to JNI in libservices.core.
         "//frameworks/base/services/core/jni",
+        "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/modules/Connectivity/service/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 827da6d..a373b71 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -294,7 +294,7 @@
     ctor public NetworkCapabilities(android.net.NetworkCapabilities);
     method public int describeContents();
     method @NonNull public int[] getCapabilities();
-    method @NonNull public int[] getEnterpriseCapabilitySubLevels();
+    method @NonNull public int[] getEnterpriseIds();
     method public int getLinkDownstreamBandwidthKbps();
     method public int getLinkUpstreamBandwidthKbps();
     method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
@@ -302,6 +302,7 @@
     method public int getSignalStrength();
     method @Nullable public android.net.TransportInfo getTransportInfo();
     method public boolean hasCapability(int);
+    method public boolean hasEnterpriseId(int);
     method public boolean hasTransport(int);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
@@ -325,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
@@ -332,6 +335,11 @@
     field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
     field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
     field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
+    field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
+    field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
+    field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
+    field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
+    field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
     field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
     field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
     field public static final int TRANSPORT_CELLULAR = 0; // 0x0
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index ec169b6..db08c48 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -133,11 +133,6 @@
   public final class NetworkCapabilities implements android.os.Parcelable {
     method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
     method public boolean hasForbiddenCapability(int);
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1; // 0x1
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2; // 0x2
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3; // 0x3
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4; // 0x4
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5; // 0x5
     field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
     field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
     field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
@@ -166,6 +161,7 @@
     method @NonNull public java.util.List<java.lang.Integer> getExcludedUids();
     method @NonNull public java.util.List<java.lang.Integer> getIncludedUids();
     method public int getPreference();
+    method public int getPreferenceEnterpriseId();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
   }
@@ -176,6 +172,7 @@
     method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@Nullable java.util.List<java.lang.Integer>);
     method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@Nullable java.util.List<java.lang.Integer>);
     method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
   }
 
   public final class TestNetworkInterface implements android.os.Parcelable {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 8d084e6..b4b3588 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -294,11 +294,11 @@
     ctor public NetworkCapabilities.Builder();
     ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
     method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseCapabilitySubLevel(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
     method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
     method @NonNull public android.net.NetworkCapabilities build();
     method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseCapabilitySubLevel(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
     method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
     method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 009890c..7593482 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,7 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
 import static android.net.NetworkRequest.Type.LISTEN;
 import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
@@ -5537,6 +5538,9 @@
         ProfileNetworkPreference.Builder preferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         preferenceBuilder.setPreference(preference);
+        if (preference != PROFILE_NETWORK_PREFERENCE_DEFAULT) {
+            preferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        }
         setProfileNetworkPreferences(profile,
                 List.of(preferenceBuilder.build()), executor, listener);
     }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 84f7cbb..4ae3a06 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -16,8 +16,6 @@
 
 package android.net;
 
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
 import android.annotation.IntDef;
@@ -149,67 +147,83 @@
     private String mRequestorPackageName;
 
     /**
-     * enterprise capability sub level 1
-     * @hide
+     * Enterprise capability identifier 1.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1;
+    public static final int NET_ENTERPRISE_ID_1 = 1;
 
     /**
-     * enterprise capability sub level 2
-     * @hide
+     * Enterprise capability identifier 2.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2;
+    public static final int NET_ENTERPRISE_ID_2 = 2;
 
     /**
-     * enterprise capability sub level 3
-     * @hide
+     * Enterprise capability identifier 3.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3;
+    public static final int NET_ENTERPRISE_ID_3 = 3;
 
     /**
-     * enterprise capability sub level 4
-     * @hide
+     * Enterprise capability identifier 4.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4;
+    public static final int NET_ENTERPRISE_ID_4 = 4;
 
     /**
-     * enterprise capability sub level 5
-     * @hide
+     * Enterprise capability identifier 5.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5;
+    public static final int NET_ENTERPRISE_ID_5 = 5;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "NET_CAPABILITY_ENTERPRISE_SUB_LEVEL" }, value = {
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
+            NET_ENTERPRISE_ID_1,
+            NET_ENTERPRISE_ID_2,
+            NET_ENTERPRISE_ID_3,
+            NET_ENTERPRISE_ID_4,
+            NET_ENTERPRISE_ID_5,
     })
 
-    public @interface EnterpriseCapabilitySubLevel {
+    public @interface EnterpriseId {
     }
 
     /**
-     * Bitfield representing the network's enterprise capability sublevel.  If any are specified
+     * Bitfield representing the network's enterprise capability identifier.  If any are specified
      * they will be satisfied by any Network that matches all of them.
-     * {@see addEnterpriseCapabilitySubLevel} for details on how masks are added
+     * {@see addEnterpriseId} for details on how masks are added
      */
-    private int mEnterpriseCapabilitySubLevel;
+    private int mEnterpriseId;
 
     /**
-     * @return all the enteprise capabilities sub level set on this {@code NetworkCapability}
-     * instance.
+     * Get enteprise identifiers set.
+     *
+     * Get all the enterprise capabilities identifier set on this {@code NetworkCapability}
+     * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+     * considered to have NET_CAPABILITY_ENTERPRISE by default.
+     * @return all the enterprise capabilities identifier set.
      *
      */
-    public @NonNull @EnterpriseCapabilitySubLevel int[] getEnterpriseCapabilitySubLevels() {
-        return NetworkCapabilitiesUtils.unpackBits(mEnterpriseCapabilitySubLevel);
+    public @NonNull @EnterpriseId int[] getEnterpriseIds() {
+        if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+            return new int[]{NET_ENTERPRISE_ID_1};
+        }
+        return NetworkCapabilitiesUtils.unpackBits(mEnterpriseId);
+    }
+
+    /**
+     * Tests for the presence of an enterprise capability identifier on this instance.
+     *
+     * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+     * considered to have NET_CAPABILITY_ENTERPRISE by default.
+     * @param enterpriseId the enterprise capability identifier to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasEnterpriseId(
+            @EnterpriseId int enterpriseId) {
+        if (enterpriseId == NET_ENTERPRISE_ID_1) {
+            if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+                return true;
+            }
+        }
+        return isValidEnterpriseId(enterpriseId)
+                && ((mEnterpriseId & (1L << enterpriseId)) != 0);
     }
 
     public NetworkCapabilities() {
@@ -258,7 +272,7 @@
         mRequestorPackageName = null;
         mSubIds = new ArraySet<>();
         mUnderlyingNetworks = null;
-        mEnterpriseCapabilitySubLevel = 0;
+        mEnterpriseId = 0;
     }
 
     /**
@@ -291,7 +305,7 @@
         // mUnderlyingNetworks is an unmodifiable list if non-null, so a defensive copy is not
         // necessary.
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
-        mEnterpriseCapabilitySubLevel = nc.mEnterpriseCapabilitySubLevel;
+        mEnterpriseId = nc.mEnterpriseId;
     }
 
     /**
@@ -343,6 +357,8 @@
             NET_CAPABILITY_BIP,
             NET_CAPABILITY_HEAD_UNIT,
             NET_CAPABILITY_MMTEL,
+            NET_CAPABILITY_PRIORITIZE_LATENCY,
+            NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
     })
     public @interface NetCapability { }
 
@@ -586,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
@@ -784,34 +810,34 @@
     }
 
     /**
-     * Adds the given enterprise capability sub level to this {@code NetworkCapability} instance.
-     * Note that when searching for a network to satisfy a request, all capabilities sub level
+     * Adds the given enterprise capability identifier to this {@code NetworkCapability} instance.
+     * Note that when searching for a network to satisfy a request, all capabilities identifier
      * requested must be satisfied.
      *
-     * @param enterpriseCapabilitySubLevel the enterprise capability sub level to be added.
+     * @param enterpriseId the enterprise capability identifier to be added.
      * @return This NetworkCapabilities instance, to facilitate chaining.
      * @hide
      */
-    private @NonNull NetworkCapabilities addEnterpriseCapabilitySubLevel(
-            @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
-        mEnterpriseCapabilitySubLevel |= 1 << enterpriseCapabilitySubLevel;
+    public @NonNull NetworkCapabilities addEnterpriseId(
+            @EnterpriseId int enterpriseId) {
+        checkValidEnterpriseId(enterpriseId);
+        mEnterpriseId |= 1 << enterpriseId;
         return this;
     }
 
     /**
-     * Removes (if found) the given enterprise capability sublevel from this
-     * {@code NetworkCapability} instance that were added via addEnterpriseCapabilitySubLevel(int)
+     * Removes (if found) the given enterprise capability identifier from this
+     * {@code NetworkCapability} instance that were added via addEnterpriseId(int)
      *
-     * @param enterpriseCapabilitySubLevel the enterprise capability sublevel to be removed.
+     * @param enterpriseId the enterprise capability identifier to be removed.
      * @return This NetworkCapabilities instance, to facilitate chaining.
      * @hide
      */
-    private @NonNull NetworkCapabilities removeEnterpriseCapabilitySubLevel(
-            @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-        checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
-        final int mask = ~(1 << enterpriseCapabilitySubLevel);
-        mEnterpriseCapabilitySubLevel &= mask;
+    private @NonNull NetworkCapabilities removeEnterpriseId(
+            @EnterpriseId  int enterpriseId) {
+        checkValidEnterpriseId(enterpriseId);
+        final int mask = ~(1 << enterpriseId);
+        mEnterpriseId &= mask;
         return this;
     }
 
@@ -915,16 +941,19 @@
         return null;
     }
 
-    private boolean equalsEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
-        return nc.mEnterpriseCapabilitySubLevel == this.mEnterpriseCapabilitySubLevel;
+    private boolean equalsEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+        return nc.mEnterpriseId == this.mEnterpriseId;
     }
 
-    private boolean satisfiedByEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
-        final int requestedEnterpriseCapabilitiesSubLevel = mEnterpriseCapabilitySubLevel;
-        final int providedEnterpriseCapabailitiesSubLevel = nc.mEnterpriseCapabilitySubLevel;
+    private boolean satisfiedByEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+        final int requestedEnterpriseCapabilitiesId = mEnterpriseId;
+        final int providedEnterpriseCapabailitiesId = nc.mEnterpriseId;
 
-        if ((providedEnterpriseCapabailitiesSubLevel & requestedEnterpriseCapabilitiesSubLevel)
-                == requestedEnterpriseCapabilitiesSubLevel) {
+        if ((providedEnterpriseCapabailitiesId & requestedEnterpriseCapabilitiesId)
+                == requestedEnterpriseCapabilitiesId) {
+            return true;
+        } else if (providedEnterpriseCapabailitiesId == 0
+                && (requestedEnterpriseCapabilitiesId == (1L << NET_ENTERPRISE_ID_1))) {
             return true;
         } else {
             return false;
@@ -1836,7 +1865,7 @@
                 && satisfiedByTransportTypes(nc)
                 && (onlyImmutable || satisfiedByLinkBandwidths(nc))
                 && satisfiedBySpecifier(nc)
-                && satisfiedByEnterpriseCapabilitiesSubLevel(nc)
+                && satisfiedByEnterpriseCapabilitiesId(nc)
                 && (onlyImmutable || satisfiedBySignalStrength(nc))
                 && (onlyImmutable || satisfiedByUids(nc))
                 && (onlyImmutable || satisfiedBySSID(nc))
@@ -1940,7 +1969,7 @@
                 && equalsAdministratorUids(that)
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
-                && equalsEnterpriseCapabilitiesSubLevel(that);
+                && equalsEnterpriseCapabilitiesId(that);
     }
 
     @Override
@@ -1965,7 +1994,7 @@
                 + Arrays.hashCode(mAdministratorUids) * 61
                 + Objects.hashCode(mSubIds) * 67
                 + Objects.hashCode(mUnderlyingNetworks) * 71
-                + mEnterpriseCapabilitySubLevel * 73;
+                + mEnterpriseId * 73;
     }
 
     @Override
@@ -2001,7 +2030,7 @@
         dest.writeString(mRequestorPackageName);
         dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
         dest.writeTypedList(mUnderlyingNetworks);
-        dest.writeInt(mEnterpriseCapabilitySubLevel);
+        dest.writeInt(mEnterpriseId);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2031,7 +2060,7 @@
                     netCap.mSubIds.add(subIdInts[i]);
                 }
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
-                netCap.mEnterpriseCapabilitySubLevel = in.readInt();
+                netCap.mEnterpriseId = in.readInt();
                 return netCap;
             }
             @Override
@@ -2123,10 +2152,10 @@
             sb.append(" SubscriptionIds: ").append(mSubIds);
         }
 
-        if (0 != mEnterpriseCapabilitySubLevel) {
-            sb.append(" EnterpriseCapabilitySublevel: ");
-            appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseCapabilitySubLevel,
-                    NetworkCapabilities::enterpriseCapabilitySublevelNameOf, "&");
+        if (0 != mEnterpriseId) {
+            sb.append(" EnterpriseId: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseId,
+                    NetworkCapabilities::enterpriseIdNameOf, "&");
         }
 
         sb.append(" UnderlyingNetworks: ");
@@ -2224,11 +2253,13 @@
             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);
         }
     }
 
-    private static @NonNull String enterpriseCapabilitySublevelNameOf(
+    private static @NonNull String enterpriseIdNameOf(
             @NetCapability int capability) {
         return Integer.toString(capability);
     }
@@ -2273,17 +2304,17 @@
         }
     }
 
-    private static boolean isValidEnterpriseCapabilitySubLevel(
-            @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        return enterpriseCapabilitySubLevel >= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1
-                && enterpriseCapabilitySubLevel <= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
+    private static boolean isValidEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        return enterpriseId >= NET_ENTERPRISE_ID_1
+                && enterpriseId <= NET_ENTERPRISE_ID_5;
     }
 
-    private static void checkValidEnterpriseCapabilitySublevel(
-            @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        if (!isValidEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel)) {
-            throw new IllegalArgumentException("enterprise capability sublevel "
-                    + enterpriseCapabilitySubLevel + " is out of range");
+    private static void checkValidEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        if (!isValidEnterpriseId(enterpriseId)) {
+            throw new IllegalArgumentException("enterprise capability identifier "
+                    + enterpriseId + " is out of range");
         }
     }
 
@@ -2613,33 +2644,33 @@
         }
 
         /**
-         * Adds the given enterprise capability sub level.
-         * Note that when searching for a network to satisfy a request, all capabilities sub level
-         * requested must be satisfied. Enterprise capability sub-level is applicable only
+         * Adds the given enterprise capability identifier.
+         * Note that when searching for a network to satisfy a request, all capabilities identifier
+         * requested must be satisfied. Enterprise capability identifier is applicable only
          * for NET_CAPABILITY_ENTERPRISE capability
          *
-         * @param enterpriseCapabilitySubLevel enterprise capability sub-level.
+         * @param enterpriseId enterprise capability identifier.
          *
          * @return this builder
          */
         @NonNull
-        public Builder addEnterpriseCapabilitySubLevel(
-                @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-            mCaps.addEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+        public Builder addEnterpriseId(
+                @EnterpriseId  int enterpriseId) {
+            mCaps.addEnterpriseId(enterpriseId);
             return this;
         }
 
         /**
-         * Removes the given enterprise capability sub level. Enterprise capability sub-level is
+         * Removes the given enterprise capability identifier. Enterprise capability identifier is
          * applicable only for NET_CAPABILITY_ENTERPRISE capability
          *
-         * @param enterpriseCapabilitySubLevel the enterprise capability subLevel
+         * @param enterpriseId the enterprise capability identifier
          * @return this builder
          */
         @NonNull
-        public Builder removeEnterpriseCapabilitySubLevel(
-                @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-            mCaps.removeEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+        public Builder removeEnterpriseId(
+                @EnterpriseId  int enterpriseId) {
+            mCaps.removeEnterpriseId(enterpriseId);
             return this;
         }
 
@@ -2902,10 +2933,10 @@
                 }
             }
 
-            if ((mCaps.getEnterpriseCapabilitySubLevels().length != 0)
+            if ((mCaps.getEnterpriseIds().length != 0)
                     && !mCaps.hasCapability(NET_CAPABILITY_ENTERPRISE)) {
-                throw new IllegalStateException("Enterprise capability sublevel is applicable only"
-                        + " with ENTERPRISE capability.");
+                throw new IllegalStateException("Enterprise capability identifier is applicable"
+                        + " only with ENTERPRISE capability.");
             }
             return new NetworkCapabilities(mCaps);
         }
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index 0571b36..f43acce 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -18,6 +18,8 @@
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -38,12 +40,15 @@
 @SystemApi(client = MODULE_LIBRARIES)
 public final class ProfileNetworkPreference implements Parcelable {
     private final @ProfileNetworkPreferencePolicy int mPreference;
+    private final @NetworkCapabilities.EnterpriseId int mPreferenceEnterpriseId;
     private final List<Integer> mIncludedUids;
     private final List<Integer> mExcludedUids;
 
     private ProfileNetworkPreference(int preference, List<Integer> includedUids,
-            List<Integer> excludedUids) {
+            List<Integer> excludedUids,
+            @NetworkCapabilities.EnterpriseId int preferenceEnterpriseId) {
         mPreference = preference;
+        mPreferenceEnterpriseId = preferenceEnterpriseId;
         if (includedUids != null) {
             mIncludedUids = new ArrayList<>(includedUids);
         } else {
@@ -61,6 +66,7 @@
         mPreference = in.readInt();
         mIncludedUids = in.readArrayList(Integer.class.getClassLoader());
         mExcludedUids = in.readArrayList(Integer.class.getClassLoader());
+        mPreferenceEnterpriseId = in.readInt();
     }
 
     public int getPreference() {
@@ -95,12 +101,30 @@
         return new ArrayList<>(mExcludedUids);
     }
 
+    /**
+     * Get preference enterprise identifier.
+     *
+     * Preference enterprise identifier will be used to create different network preferences
+     * within enterprise preference category.
+     * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+     * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+     * Preference identifier is not applicable if preference is set as
+     * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+     * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+     * @return Preference enterprise identifier.
+     *
+     */
+    public @NetworkCapabilities.EnterpriseId int getPreferenceEnterpriseId() {
+        return mPreferenceEnterpriseId;
+    }
+
     @Override
     public String toString() {
         return "ProfileNetworkPreference{"
                 + "mPreference=" + getPreference()
                 + "mIncludedUids=" + mIncludedUids.toString()
                 + "mExcludedUids=" + mExcludedUids.toString()
+                + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
                 + '}';
     }
 
@@ -111,12 +135,14 @@
         final ProfileNetworkPreference that = (ProfileNetworkPreference) o;
         return mPreference == that.mPreference
                 && (Objects.equals(mIncludedUids, that.mIncludedUids))
-                && (Objects.equals(mExcludedUids, that.mExcludedUids));
+                && (Objects.equals(mExcludedUids, that.mExcludedUids))
+                && mPreferenceEnterpriseId == that.mPreferenceEnterpriseId;
     }
 
     @Override
     public int hashCode() {
         return mPreference
+                + mPreferenceEnterpriseId * 2
                 + (Objects.hashCode(mIncludedUids) * 11)
                 + (Objects.hashCode(mExcludedUids) * 13);
     }
@@ -130,6 +156,7 @@
                 PROFILE_NETWORK_PREFERENCE_DEFAULT;
         private @NonNull List<Integer> mIncludedUids = new ArrayList<>();
         private @NonNull List<Integer> mExcludedUids = new ArrayList<>();
+        private int mPreferenceEnterpriseId;
 
         /**
          * Constructs an empty Builder with PROFILE_NETWORK_PREFERENCE_DEFAULT profile preference
@@ -191,7 +218,24 @@
             return this;
         }
 
-         /**
+        /**
+         * Check if given preference enterprise identifier is valid
+         *
+         * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+         * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+         * @return True if valid else false
+         * @hide
+         */
+        private boolean isEnterpriseIdentifierValid(
+                @NetworkCapabilities.EnterpriseId int identifier) {
+            if ((identifier >= NET_ENTERPRISE_ID_1)
+                    && (identifier <= NET_ENTERPRISE_ID_5)) {
+                return true;
+            }
+            return false;
+        }
+
+        /**
          * Returns an instance of {@link ProfileNetworkPreference} created from the
          * fields set on this builder.
          */
@@ -201,7 +245,35 @@
                 throw new IllegalArgumentException("Both includedUids and excludedUids "
                         + "cannot be nonempty");
             }
-            return new ProfileNetworkPreference(mPreference, mIncludedUids, mExcludedUids);
+
+            if (((mPreference != PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    && (!isEnterpriseIdentifierValid(mPreferenceEnterpriseId)))
+                    || ((mPreference == PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    && (mPreferenceEnterpriseId != 0))) {
+                throw new IllegalStateException("Invalid preference enterprise identifier");
+            }
+            return new ProfileNetworkPreference(mPreference, mIncludedUids,
+                    mExcludedUids, mPreferenceEnterpriseId);
+        }
+
+        /**
+         * Set the preference enterprise identifier.
+         *
+         * Preference enterprise identifier will be used to create different network preferences
+         * within enterprise preference category.
+         * Valid values starts from NetworkCapabilities.NET_ENTERPRISE_ID_1 to
+         * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+         * Preference identifier is not applicable if preference is set as
+         * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+         * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+         * @param preferenceId  preference sub level
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setPreferenceEnterpriseId(
+                @NetworkCapabilities.EnterpriseId int preferenceId) {
+            mPreferenceEnterpriseId = preferenceId;
+            return this;
         }
     }
 
@@ -210,6 +282,7 @@
         dest.writeInt(mPreference);
         dest.writeList(mIncludedUids);
         dest.writeList(mExcludedUids);
+        dest.writeInt(mPreferenceEnterpriseId);
     }
 
     @Override
diff --git a/service/Android.bp b/service/Android.bp
index e376ff7..d1a9004 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -65,6 +65,7 @@
         "libbase_headers",
     ],
     static_libs: [
+        "libbase",
         "libclat",
         "libip_checksum",
         "libnetjniutils",
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index e5d1a88..cc0cbcc 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -90,5 +90,9 @@
 # From services-connectivity-shared-srcs
 rule android.net.util.NetworkConstants* com.android.connectivity.@0
 
+# From modules-utils-statemachine
+rule com.android.internal.util.IState* com.android.connectivity.@0
+rule com.android.internal.util.State* com.android.connectivity.@0
+
 # Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
 # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index a9d7946..9603139 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -19,17 +19,22 @@
 #include <fcntl.h>
 #include <linux/if_tun.h>
 #include <linux/ioctl.h>
+#include <log/log.h>
 #include <nativehelper/JNIHelp.h>
 #include <net/if.h>
+#include <string>
 
 #include <netjniutils/netjniutils.h>
 
+#include "libclat/bpfhelper.h"
 #include "libclat/clatutils.h"
 #include "nativehelper/scoped_utf_chars.h"
 
 // Sync from system/netd/include/netid_client.h
 #define MARK_UNSET 0u
 
+#define DEVICEPREFIX "v4-"
+
 namespace android {
 static void throwIOException(JNIEnv* env, const char* msg, int error) {
     jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
@@ -237,6 +242,86 @@
     }
 }
 
+int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4,
+        const std::string& v6, net::clat::ClatdTracker* output) {
+    strlcpy(output->iface, iface.c_str(), sizeof(output->iface));
+    output->ifIndex = if_nametoindex(iface.c_str());
+    if (output->ifIndex == 0) {
+        ALOGE("interface %s not found", output->iface);
+        return -1;
+    }
+
+    unsigned len = snprintf(output->v4iface, sizeof(output->v4iface),
+            "%s%s", DEVICEPREFIX, iface.c_str());
+    if (len >= sizeof(output->v4iface)) {
+        ALOGE("interface name too long '%s'", output->v4iface);
+        return -1;
+    }
+
+    output->v4ifIndex = if_nametoindex(output->v4iface);
+    if (output->v4ifIndex == 0) {
+        ALOGE("v4-interface %s not found", output->v4iface);
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) {
+        ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str());
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) {
+        ALOGE("Invalid IPv4 address %s", v4.c_str());
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) {
+        ALOGE("Invalid source address %s", v6.c_str());
+        return -1;
+    }
+
+    return 0;
+}
+
+// TODO: fork clatd and rename to .._startClatd.
+static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf(
+        JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
+        jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
+    ScopedUtfChars ifaceStr(env, iface);
+    ScopedUtfChars pfx96Str(env, pfx96);
+    ScopedUtfChars v4Str(env, v4);
+    ScopedUtfChars v6Str(env, v6);
+
+    // 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(),
+                &tracker)) {
+            net::clat::maybeStartBpf(tracker);
+        }
+    }
+
+    return 0; // TODO: return forked clatd 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 */) {
+    ScopedUtfChars ifaceStr(env, iface);
+    ScopedUtfChars pfx96Str(env, pfx96);
+    ScopedUtfChars v4Str(env, v4);
+    ScopedUtfChars v6Str(env, v6);
+
+    if (!net::clat::initMaps()) {
+        net::clat::ClatdTracker tracker = {};
+        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
+                &tracker)) {
+            net::clat::maybeStopBpf(tracker);
+        }
+    }
+}
+
 /*
  * JNI registration.
  */
@@ -259,6 +344,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",
+         "(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",
+         "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
+          (void*)com_android_server_connectivity_ClatCoordinator_maybeStopBpf},
 };
 
 int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 8540787..5e208d8 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -19,9 +19,22 @@
 cc_library_static {
     name: "libclat",
     defaults: ["netd_defaults"],
-    srcs: ["clatutils.cpp"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "bpf_headers",
+        "bpf_syscall_wrappers",
+    ],
+    srcs: [
+        "TcUtils.cpp",  // TODO: move to frameworks/libs/net
+        "bpfhelper.cpp",
+        "clatutils.cpp",
+    ],
     stl: "libc++_static",
-    static_libs: ["libip_checksum"],
+    static_libs: [
+        "libbase",
+        "libip_checksum",
+        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
+    ],
     shared_libs: ["liblog"],
     export_include_dirs: ["include"],
     min_sdk_version: "30",
@@ -32,7 +45,13 @@
     name: "libclat_test",
     defaults: ["netd_defaults"],
     test_suites: ["device-tests"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "bpf_headers",
+        "bpf_syscall_wrappers",
+    ],
     srcs: [
+        "TcUtilsTest.cpp",
         "clatutils_test.cpp",
     ],
     static_libs: [
@@ -40,10 +59,12 @@
         "libclat",
         "libip_checksum",
         "libnetd_test_tun_interface",
+        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
+        "libtcutils",
     ],
     shared_libs: [
         "liblog",
         "libnetutils",
     ],
     require_root: true,
-}
\ No newline at end of file
+}
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp
new file mode 100644
index 0000000..cdfb763
--- /dev/null
+++ b/service/native/libs/libclat/TcUtils.cpp
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "TcUtils"
+
+#include "libclat/TcUtils.h"
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <log/log.h>
+
+#include "android-base/unique_fd.h"
+
+namespace android {
+namespace net {
+
+using std::max;
+
+// Sync from system/netd/server/NetlinkCommands.h
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+
+static int doSIOCGIF(const std::string& interface, int opt) {
+    base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
+
+    if (ufd < 0) {
+        const int err = errno;
+        ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)");
+        return -err;
+    };
+
+    struct ifreq ifr = {};
+    // We use strncpy() instead of strlcpy() since kernel has to be able
+    // to handle non-zero terminated junk passed in by userspace anyway,
+    // and this way too long interface names (more than IFNAMSIZ-1 = 15
+    // characters plus terminating NULL) will not get truncated to 15
+    // characters and zero-terminated and thus potentially erroneously
+    // match a truncated interface if one were to exist.
+    strncpy(ifr.ifr_name, interface.c_str(), sizeof(ifr.ifr_name));
+
+    if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno;
+
+    if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family;
+    if (opt == SIOCGIFMTU) return ifr.ifr_mtu;
+    return -EINVAL;
+}
+
+int hardwareAddressType(const std::string& interface) {
+    return doSIOCGIF(interface, SIOCGIFHWADDR);
+}
+
+int deviceMTU(const std::string& interface) {
+    return doSIOCGIF(interface, SIOCGIFMTU);
+}
+
+base::Result<bool> isEthernet(const std::string& interface) {
+    int rv = hardwareAddressType(interface);
+    if (rv < 0) {
+        errno = -rv;
+        return ErrnoErrorf("Get hardware address type of interface {} failed", interface);
+    }
+
+    switch (rv) {
+        case ARPHRD_ETHER:
+            return true;
+        case ARPHRD_NONE:
+        case ARPHRD_RAWIP:  // in Linux 4.14+ rmnet support was upstreamed and this is 519
+        case 530:           // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
+            return false;
+        default:
+            errno = EAFNOSUPPORT;  // Address family not supported
+            return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface);
+    }
+}
+
+// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol)
+// and //system/netd/server/SockDiag.cpp:checkError(fd)
+static int sendAndProcessNetlinkResponse(const void* req, int len) {
+    base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
+    if (fd == -1) {
+        const int err = errno;
+        ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)");
+        return -err;
+    }
+
+    static constexpr int on = 1;
+    int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
+    if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+
+    // this is needed to get sane strace netlink parsing, it allocates the pid
+    rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+    if (rv) {
+        const int err = errno;
+        ALOGE("bind(fd, {AF_NETLINK, 0, 0})");
+        return -err;
+    }
+
+    // we do not want to receive messages from anyone besides the kernel
+    rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+    if (rv) {
+        const int err = errno;
+        ALOGE("connect(fd, {AF_NETLINK, 0, 0})");
+        return -err;
+    }
+
+    rv = send(fd, req, len, 0);
+    if (rv == -1) return -errno;
+    if (rv != len) return -EMSGSIZE;
+
+    struct {
+        nlmsghdr h;
+        nlmsgerr e;
+        char buf[256];
+    } resp = {};
+
+    rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+    if (rv == -1) {
+        const int err = errno;
+        ALOGE("recv() failed");
+        return -err;
+    }
+
+    if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+        ALOGE("recv() returned short packet: %d", rv);
+        return -EMSGSIZE;
+    }
+
+    if (resp.h.nlmsg_len != (unsigned)rv) {
+        ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv);
+        return -EBADMSG;
+    }
+
+    if (resp.h.nlmsg_type != NLMSG_ERROR) {
+        ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+        return -EBADMSG;
+    }
+
+    return resp.e.error;  // returns 0 on success
+}
+
+// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
+// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
+// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
+    // This is the name of the qdisc we are attaching.
+    // Some hoop jumping to make this compile time constant with known size,
+    // so that the structure declaration is well defined at compile time.
+#define CLSACT "clsact"
+    // sizeof() includes the terminating NULL
+    static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
+
+    const struct {
+        nlmsghdr n;
+        tcmsg t;
+        struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
+        } kind;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = nlMsgType,
+                            .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
+                            .tcm_parent = TC_H_CLSACT,
+                    },
+            .kind =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
+                                            .nla_type = TCA_KIND,
+                                    },
+                            .str = CLSACT,
+                    },
+    };
+#undef CLSACT
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) {
+    // This is the name of the filter we're attaching (ie. this is the 'bpf'
+    // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF.
+    //
+    // We go through some hoops in order to make this compile time constants
+    // so that we can define the struct further down the function with the
+    // field for this sized correctly already during the build.
+#define BPF "bpf"
+    // sizeof() includes the terminating NULL
+    static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF);
+
+    // This is to replicate program name suffix used by 'tc' Linux cli
+    // when it attaches programs.
+#define FSOBJ_SUFFIX ":[*fsobj]"
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj]
+    // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces.
+    // (also compatible with anything that has 0 size L2 header)
+    static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_ingress6_clat_ether:[*fsobj]
+    // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces.
+    // (also compatible with anything that has standard ethernet header)
+    static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_egress4_clat_rawip:[*fsobj]
+    // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces.
+    // (also compatible with anything that has 0 size L2 header)
+    static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_egress4_clat_ether:[*fsobj]
+    // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces.
+    // (also compatible with anything that has standard ethernet header)
+    static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+#undef FSOBJ_SUFFIX
+
+    // The actual name we'll use is determined at run time via 'ethernet' and 'ingress'
+    // booleans.  We need to compile time allocate enough space in the struct
+    // hence this macro magic to make sure we have enough space for either
+    // possibility.  In practice some of these are actually the same size.
+    static constexpr size_t ASCIIZ_MAXLEN_NAME = max({
+            sizeof(name_clat_rx_rawip),
+            sizeof(name_clat_rx_ether),
+            sizeof(name_clat_tx_rawip),
+            sizeof(name_clat_tx_ether),
+    });
+
+    // These are not compile time constants: 'name' is used in strncpy below
+    const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip;
+    const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip;
+    const char* const name = ingress ? name_clat_rx : name_clat_tx;
+
+    struct {
+        nlmsghdr n;
+        tcmsg t;
+        struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)];
+        } kind;
+        struct {
+            nlattr attr;
+            struct {
+                nlattr attr;
+                __u32 u32;
+            } fd;
+            struct {
+                nlattr attr;
+                char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)];
+            } name;
+            struct {
+                nlattr attr;
+                __u32 u32;
+            } flags;
+        } options;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = RTM_NEWTFILTER,
+                            .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_UNSPEC,
+                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+                            .tcm_info = static_cast<__u32>((PRIO_CLAT << 16) | htons(proto)),
+                    },
+            .kind =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = sizeof(req.kind),
+                                            .nla_type = TCA_KIND,
+                                    },
+                            .str = BPF,
+                    },
+            .options =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = sizeof(req.options),
+                                            .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+                                    },
+                            .fd =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.fd),
+                                                            .nla_type = TCA_BPF_FD,
+                                                    },
+                                            .u32 = static_cast<__u32>(bpfFd),
+                                    },
+                            .name =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.name),
+                                                            .nla_type = TCA_BPF_NAME,
+                                                    },
+                                            // Visible via 'tc filter show', but
+                                            // is overwritten by strncpy below
+                                            .str = "placeholder",
+                                    },
+                            .flags =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.flags),
+                                                            .nla_type = TCA_BPF_FLAGS,
+                                                    },
+                                            .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+                                    },
+                    },
+    };
+#undef BPF
+
+    strncpy(req.options.name.str, name, sizeof(req.options.name.str));
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter del dev .. in/egress prio 4 protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
+    const struct {
+        nlmsghdr n;
+        tcmsg t;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = RTM_DELTFILTER,
+                            .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_UNSPEC,
+                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+                            .tcm_info = (static_cast<uint32_t>(prio) << 16) |
+                                        static_cast<uint32_t>(htons(proto)),
+                    },
+    };
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp
new file mode 100644
index 0000000..08f3042
--- /dev/null
+++ b/service/native/libs/libclat/TcUtilsTest.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * TcUtilsTest.cpp - unit tests for TcUtils.cpp
+ */
+
+#include <gtest/gtest.h>
+
+#include "libclat/TcUtils.h"
+
+#include <linux/if_arp.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+class TcUtilsTest : public ::testing::Test {
+  public:
+    void SetUp() {}
+};
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) {
+    ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) {
+    ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo"));
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) {
+    int type = hardwareAddressType("wlan0");
+    if (type == -ENODEV) return;
+
+    ASSERT_EQ(ARPHRD_ETHER, type);
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) {
+    int type = hardwareAddressType("rmnet_data0");
+    if (type == -ENODEV) return;
+
+    ASSERT_NE(ARPHRD_ETHER, type);
+
+    // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices.
+    if (type == 530) return;
+
+    ASSERT_EQ(ARPHRD_RAWIP, type);
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) {
+    auto res = isEthernet("not_existing_if");
+    ASSERT_FALSE(res.ok());
+    ASSERT_EQ(ENODEV, res.error().code());
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfLoopback) {
+    auto res = isEthernet("lo");
+    ASSERT_FALSE(res.ok());
+    ASSERT_EQ(EAFNOSUPPORT, res.error().code());
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+// See also HardwareAddressTypeOfWireless.
+TEST_F(TcUtilsTest, IsEthernetOfWireless) {
+    auto res = isEthernet("wlan0");
+    if (!res.ok() && res.error().code() == ENODEV) return;
+
+    ASSERT_RESULT_OK(res);
+    ASSERT_TRUE(res.value());
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+// See also HardwareAddressTypeOfCellular.
+TEST_F(TcUtilsTest, IsEthernetOfCellular) {
+    auto res = isEthernet("rmnet_data0");
+    if (!res.ok() && res.error().code() == ENODEV) return;
+
+    ASSERT_RESULT_OK(res);
+    ASSERT_FALSE(res.value());
+}
+
+TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) {
+    ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, DeviceMTUofLoopback) {
+    ASSERT_EQ(65536, deviceMTU("lo"));
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4MapFd) {
+    int fd = getClatEgress4MapFd();
+    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) {
+    int fd = getClatEgress4ProgFd(RAWIP);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) {
+    int fd = getClatEgress4ProgFd(ETHER);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6MapFd) {
+    int fd = getClatIngress6MapFd();
+    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) {
+    int fd = getClatIngress6ProgFd(RAWIP);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) {
+    int fd = getClatIngress6ProgFd(ETHER);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+// See Linux kernel source in include/net/flow.h
+#define LOOPBACK_IFINDEX 1
+
+TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) {
+    // This attaches and detaches a configuration-less and thus no-op clsact
+    // qdisc to loopback interface (and it takes fractions of a second)
+    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+}
+
+static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) {
+    // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation...
+    const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+    int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet);
+    ASSERT_GE(clatBpfFd, 3);
+
+    // This attaches and detaches a clsact plus ebpf program to loopback
+    // interface, but it should not affect traffic by virtue of us not
+    // actually populating the ebpf control map.
+    // Furthermore: it only takes fractions of a second.
+    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    if (ingress) {
+        EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+        EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    } else {
+        EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+        EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    }
+    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+
+    close(clatBpfFd);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) {
+    checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) {
+    checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) {
+    checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) {
+    checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER);
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp
new file mode 100644
index 0000000..6e230d0
--- /dev/null
+++ b/service/native/libs/libclat/bpfhelper.cpp
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * main.c - main function
+ */
+#define LOG_TAG "bpfhelper"
+
+#include "libclat/bpfhelper.h"
+
+#include <android-base/unique_fd.h>
+#include <log/log.h>
+
+#include "bpf/BpfMap.h"
+#include "libclat/TcUtils.h"
+
+#define DEVICEPREFIX "v4-"
+
+using android::base::unique_fd;
+using android::net::RAWIP;
+using android::net::getClatEgress4MapFd;
+using android::net::getClatIngress6MapFd;
+using android::net::getClatEgress4ProgFd;
+using android::net::getClatIngress6ProgFd;
+using android::net::tcQdiscAddDevClsact;
+using android::net::tcFilterAddDevEgressClatIpv4;
+using android::net::tcFilterAddDevIngressClatIpv6;
+using android::net::tcFilterDelDevEgressClatIpv4;
+using android::net::tcFilterDelDevIngressClatIpv6;
+using android::bpf::BpfMap;
+
+BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map;
+BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map;
+
+namespace android {
+namespace net {
+namespace clat {
+
+// TODO: have a clearMap function to remove all stubs while system server crash.
+// For long term, move bpf access into java and map initialization should live
+// ClatCoordinator constructor.
+int initMaps(void) {
+    int rv = getClatEgress4MapFd();
+    if (rv < 0) {
+        ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv));
+        return -rv;
+    }
+    mClatEgress4Map.reset(rv);
+
+    rv = getClatIngress6MapFd();
+    if (rv < 0) {
+        ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv));
+        mClatEgress4Map.reset(-1);
+        return -rv;
+    }
+    mClatIngress6Map.reset(rv);
+
+    return 0;
+}
+
+void maybeStartBpf(const ClatdTracker& tracker) {
+    auto isEthernet = android::net::isEthernet(tracker.iface);
+    if (!isEthernet.ok()) {
+        ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex,
+              isEthernet.error().message().c_str());
+        return;
+    }
+
+    // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
+    int rv = getClatEgress4ProgFd(RAWIP);
+    if (rv < 0) {
+        ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv));
+        return;
+    }
+    unique_fd txRawIpProgFd(rv);
+
+    rv = getClatIngress6ProgFd(isEthernet.value());
+    if (rv < 0) {
+        ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv));
+        return;
+    }
+    unique_fd rxProgFd(rv);
+
+    ClatEgress4Key txKey = {
+            .iif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+    ClatEgress4Value txValue = {
+            .oif = tracker.ifIndex,
+            .local6 = tracker.v6,
+            .pfx96 = tracker.pfx96,
+            .oifIsEthernet = isEthernet.value(),
+    };
+
+    auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY);
+    if (!ret.ok()) {
+        ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    ClatIngress6Key rxKey = {
+            .iif = tracker.ifIndex,
+            .pfx96 = tracker.pfx96,
+            .local6 = tracker.v6,
+    };
+    ClatIngress6Value rxValue = {
+            // TODO: move all the clat code to eBPF and remove the tun interface entirely.
+            .oif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+
+    ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY);
+    if (!ret.ok()) {
+        ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code()));
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    // We do tc setup *after* populating the maps, so scanning through them
+    // can always be used to tell us what needs cleanup.
+
+    // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork.
+    // But clat is started before the v4- interface is added to the network. The clat startup have
+    // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
+    // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController.
+    rv = tcQdiscAddDevClsact(tracker.v4ifIndex);
+    if (rv) {
+        ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface,
+              strerror(-rv));
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP);
+    if (rv) {
+        ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex,
+              tracker.v4iface, strerror(-rv));
+
+        // The v4- interface clsact is not deleted for unwinding error because once it is created
+        // with interface addition, the lifetime is till interface deletion. Moreover, the clsact
+        // has no clat filter now. It should not break anything.
+
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value());
+    if (rv) {
+        ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex,
+              tracker.iface, isEthernet.value(), strerror(-rv));
+        rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+        if (rv) {
+            ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+                  tracker.v4iface, strerror(-rv));
+        }
+
+        // The v4- interface clsact is not deleted. See the reason in the error unwinding code of
+        // the egress filter attaching of v4- tun interface.
+
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    // success
+}
+
+void maybeStopBpf(const ClatdTracker& tracker) {
+    int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex);
+    if (rv < 0) {
+        ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface,
+              strerror(-rv));
+    }
+
+    rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+    if (rv < 0) {
+        ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+              tracker.v4iface, strerror(-rv));
+    }
+
+    // We cleanup the maps last, so scanning through them can be used to
+    // determine what still needs cleanup.
+
+    ClatEgress4Key txKey = {
+            .iif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+
+    auto ret = mClatEgress4Map.deleteValue(txKey);
+    if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+
+    ClatIngress6Key rxKey = {
+            .iif = tracker.ifIndex,
+            .pfx96 = tracker.pfx96,
+            .local6 = tracker.v6,
+    };
+
+    ret = mClatIngress6Map.deleteValue(rxKey);
+    if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+}
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h
new file mode 100644
index 0000000..212838e
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/TcUtils.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+
+#include <string>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+// For better code clarity - do not change values - used for booleans like
+// with_ethernet_header or isEthernet.
+constexpr bool RAWIP = false;
+constexpr bool ETHER = true;
+
+// For better code clarity when used for 'bool ingress' parameter.
+constexpr bool EGRESS = false;
+constexpr bool INGRESS = true;
+
+// The priority of clat hook - must be after tethering.
+constexpr uint16_t PRIO_CLAT = 4;
+
+// this returns an ARPHRD_* constant or a -errno
+int hardwareAddressType(const std::string& interface);
+
+// return MTU or -errno
+int deviceMTU(const std::string& interface);
+
+base::Result<bool> isEthernet(const std::string& interface);
+
+inline int getClatEgress4MapFd(void) {
+    const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatEgress4ProgFd(bool with_ethernet_header) {
+    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH
+                                                             : CLAT_EGRESS4_PROG_RAWIP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6MapFd(void) {
+    const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6ProgFd(bool with_ethernet_header) {
+    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH
+                                                             : CLAT_INGRESS6_PROG_RAWIP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
+
+inline int tcQdiscAddDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
+}
+
+inline int tcQdiscReplaceDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
+}
+
+inline int tcQdiscDelDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet);
+
+// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) {
+    return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet);
+}
+
+// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) {
+    return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
+
+// tc filter del dev .. ingress prio 4 protocol ipv6
+inline int tcFilterDelDevIngressClatIpv6(int ifIndex) {
+    return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6);
+}
+
+// tc filter del dev .. egress prio 4 protocol ip
+inline int tcFilterDelDevEgressClatIpv4(int ifIndex) {
+    return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP);
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h
new file mode 100644
index 0000000..c0328c0
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/bpfhelper.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+
+namespace android {
+namespace net {
+namespace clat {
+
+struct ClatdTracker {
+    unsigned ifIndex;
+    char iface[IFNAMSIZ];
+    unsigned v4ifIndex;
+    char v4iface[IFNAMSIZ];
+    in_addr v4;
+    in6_addr v6;
+    in6_addr pfx96;
+};
+
+int initMaps(void);
+void maybeStartBpf(const ClatdTracker& tracker);
+void maybeStopBpf(const ClatdTracker& tracker);
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6c27c4a..ae6fe11 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -73,6 +73,8 @@
 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_VALIDATED;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -10147,11 +10149,19 @@
             switch (preference.getPreference()) {
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
                     nc = null;
+                    if (preference.getPreferenceEnterpriseId() != 0) {
+                        throw new IllegalArgumentException(
+                                "Invalid enterprise identifier in setProfileNetworkPreferences");
+                    }
                     break;
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK:
                     allowFallback = false;
                     // continue to process the enterprise preference.
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                    if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
+                        throw new IllegalArgumentException(
+                                "Invalid enterprise identifier in setProfileNetworkPreferences");
+                    }
                     final Set<UidRange> uidRangeSet =
                             getUidListToBeAppliedForNetworkPreference(profile, preference);
                     if (!isRangeAlreadyInPreferenceList(preferenceList, uidRangeSet)) {
@@ -10161,6 +10171,8 @@
                                 "Overlapping uid range in setProfileNetworkPreferences");
                     }
                     nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+                    nc.addEnterpriseId(
+                            preference.getPreferenceEnterpriseId());
                     nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
                     break;
                 default:
@@ -10203,6 +10215,15 @@
         return uidRangeSet;
     }
 
+    private boolean isEnterpriseIdentifierValid(
+            @NetworkCapabilities.EnterpriseId int identifier) {
+        if ((identifier >= NET_ENTERPRISE_ID_1)
+                && (identifier <= NET_ENTERPRISE_ID_5)) {
+            return true;
+        }
+        return false;
+    }
+
     private void validateNetworkCapabilitiesOfProfileNetworkPreference(
             @Nullable final NetworkCapabilities nc) {
         if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 4d243c4..5a5e24a 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -74,6 +74,12 @@
     private final Dependencies mDeps;
     @Nullable
     private String mIface = null;
+    @Nullable
+    private String mNat64Prefix = null;
+    @Nullable
+    private String mXlatLocalAddress4 = null;
+    @Nullable
+    private String mXlatLocalAddress6 = null;
     private int mPid = INVALID_PID;
 
     @VisibleForTesting
@@ -162,6 +168,23 @@
                 throws IOException {
             native_configurePacketSocket(sock, v6, ifindex);
         }
+
+        /**
+         * Maybe start bpf.
+         */
+        public int maybeStartBpf(@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);
+        }
+
+        /**
+         * Maybe stop bpf.
+         */
+        public void maybeStopBpf(String iface, String pfx96, String v4, String v6, int pid)
+                throws IOException {
+            native_maybeStopBpf(iface, pfx96, v4, v6, pid);
+        }
     }
 
     @VisibleForTesting
@@ -304,10 +327,38 @@
             throw new IOException("configure packet socket failed: " + e);
         }
 
+        // [5] Maybe start bpf.
+        try {
+            mDeps.maybeStartBpf(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);
+        }
+
         // TODO: start clatd and returns local xlat464 v6 address.
         return null;
     }
 
+    /**
+     * Stop clatd
+     */
+    public void clatStop() throws IOException {
+        mDeps.maybeStopBpf(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6,
+                mPid /* unused */);
+        // TODO: remove setIptablesDropRule
+
+        Log.i(TAG, "clatd on " + mIface + " stopped");
+
+        mIface = null;
+        mNat64Prefix = null;
+        mXlatLocalAddress4 = null;
+        mXlatLocalAddress6 = null;
+    }
+
     private static native String native_selectIpv4Address(String v4addr, int prefixlen)
             throws IOException;
     private static native String native_generateIpv6Address(String iface, String v4,
@@ -321,4 +372,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,
+            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;
 }
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 09d36e5..bea00a9 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -23,11 +23,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
@@ -39,9 +34,16 @@
 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;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -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();
@@ -790,78 +817,79 @@
     }
 
     @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    public void testEnterpriseCapabilitySubLevel() {
+    public void testEnterpriseId() {
         final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
                 .build();
-        assertEquals(1, nc1.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc1.getEnterpriseCapabilitySubLevels()[0]);
+        assertEquals(1, nc1.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc1.getEnterpriseIds()[0]);
         final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
-        assertEquals(2, nc2.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc2.getEnterpriseCapabilitySubLevels()[0]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-                nc2.getEnterpriseCapabilitySubLevels()[1]);
+        assertEquals(2, nc2.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc2.getEnterpriseIds()[0]);
+        assertEquals(NET_ENTERPRISE_ID_2,
+                nc2.getEnterpriseIds()[1]);
         final NetworkCapabilities nc3 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_3)
+                .addEnterpriseId(NET_ENTERPRISE_ID_4)
+                .addEnterpriseId(NET_ENTERPRISE_ID_5)
                 .build();
-        assertEquals(5, nc3.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc3.getEnterpriseCapabilitySubLevels()[0]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-                nc3.getEnterpriseCapabilitySubLevels()[1]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
-                nc3.getEnterpriseCapabilitySubLevels()[2]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
-                nc3.getEnterpriseCapabilitySubLevels()[3]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
-                nc3.getEnterpriseCapabilitySubLevels()[4]);
+        assertEquals(5, nc3.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc3.getEnterpriseIds()[0]);
+        assertEquals(NET_ENTERPRISE_ID_2,
+                nc3.getEnterpriseIds()[1]);
+        assertEquals(NET_ENTERPRISE_ID_3,
+                nc3.getEnterpriseIds()[2]);
+        assertEquals(NET_ENTERPRISE_ID_4,
+                nc3.getEnterpriseIds()[3]);
+        assertEquals(NET_ENTERPRISE_ID_5,
+                nc3.getEnterpriseIds()[4]);
 
         final Class<IllegalArgumentException> illegalArgumentExceptionClass =
                 IllegalArgumentException.class;
         assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
-                .addEnterpriseCapabilitySubLevel(6)
+                .addEnterpriseId(6)
                 .build());
         assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
-                .removeEnterpriseCapabilitySubLevel(6)
+                .removeEnterpriseId(6)
                 .build());
 
         final Class<IllegalStateException> illegalStateException =
                 IllegalStateException.class;
         assertThrows(illegalStateException, () -> new NetworkCapabilities.Builder()
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
                 .build());
 
         final NetworkCapabilities nc4 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
-        assertEquals(0, nc4.getEnterpriseCapabilitySubLevels().length);
+        assertEquals(1, nc4.getEnterpriseIds().length);
+        assertTrue(nc4.hasEnterpriseId(NET_ENTERPRISE_ID_1));
 
         final NetworkCapabilities nc5 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_CBS)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
 
         assertTrue(nc4.satisfiedByNetworkCapabilities(nc1));
-        assertFalse(nc1.satisfiedByNetworkCapabilities(nc4));
+        assertTrue(nc1.satisfiedByNetworkCapabilities(nc4));
 
         assertFalse(nc3.satisfiedByNetworkCapabilities(nc2));
         assertTrue(nc2.satisfiedByNetworkCapabilities(nc3));
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 f95e79f..2b698fd 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -88,7 +88,9 @@
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
         "java/com/android/server/net/ipmemorystore/*.java",
+        "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
         "java/com/android/server/net/NetworkStats*.java",
+        "java/com/android/server/net/TestableUsageCallback.kt",
     ]
 }
 
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index 6ad8b06..ec0420e 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -17,7 +17,11 @@
 package android.net
 
 import android.content.Context
+import android.net.ConnectivityManager.MAX_NETWORK_TYPE
+import android.net.ConnectivityManager.TYPE_ETHERNET
 import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.ConnectivityManager.TYPE_WIFI
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
@@ -30,10 +34,12 @@
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
 private const val TEST_IMSI = "testimsi"
+private const val TEST_WIFI_KEY = "testwifikey"
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
@@ -126,6 +132,92 @@
         assertEquals(identFromLegacyBuild, identFromSnapshot)
         assertEquals(identFromConstructor, identFromSnapshot)
 
-        // TODO: Add test cases for wifiNetworkKey and ratType.
+        // Assert non-wifi can't have wifi network key.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkIdentity.Builder()
+                    .setType(TYPE_ETHERNET)
+                    .setWifiNetworkKey(TEST_WIFI_KEY)
+                    .build()
+        }
+
+        // Assert non-mobile can't have ratType.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkIdentity.Builder()
+                    .setType(TYPE_WIFI)
+                    .setRatType(TelephonyManager.NETWORK_TYPE_LTE)
+                    .build()
+        }
+    }
+
+    @Test
+    fun testBuilder_type() {
+        // Assert illegal type values cannot make an identity.
+        listOf(Integer.MIN_VALUE, TYPE_NONE - 1, MAX_NETWORK_TYPE + 1, Integer.MAX_VALUE)
+                .forEach { type ->
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder().setType(type).build()
+                    }
+                }
+
+        // Verify legitimate type values can make an identity.
+        for (type in TYPE_NONE..MAX_NETWORK_TYPE) {
+            NetworkIdentity.Builder().setType(type).build().also {
+                assertEquals(it.type, type)
+            }
+        }
+    }
+
+    @Test
+    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)
+                .forEach {
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder()
+                                .setType(TYPE_MOBILE)
+                                .setRatType(it)
+                                .build()
+                    }
+                }
+
+        // Verify legitimate ratTypes can make an identity.
+        TelephonyManager.getAllNetworkTypes().toMutableList().also {
+            it.add(TelephonyManager.NETWORK_TYPE_UNKNOWN)
+        }.forEach { rat ->
+            NetworkIdentity.Builder()
+                    .setType(TYPE_MOBILE)
+                    .setRatType(rat)
+                    .build().also {
+                        assertEquals(it.ratType, rat)
+                    }
+        }
+    }
+
+    @Test
+    fun testBuilder_oemManaged() {
+        // Assert illegal oemManage values cannot make an identity.
+        listOf(Integer.MIN_VALUE, NetworkTemplate.OEM_MANAGED_ALL, NetworkTemplate.OEM_MANAGED_YES,
+                Integer.MAX_VALUE)
+                .forEach {
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder()
+                                .setType(TYPE_MOBILE)
+                                .setRatType(it)
+                                .build()
+                    }
+                }
+
+        // Verify legitimate oem managed values can make an identity.
+        listOf(NetworkTemplate.OEM_MANAGED_NO, NetworkTemplate.OEM_MANAGED_PAID,
+                NetworkTemplate.OEM_MANAGED_PRIVATE, NetworkTemplate.OEM_MANAGED_PAID or
+                NetworkTemplate.OEM_MANAGED_PRIVATE)
+                .forEach { oemManaged ->
+                    NetworkIdentity.Builder()
+                            .setOemManaged(oemManaged)
+                            .build().also {
+                                assertEquals(it.oemManaged, oemManaged)
+                            }
+                }
     }
 }
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/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c5f8c00..c170605 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,6 +56,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
+import java.util.List;
 import java.util.Random;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -532,6 +533,40 @@
         assertEquals(512L + 4096L, stats.getTotalBytes());
     }
 
+    @Test
+    public void testBuilder() {
+        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
+                4, 50, 5, 60);
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
+                41, 7, 1, 0);
+        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
+                14, 31, 2, 80);
+
+        final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
+                .Builder(HOUR_IN_MILLIS, 10).build();
+        assertEquals(0, statsEmpty.getEntries().size());
+        assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
+
+        NetworkStatsHistory statsSingle = new NetworkStatsHistory
+                .Builder(HOUR_IN_MILLIS, 8)
+                .addEntry(entry1)
+                .build();
+        assertEquals(1, statsSingle.getEntries().size());
+        assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
+        assertEquals(entry1, statsSingle.getEntries().get(0));
+
+        NetworkStatsHistory statsMultiple = new NetworkStatsHistory
+                .Builder(SECOND_IN_MILLIS, 0)
+                .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+                .build();
+        final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
+        assertEquals(3, entries.size());
+        assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
+        assertEquals(entry1, entries.get(0));
+        assertEquals(entry2, entries.get(1));
+        assertEquals(entry3, entries.get(2));
+    }
+
     private static void assertIndexBeforeAfter(
             NetworkStatsHistory stats, int before, int after, long time) {
         assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 14ff981..652aee9 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -101,6 +101,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -13780,6 +13781,14 @@
         return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
     }
 
+    private TestNetworkAgentWrapper makeEnterpriseNetworkAgent(int enterpriseId) throws Exception {
+        final NetworkCapabilities workNc = new NetworkCapabilities();
+        workNc.addCapability(NET_CAPABILITY_ENTERPRISE);
+        workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        workNc.addEnterpriseId(enterpriseId);
+        return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
+    }
+
     private TestNetworkCallback mEnterpriseCallback;
     private UserHandle setupEnterpriseNetwork() {
         final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
@@ -13827,7 +13836,8 @@
         inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
                 mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
 
-        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        final TestNetworkAgentWrapper workAgent =
+                makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
         if (connectWorkProfileAgentAhead) {
             workAgent.connect(false);
         }
@@ -13841,7 +13851,7 @@
                 == PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK) {
             allowFallback = false;
         }
-        if (allowFallback) {
+        if (allowFallback && !connectWorkProfileAgentAhead) {
             // Setting a network preference for this user will create a new set of routing rules for
             // the UID range that corresponds to this user, inorder to define the default network
             // for these apps separately. This is true because the multi-layer request relevant to
@@ -13858,7 +13868,7 @@
 
         // The enterprise network is not ready yet.
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
-        if (allowFallback) {
+        if (allowFallback && !connectWorkProfileAgentAhead) {
             assertNoCallbacks(profileDefaultNetworkCallback);
         } else if (!connectWorkProfileAgentAhead) {
             profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
@@ -13884,7 +13894,7 @@
                 uidRangeFor(testHandle, profileNetworkPreference),
                 PREFERENCE_ORDER_PROFILE));
 
-        if (allowFallback) {
+        if (allowFallback && !connectWorkProfileAgentAhead) {
             inOrder.verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
                     mCellNetworkAgent.getNetwork().netId,
                     uidRangeFor(testHandle, profileNetworkPreference),
@@ -13897,7 +13907,10 @@
         workAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
         profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
                 nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)
-                        && nc.hasCapability(NET_CAPABILITY_ENTERPRISE));
+                        && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
+                        && nc.hasEnterpriseId(
+                                profileNetworkPreference.getPreferenceEnterpriseId())
+                        && nc.getEnterpriseIds().length == 1);
         if (disAllowProfileDefaultNetworkCallback != null) {
             assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
         }
@@ -13988,7 +14001,8 @@
         // If the control comes here, callbacks seem to behave correctly in the presence of
         // a default network when the enterprise network goes up and down. Now, make sure they
         // also behave correctly in the absence of a system-wide default network.
-        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent();
+        final TestNetworkAgentWrapper workAgent2 =
+                makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
         workAgent2.connect(false);
 
         profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
@@ -14006,7 +14020,10 @@
         workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid());
         profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
                 nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
-                        && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+                        && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                        && nc.hasEnterpriseId(
+                        profileNetworkPreference.getPreferenceEnterpriseId())
+                        && nc.getEnterpriseIds().length == 1);
         if (disAllowProfileDefaultNetworkCallback != null) {
             assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
         }
@@ -14037,10 +14054,11 @@
     @Test
     public void testPreferenceForUserNetworkUpDown() throws Exception {
         final UserHandle testHandle = setupEnterpriseNetwork();
+        registerDefaultNetworkCallbacks();
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
-        registerDefaultNetworkCallbacks();
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), false,
                 testHandle, mProfileDefaultNetworkCallback, null);
@@ -14058,6 +14076,7 @@
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), false,
@@ -14078,6 +14097,7 @@
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), true, testHandle,
@@ -14094,6 +14114,7 @@
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setIncludedUids(
                 List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID)));
         registerDefaultNetworkCallbacks();
@@ -14112,6 +14133,7 @@
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setIncludedUids(
                 List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
         registerDefaultNetworkCallbacks();
@@ -14130,6 +14152,7 @@
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setExcludedUids(
                 List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
         registerDefaultNetworkCallbacks();
@@ -14149,6 +14172,7 @@
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder.setExcludedUids(
                 List.of(testHandle.getUid(0) - 1));
         final TestOnCompleteListener listener = new TestOnCompleteListener();
@@ -14178,6 +14202,7 @@
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         profileNetworkPreferenceBuilder2.setIncludedUids(
                 List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
         profileNetworkPreferenceBuilder.setIncludedUids(
@@ -14213,6 +14238,84 @@
     }
 
     /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active. Make sure they behave as expected whether
+     * there is a general default network or not when configured to fallback to default network
+     * along with already connected enterprise work agent
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithFallbackWithAlreadyConnectedWorkAgent()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback,
+                null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithDefaultEnterpriseId()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback,
+                null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithId2()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
+                NetworkCapabilities.NET_ENTERPRISE_ID_2);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback, null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithInvalidId()
+            throws Exception {
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(0);
+        registerDefaultNetworkCallbacks();
+        assertThrows("Should not be able to set invalid enterprise id",
+                IllegalStateException.class, () -> profileNetworkPreferenceBuilder.build());
+    }
+
+    /**
      * Test that, in a given networking context, calling setPreferenceForUser to set per-profile
      * defaults on then off works as expected.
      */
@@ -14362,10 +14465,15 @@
     public void testProfileNetworkPrefWrongPreference() throws Exception {
         final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
         mServiceContext.setWorkProfile(testHandle, true);
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         assertThrows("Should not be able to set an illegal preference",
                 IllegalArgumentException.class,
-                () -> mCm.setProfileNetworkPreference(testHandle,
-                        PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1,
+                () -> mCm.setProfileNetworkPreferences(testHandle,
+                        List.of(profileNetworkPreferenceBuilder.build()),
                         null, null));
     }
 
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
index ea29da0..0c58582 100644
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -32,6 +33,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.INetdUnsolicitedEventListener;
 import android.net.LinkAddress;
@@ -71,6 +73,7 @@
 public class NetworkManagementServiceTest {
     private NetworkManagementService mNMService;
     @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
     @Mock private IBatteryStats.Stub mBatteryStatsService;
     @Mock private INetd.Stub mNetdService;
 
@@ -113,6 +116,9 @@
         MockitoAnnotations.initMocks(this);
         doNothing().when(mNetdService)
                 .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                eq(ConnectivityManager.class));
+        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
         // Start the service and wait until it connects to our socket.
         mNMService = NetworkManagementService.create(mContext, mDeps);
     }
@@ -239,6 +245,7 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
         assertTrue("Should be true since mobile data usage is restricted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* enabled */);
 
         mNMService.setDataSaverModeEnabled(true);
         verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -246,13 +253,16 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
         assertTrue("Should be true since data saver is on and the uid is not allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* false */);
 
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
         assertFalse("Should be false since data saver is on and the uid is allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, true /* enabled */);
 
         // remove uid from allowlist and turn datasaver off again
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, false /* enabled */);
         mNMService.setDataSaverModeEnabled(false);
         verify(mNetdService).bandwidthEnableDataSaver(false);
         assertFalse("Network should not be restricted when data saver is off",
@@ -306,12 +316,14 @@
         for (int chain : chains) {
             final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
             mNMService.setFirewallChainEnabled(chain, true);
+            verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
             for (int state : states) {
                 mNMService.setFirewallUidRule(chain, TEST_UID, state);
                 assertEquals(errorMsg.apply(chain, state),
                         expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
             }
             mNMService.setFirewallChainEnabled(chain, false);
+            verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
         }
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index c86e699..ec51537 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -296,7 +296,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
     }
 
     @Test
@@ -315,7 +315,7 @@
 
         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
     }
 
     @Test
@@ -334,7 +334,7 @@
 
         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
     }
 
     @Test
@@ -351,7 +351,7 @@
 
         // Default global setting should be used: 12 - 7 = 5
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
     }
 
     @Test
@@ -366,7 +366,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
 
         // Update setting
         setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
@@ -376,7 +376,7 @@
         // Callback must have been re-registered with new setting
         verify(mStatsManager, times(1)).unregisterUsageCallback(any());
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
     }
 
     @Test
@@ -391,7 +391,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
 
         when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
                 .thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
@@ -402,6 +402,6 @@
 
         // Uses the new setting (16 - 2 = 14MB)
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
     }
 }
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
new file mode 100644
index 0000000..987b7b7
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.net;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.INetd;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.Struct.U32;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class BpfInterfaceMapUpdaterTest {
+    private static final int TEST_INDEX = 1;
+    private static final int TEST_INDEX2 = 2;
+    private static final String TEST_INTERFACE_NAME = "test1";
+    private static final String TEST_INTERFACE_NAME2 = "test2";
+
+    private final TestLooper mLooper = new TestLooper();
+    private BaseNetdUnsolicitedEventListener mListener;
+    private BpfInterfaceMapUpdater mUpdater;
+    @Mock private IBpfMap<U32, InterfaceMapValue> mBpfMap;
+    @Mock private INetd mNetd;
+    @Mock private Context mContext;
+
+    private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
+        @Override
+        public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() {
+            return mBpfMap;
+        }
+
+        @Override
+        public InterfaceParams getInterfaceParams(String ifaceName) {
+            if (ifaceName.equals(TEST_INTERFACE_NAME)) {
+                return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
+                        MacAddress.ALL_ZEROS_ADDRESS);
+            } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
+                return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
+                        MacAddress.ALL_ZEROS_ADDRESS);
+            }
+
+            return null;
+        }
+
+        @Override
+        public INetd getINetd(Context ctx) {
+            return mNetd;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
+        mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
+                new TestDependencies());
+    }
+
+    private void verifyStartUpdater() throws Exception {
+        mUpdater.start();
+        mLooper.dispatchAll();
+        final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
+        verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
+        mListener = listenerCaptor.getValue();
+        verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX)),
+                eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
+    }
+
+    @Test
+    public void testUpdateInterfaceMap() throws Exception {
+        verifyStartUpdater();
+
+        mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
+        mLooper.dispatchAll();
+        verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX2)),
+                eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
+
+        // Check that when onInterfaceRemoved is called, nothing happens.
+        mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
+        mLooper.dispatchAll();
+        verifyNoMoreInteractions(mBpfMap);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 8340a13..0e2c293 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -32,9 +32,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 
+import android.content.Context;
 import android.content.res.Resources;
-import android.net.INetd;
+import android.net.ConnectivityManager;
 import android.net.NetworkStats;
 import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
@@ -73,8 +76,8 @@
 
     private File mTestProc;
     private NetworkStatsFactory mFactory;
-    @Mock
-    private INetd mNetd;
+    @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
 
     @Before
     public void setUp() throws Exception {
@@ -85,7 +88,10 @@
         // applications. So in order to have a test support native library, the native code
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
-        mFactory = new NetworkStatsFactory(mTestProc, false, mNetd);
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                eq(ConnectivityManager.class));
+        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
+        mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
     }
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index d993d1f..66dcf6d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -36,19 +36,15 @@
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 
-import android.app.usage.NetworkStatsManager;
 import android.net.DataUsageRequest;
 import android.net.NetworkIdentity;
 import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
 import android.net.NetworkStatsAccess;
 import android.net.NetworkTemplate;
-import android.os.ConditionVariable;
-import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Messenger;
 import android.os.Process;
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
@@ -56,7 +52,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -97,21 +92,15 @@
     private static final long WAIT_TIMEOUT_MS = 500;
     private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
     private static final long BASE_BYTES = 7 * MB_IN_BYTES;
-    private static final int INVALID_TYPE = -1;
-
-    private long mElapsedRealtime;
 
     private HandlerThread mObserverHandlerThread;
-    private Handler mObserverNoopHandler;
-
-    private LatchedHandler mHandler;
 
     private NetworkStatsObservers mStatsObservers;
-    private Messenger mMessenger;
     private ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
     private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
 
-    @Mock private IBinder mockBinder;
+    @Mock private IBinder mUsageCallbackBinder;
+    private TestableUsageCallback mUsageCallback;
 
     @Before
     public void setUp() throws Exception {
@@ -127,24 +116,29 @@
             }
         };
 
-        mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable());
-        mMessenger = new Messenger(mHandler);
-
         mActiveIfaces = new ArrayMap<>();
         mActiveUidIfaces = new ArrayMap<>();
+        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, mMessenger, mockBinder,
-                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
@@ -153,7 +147,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
@@ -165,13 +159,13 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
-        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
-        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
@@ -191,17 +185,19 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
-        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class),
+                anyInt());
 
         mStatsObservers.unregister(request, Process.SYSTEM_UID);
         waitForObserverToIdle();
 
-        Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class),
+                anyInt());
     }
 
     @Test
@@ -209,17 +205,18 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
-        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder)
+                .linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         mStatsObservers.unregister(request, UID_BLUE);
         waitForObserverToIdle();
 
-        Mockito.verifyZeroInteractions(mockBinder);
+        Mockito.verifyZeroInteractions(mUsageCallbackBinder);
     }
 
     private NetworkIdentitySet makeTestIdentSet() {
@@ -236,7 +233,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -260,7 +257,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -290,7 +287,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -313,7 +310,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -321,7 +318,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -346,7 +343,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -354,7 +351,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -386,7 +383,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -411,7 +408,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -419,7 +416,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -448,6 +445,5 @@
 
     private void waitForObserverToIdle() {
         HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS);
-        HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS);
     }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index d7bbf50..ea35c31 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -80,7 +80,6 @@
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
-import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.database.ContentObserver;
@@ -101,13 +100,9 @@
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
 import android.os.Build;
-import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
 import android.os.PowerManager;
 import android.os.SimpleClock;
 import android.provider.Settings;
@@ -188,10 +183,12 @@
     private @Mock TetheringManager mTetheringManager;
     private @Mock NetworkStatsFactory mStatsFactory;
     private @Mock NetworkStatsSettings mSettings;
-    private @Mock IBinder mBinder;
+    private @Mock IBinder mUsageCallbackBinder;
+    private TestableUsageCallback mUsageCallback;
     private @Mock AlarmManager mAlarmManager;
     @Mock
     private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+    private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
     private HandlerThread mHandlerThread;
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
@@ -313,6 +310,8 @@
         verify(mTetheringManager).registerTetheringEventCallback(
                 any(), tetheringEventCbCaptor.capture());
         mTetheringEventCallback = tetheringEventCbCaptor.getValue();
+
+        mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
     @NonNull
@@ -342,6 +341,12 @@
             public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
                 return mLocationPermissionChecker;
             }
+
+            @Override
+            public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+                    @NonNull Context ctx, @NonNull Handler handler) {
+                return mBpfInterfaceMapUpdater;
+            }
         };
     }
 
@@ -1240,20 +1245,14 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes);
 
-        // Create a messenger that waits for callback activity
-        ConditionVariable cv = new ConditionVariable(false);
-        LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv);
-        Messenger messenger = new Messenger(latchedHandler);
-
         // Force poll
         expectDefaultSettings();
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
 
         // Register and verify request and that binder was called
-        DataUsageRequest request =
-                mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest,
-                        messenger, mBinder);
+        DataUsageRequest request = mService.registerUsageCallback(
+                mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -1262,7 +1261,7 @@
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
 
         // Make sure that the caller binder gets connected
-        verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         // modify some number on wifi, and trigger poll event
         // not enough traffic to call data usage callback
@@ -1277,7 +1276,7 @@
         assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
 
         // make sure callback has not being called
-        assertEquals(INVALID_TYPE, latchedHandler.lastMessageType);
+        mUsageCallback.assertNoCallback();
 
         // and bump forward again, with counters going higher. this is
         // important, since it will trigger the data usage callback
@@ -1292,23 +1291,21 @@
         assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0);
 
 
-        // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
-        assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType);
-        cv.close();
+        // Wait for the caller to invoke expectOnThresholdReached.
+        mUsageCallback.expectOnThresholdReached(request);
 
         // Allow binder to disconnect
-        when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true);
+        when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()))
+                .thenReturn(true);
 
         // Unregister request
         mService.unregisterUsageRequest(request);
 
-        // Wait for the caller to ack receipt of CALLBACK_RELEASED
-        assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType);
+        // Wait for the caller to invoke expectOnCallbackReleased.
+        mUsageCallback.expectOnCallbackReleased(request);
 
         // Make sure that the caller binder gets disconnected
-        verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
     }
 
     @Test
@@ -1884,21 +1881,4 @@
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
     }
-
-    static class LatchedHandler extends Handler {
-        private final ConditionVariable mCv;
-        int lastMessageType = INVALID_TYPE;
-
-        LatchedHandler(Looper looper, ConditionVariable cv) {
-            super(looper);
-            mCv = cv;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            lastMessageType = msg.what;
-            mCv.open();
-            super.handleMessage(msg);
-        }
-    }
 }
diff --git a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
new file mode 100644
index 0000000..1917ec3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.net
+
+import android.net.DataUsageRequest
+import android.net.netstats.IUsageCallback
+import android.os.IBinder
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+// TODO: Move the class to static libs once all downstream have IUsageCallback definition.
+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(request))
+    }
+
+    override fun onCallbackReleased(request: DataUsageRequest) {
+        history.add(CallbackType.OnCallbackReleased(request))
+    }
+
+    fun expectOnThresholdReached(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnThresholdReached>(request, DEFAULT_TIMEOUT_MS)
+    }
+
+    fun expectOnCallbackReleased(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnCallbackReleased>(request, DEFAULT_TIMEOUT_MS)
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
+        val cb = history.poll(timeout, TimeUnit.MILLISECONDS)
+        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
+    }
+}
\ No newline at end of file