Merge "Revert "Add onSupportedTetheringType callback""
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 95f854b..4774866 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -22,6 +22,30 @@
         }
       ]
     },
+    // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
+    {
+      "name": "CtsNetTestCasesLatestSdk",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    // CTS tests that target older SDKs.
+    {
+      "name": "CtsNetTestCasesMaxTargetSdk31",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
     {
       "name": "bpf_existence_test"
     },
@@ -29,6 +53,9 @@
       "name": "connectivity_native_test"
     },
     {
+      "name": "libclat_test"
+    },
+    {
       "name": "netd_updatable_unit_test"
     },
     {
@@ -56,16 +83,10 @@
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
-      "name": "libclat_test"
-    },
-    {
       "name": "traffic_controller_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
-      "name": "libnetworkstats_test"
-    },
-    {
       "name": "FrameworksNetDeflakeTest"
     }
   ],
@@ -82,6 +103,17 @@
       ]
     },
     {
+      "name": "CtsNetTestCasesMaxTargetSdk31[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
       "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
@@ -139,23 +171,6 @@
       ]
     }
   ],
-  "auto-postsubmit": [
-    // Test tag for automotive targets. These are only running in postsubmit so as to harden the
-    // automotive targets to avoid introducing additional test flake and build time. The plan for
-    // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
-    // Additionally, this tag is used in targeted test suites to limit resource usage on the test
-    // infra during the hardening phase.
-    // TODO: this tag to be removed once the above is no longer an issue.
-    {
-      "name": "FrameworksNetTests"
-    },
-    {
-      "name": "FrameworksNetIntegrationTests"
-    },
-    {
-      "name": "FrameworksNetDeflakeTest"
-    }
-  ],
   "imports": [
     {
       "path": "frameworks/base/core/java/android/net"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 29f6e12..bfba5cd 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,7 +21,7 @@
 java_defaults {
     name: "TetheringApiLevel",
     sdk_version: "module_current",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     min_sdk_version: "30",
 }
 
@@ -33,9 +33,11 @@
         ":framework-connectivity-shared-srcs",
         ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
+        ":statslog-tethering-java-gen",
     ],
     static_libs: [
         "androidx.annotation_annotation",
+        "connectivity-net-module-utils-bpf",
         "modules-utils-build",
         "modules-utils-statemachine",
         "networkstack-client",
@@ -133,7 +135,7 @@
         "-Wthread-safety",
     ],
 
-    ldflags: ["-Wl,--exclude-libs=ALL,-error-limit=0"],
+    ldflags: ["-Wl,--exclude-libs=ALL,--error-limit=0"],
 }
 
 // Common defaults for compiling the actual APK.
@@ -178,9 +180,9 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The permission configuration *must* be included to ensure security of the device
+    // The network stack *must* be included to ensure security of the device
     required: [
-        "NetworkPermissionConfig",
+        "NetworkStack",
         "privapp_allowlist_com.android.tethering",
     ],
     apex_available: ["com.android.tethering"],
@@ -198,9 +200,9 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The permission configuration *must* be included to ensure security of the device
+    // The network stack *must* be included to ensure security of the device
     required: [
-        "NetworkPermissionConfig",
+        "NetworkStackNext",
         "privapp_allowlist_com.android.tethering",
     ],
     apex_available: ["com.android.tethering"],
@@ -223,3 +225,11 @@
     apex_available: ["com.android.tethering"],
     min_sdk_version: "30",
 }
+
+genrule {
+    name: "statslog-tethering-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
+         " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+    out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index bd8fe7c..b3cae7c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -70,9 +70,9 @@
     canned_fs_config: "canned_fs_config",
     bpfs: [
         "block.o",
-        "clatd.o_mainline",
+        "clatd.o",
         "dscp_policy.o",
-        "netd.o_mainline",
+        "netd.o",
         "offload.o",
         "test.o",
     ],
@@ -134,11 +134,11 @@
     hidden_api: {
         max_target_r_low_priority: [
             "hiddenapi/hiddenapi-max-target-r-loprio.txt",
-        ],
+	],
         max_target_o_low_priority: [
             "hiddenapi/hiddenapi-max-target-o-low-priority.txt",
             "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
-        ],
+	],
         unsupported: [
             "hiddenapi/hiddenapi-unsupported.txt",
             "hiddenapi/hiddenapi-unsupported-tiramisu.txt",
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
index 88f13b2..5d5ede6 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,7 @@
 {
   "name": "com.android.tethering",
-  "version": 319999900
+
+  // Placeholder module version to be replaced during build.
+  // Do not change!
+  "version": 0
 }
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 22d2c5d..18ef631 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
 import android.net.INetd;
 import android.net.MacAddress;
 import android.net.TetherStatsParcel;
-import android.net.util.SharedLog;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.SparseArray;
@@ -28,11 +27,12 @@
 import androidx.annotation.Nullable;
 
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 5afb862..fd9dab5 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 
 import android.net.MacAddress;
-import android.net.util.SharedLog;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -31,8 +30,11 @@
 
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfUtils;
@@ -42,8 +44,6 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 
 import java.io.FileDescriptor;
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 915e210..69cbab5 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -25,9 +25,9 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 40eed3f..904e491 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -4,6 +4,7 @@
 # module will be overwritten by the ones in the framework.
 rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
 rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+rule android.util.IndentingPrintWriter* com.android.networkstack.tethering.util.AndroidUtilIndentingPrintWriter@1
 
 rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1
 
@@ -13,4 +14,7 @@
 # Classes from net-utils-device-common
 rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
 
-rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
\ No newline at end of file
+rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
+
+# Classes for hardware offload hidl interface
+rule android.hidl.base.V1_0.DebugInfo* com.android.networkstack.tethering.hidl.base.V1_0.DebugInfo@1
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 7b5ae0d..2905e28 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -12,6 +12,11 @@
     native <methods>;
 }
 
+# Ensure runtime-visible field annotations are kept when using R8 full mode.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+-keep interface com.android.networkstack.tethering.util.Struct$Field {
+    *;
+}
 -keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
     *;
 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c718f4c..da7ca56 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -46,7 +46,6 @@
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -64,11 +63,13 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 
@@ -282,13 +283,15 @@
 
     private LinkAddress mIpv4Address;
 
+    private final TetheringMetrics mTetheringMetrics;
+
     // TODO: Add a dependency object to pass the data members or variables from the tethering
     // object. It helps to reduce the arguments of the constructor.
     public IpServer(
             String ifaceName, Looper looper, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
-            Dependencies deps) {
+            TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, looper);
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -303,6 +306,7 @@
         mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
+        mTetheringMetrics = tetheringMetrics;
         resetLinkProperties();
         mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
@@ -1201,6 +1205,9 @@
             stopConntrackMonitoring();
 
             resetLinkProperties();
+
+            mTetheringMetrics.updateErrorCode(mInterfaceType, mLastError);
+            mTetheringMetrics.sendReport(mInterfaceType);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index f8a1094..133ae01 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -44,14 +44,12 @@
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.OsConstants;
 import android.text.TextUtils;
 import android.util.ArraySet;
-import android.util.Base64;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -61,19 +59,25 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
 import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
 
+import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -118,9 +122,8 @@
     private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
     private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
     private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
-
-    // Using "," as a separator is safe because base64 characters are [0-9a-zA-Z/=+].
-    private static final String DUMP_BASE64_DELIMITER = ",";
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
 
     /** The names of all the BPF counters defined in bpf_tethering.h. */
     public static final String[] sBpfCounterNames = getBpfCounterNames();
@@ -938,6 +941,8 @@
      * be allowed to be accessed on the handler thread.
      */
     public void dump(@NonNull IndentingPrintWriter pw) {
+        // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
+        // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
         pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
         pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
         pw.println("Stats provider " + (mStatsProvider != null
@@ -1019,7 +1024,7 @@
             map.forEach((k, v) -> {
                 pw.println(String.format("%s: %s", k, v));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping BPF stats map: " + e);
         }
     }
@@ -1067,44 +1072,56 @@
                 return;
             }
             map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv6 upstream map: " + e);
         }
     }
 
-    private String ipv4RuleToBase64String(Tether4Key key, Tether4Value value) {
-        final byte[] keyBytes = key.writeToBytes();
-        final String keyBase64Str = Base64.encodeToString(keyBytes, Base64.DEFAULT)
-                .replace("\n", "");
-        final byte[] valueBytes = value.writeToBytes();
-        final String valueBase64Str = Base64.encodeToString(valueBytes, Base64.DEFAULT)
-                .replace("\n", "");
-
-        return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
-    }
-
-    private void dumpRawIpv4ForwardingRuleMap(
-            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+    private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+            IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
-            pw.println("No IPv4 support");
+            pw.println("No BPF support");
             return;
         }
         if (map.isEmpty()) {
-            pw.println("No rules");
+            pw.println("No entries");
             return;
         }
-        map.forEach((k, v) -> pw.println(ipv4RuleToBase64String(k, v)));
+        map.forEach((k, v) -> pw.println(BpfDump.toBase64EncodedString(k, v)));
     }
 
     /**
-     * Dump raw BPF map in base64 encoded strings. For test only.
+     * Dump raw BPF map into the base64 encoded strings "<base64 key>,<base64 value>".
+     * Allow to dump only one map path once. For test only.
+     *
+     * Usage:
+     * $ dumpsys tethering bpfRawMap --<map name>
+     *
+     * Output:
+     * <base64 encoded key #1>,<base64 encoded value #1>
+     * <base64 encoded key #2>,<base64 encoded value #2>
+     * ..
      */
-    public void dumpRawMap(@NonNull IndentingPrintWriter pw) {
-        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-            // TODO: dump downstream map.
-            dumpRawIpv4ForwardingRuleMap(upstreamMap, pw);
-        } catch (ErrnoException e) {
-            pw.println("Error dumping IPv4 map: " + e);
+    public void dumpRawMap(@NonNull IndentingPrintWriter pw, @Nullable String[] args) {
+        // TODO: consider checking the arg order that <map name> is after "bpfRawMap". Probably
+        // it is okay for now because this is used by test only and test is supposed to use
+        // expected argument order.
+        // TODO: dump downstream4 map.
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
+            try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+                dumpRawMap(statsMap, pw);
+            } catch (ErrnoException | IOException e) {
+                pw.println("Error dumping stats map: " + e);
+            }
+            return;
+        }
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
+            try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
+                dumpRawMap(upstreamMap, pw);
+            } catch (ErrnoException | IOException e) {
+                pw.println("Error dumping IPv4 map: " + e);
+            }
+            return;
         }
     }
 
@@ -1172,7 +1189,7 @@
             pw.increaseIndent();
             dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
             pw.decreaseIndent();
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
     }
@@ -1197,7 +1214,7 @@
                 }
                 if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping counter map: " + e);
         }
     }
@@ -1221,7 +1238,7 @@
                 pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
                         v.ifIndex, getIfName(v.ifIndex)));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping dev map: " + e);
         }
         pw.decreaseIndent();
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 844efde..741af5c 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -34,7 +34,6 @@
 import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
 
 import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
-import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
 
 import android.app.AlarmManager;
 import android.app.PendingIntent;
@@ -44,20 +43,18 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.net.util.SharedLog;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcel;
-import android.os.PersistableBundle;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.telephony.CarrierConfigManager;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
 import java.util.BitSet;
@@ -307,13 +304,13 @@
         if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)) {
             return TETHERING_PROVISIONING_NOT_REQUIRED;
         }
-        // TODO: Find a way to avoid get carrier config twice.
-        if (carrierConfigAffirmsCarrierNotSupport(config)) {
+
+        if (!config.isCarrierSupportTethering) {
             // To block tethering, behave as if running provisioning check and failed.
             return TETHERING_PROVISIONING_CARRIER_UNSUPPORT;
         }
 
-        if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+        if (!config.isCarrierConfigAffirmsEntitlementCheckRequired) {
             return TETHERING_PROVISIONING_NOT_REQUIRED;
         }
         return (config.provisioningApp.length == 2)
@@ -380,57 +377,6 @@
     }
 
     /**
-     * Get carrier configuration bundle.
-     * @param config an object that encapsulates the various tethering configuration elements.
-     * */
-    public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
-        final CarrierConfigManager configManager = mContext
-                .getSystemService(CarrierConfigManager.class);
-        if (configManager == null) return null;
-
-        final PersistableBundle carrierConfig = configManager.getConfigForSubId(
-                config.activeDataSubId);
-
-        if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
-            return carrierConfig;
-        }
-
-        return null;
-    }
-
-    // The logic here is aimed solely at confirming that a CarrierConfig exists
-    // and affirms that entitlement checks are not required.
-    //
-    // TODO: find a better way to express this, or alter the checking process
-    // entirely so that this is more intuitive.
-    // TODO: Find a way to avoid using getCarrierConfig everytime.
-    private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
-            final TetheringConfiguration config) {
-        // Check carrier config for entitlement checks
-        final PersistableBundle carrierConfig = getCarrierConfig(config);
-        if (carrierConfig == null) return false;
-
-        // A CarrierConfigManager was found and it has a config.
-        final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
-                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
-        return !isEntitlementCheckRequired;
-    }
-
-    private boolean carrierConfigAffirmsCarrierNotSupport(final TetheringConfiguration config) {
-        if (!SdkLevel.isAtLeastT()) {
-            return false;
-        }
-        // Check carrier config for entitlement checks
-        final PersistableBundle carrierConfig = getCarrierConfig(config);
-        if (carrierConfig == null) return false;
-
-        // A CarrierConfigManager was found and it has a config.
-        final boolean mIsCarrierSupport = carrierConfig.getBoolean(
-                KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
-        return !mIsCarrierSupport;
-    }
-
-    /**
      * Run no UI tethering provisioning check.
      * @param type tethering type from TetheringManager.TETHERING_{@code *}
      * @param subId default data subscription ID.
@@ -479,7 +425,7 @@
 
     private void runTetheringProvisioning(
             boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
-        if (carrierConfigAffirmsCarrierNotSupport(config)) {
+        if (!config.isCarrierSupportTethering) {
             mListener.onTetherProvisioningFailed(downstreamType, "Carrier does not support.");
             if (showProvisioningUi) {
                 showCarrierUnsupportedDialog();
@@ -497,7 +443,7 @@
     }
 
     private void showCarrierUnsupportedDialog() {
-        // This is only used when carrierConfigAffirmsCarrierNotSupport() is true.
+        // This is only used when TetheringConfiguration.isCarrierSupportTethering is false.
         if (!SdkLevel.isAtLeastT()) {
             return;
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
index f3dcaa2..ab3929d 100644
--- a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
@@ -24,9 +24,10 @@
 import android.net.RouteInfo;
 import android.net.ip.IpServer;
 import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
 import android.util.Log;
 
+import com.android.net.module.util.SharedLog;
+
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
index d60c21d..94684af 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadController.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -43,7 +43,6 @@
 import android.net.NetworkStats.Entry;
 import android.net.RouteInfo;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.provider.Settings;
 import android.system.ErrnoException;
@@ -53,6 +52,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 9da66d8..fbb342d 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -28,7 +28,6 @@
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
 import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
-import android.net.util.SharedLog;
 import android.net.util.SocketUtils;
 import android.os.Handler;
 import android.os.NativeHandle;
@@ -40,6 +39,7 @@
 import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.NetlinkSocket;
 import com.android.net.module.util.netlink.StructNfGenMsg;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index cc2422f..41a10ae 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -172,6 +172,9 @@
             return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
         }
 
+        // This ensures that tethering isn't started on 2 different interfaces with the same type.
+        // Once tethering could support multiple interface with the same type,
+        // TetheringSoftApCallback would need to handle it among others.
         final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
         if (useLastAddress && cachedAddress != null
                 && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 0b607bd..3f328db 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -99,7 +99,6 @@
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
 import android.net.shared.NetdUtils;
-import android.net.util.SharedLog;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
 import android.net.wifi.p2p.WifiP2pGroup;
@@ -135,10 +134,13 @@
 import com.android.internal.util.StateMachine;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.TetheringUtils;
@@ -253,6 +255,7 @@
     private final UserManager mUserManager;
     private final BpfCoordinator mBpfCoordinator;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+    private final TetheringMetrics mTetheringMetrics;
     private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
 
     private volatile TetheringConfiguration mConfig;
@@ -286,6 +289,7 @@
         mNetd = mDeps.getINetd(mContext);
         mLooper = mDeps.getTetheringLooper();
         mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
+        mTetheringMetrics = mDeps.getTetheringMetrics();
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -439,8 +443,22 @@
                 mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
 
         final WifiManager wifiManager = getWifiManager();
+        TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
         if (wifiManager != null) {
-            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+            wifiManager.registerSoftApCallback(mExecutor, softApCallback);
+        }
+        if (SdkLevel.isAtLeastT() && wifiManager != null) {
+            try {
+                // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+                // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+                // or MAINLINE_NETWORK_STACK permission would also able to use this API.
+                wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+            } catch (UnsupportedOperationException e) {
+                // Since wifi module development in internal branch,
+                // #registerLocalOnlyHotspotSoftApCallback currently doesn't supported in AOSP
+                // before AOSP switch to Android T + 1.
+                Log.wtf(TAG, "registerLocalOnlyHotspotSoftApCallback API is not supported");
+            }
         }
 
         startTrackDefaultNetwork();
@@ -476,7 +494,7 @@
             // To avoid launching unexpected provisioning checks, ignore re-provisioning
             // when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning()
             // will be triggered again when CarrierConfig is loaded.
-            if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+            if (TetheringConfiguration.getCarrierConfig(mContext, subId) != null) {
                 mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
             } else {
                 mLog.log("IGNORED reevaluate provisioning, no carrier config loaded");
@@ -534,6 +552,13 @@
         }
 
         // Called by wifi when the number of soft AP clients changed.
+        // Currently multiple softAp would not behave well in PrivateAddressCoordinator
+        // (where it gets the address from cache), it ensure tethering only support one ipServer for
+        // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
+        // onConnectedClientsChanged should also be updated to support tracking different softAp's
+        // clients individually.
+        // TODO: Add wtf log and have check to reject request duplicated type with different
+        // interface.
         @Override
         public void onConnectedClientsChanged(final List<WifiClient> clients) {
             updateConnectedClients(clients);
@@ -608,7 +633,8 @@
         processInterfaceStateChange(iface, false /* enabled */);
     }
 
-    void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
+    void startTethering(final TetheringRequestParcel request, final String callerPkg,
+            final IIntResultListener listener) {
         mHandler.post(() -> {
             final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
                     request.tetheringType);
@@ -628,6 +654,7 @@
                         request.showProvisioningUi);
             }
             enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
+            mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
         });
     }
 
@@ -687,7 +714,11 @@
 
         // If changing tethering fail, remove corresponding request
         // no matter who trigger the start/stop.
-        if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type);
+        if (result != TETHER_ERROR_NO_ERROR) {
+            mActiveTetheringRequests.remove(type);
+            mTetheringMetrics.updateErrorCode(type, result);
+            mTetheringMetrics.sendReport(type);
+        }
     }
 
     private int setWifiTethering(final boolean enable) {
@@ -1280,7 +1311,7 @@
 
             // Finally bring up serving on the new interface
             mWifiP2pTetherInterface = group.getInterface();
-            enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+            enableWifiP2pIpServing(mWifiP2pTetherInterface);
         }
 
         private void handleUserRestrictionAction() {
@@ -1368,23 +1399,27 @@
     private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
             boolean isNcm) {
         ensureIpServerStarted(ifname, tetheringType, isNcm);
-        changeInterfaceState(ifname, ipServingMode);
+        if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+            Log.e(TAG, "unable start tethering on iface " + ifname);
+        }
     }
 
-    private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
-        mLog.log("Canceling WiFi tethering request -"
-                + " type=" + tetheringType
-                + " interface=" + ifname
-                + " state=" + apState);
-
-        if (!TextUtils.isEmpty(ifname)) {
-            final TetherState ts = mTetherStates.get(ifname);
-            if (ts != null) {
-                ts.ipServer.unwanted();
-                return;
-            }
+    private void disableWifiIpServingCommon(int tetheringType, String ifname) {
+        if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
+            mTetherStates.get(ifname).ipServer.unwanted();
+            return;
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mLog.e("Tethering no longer handle untracked interface after T: " + ifname);
+            return;
+        }
+
+        // Attempt to guess the interface name before T. Pure AOSP code should never enter here
+        // because WIFI_AP_STATE_CHANGED intent always include ifname and it should be tracked
+        // by mTetherStates. In case OEMs have some modification in wifi side which pass null
+        // or empty ifname. Before T, tethering allow to disable the first wifi ipServer if
+        // given ifname don't match any tracking ipServer.
         for (int i = 0; i < mTetherStates.size(); i++) {
             final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
             if (ipServer.interfaceType() == tetheringType) {
@@ -1392,7 +1427,6 @@
                 return;
             }
         }
-
         mLog.log("Error disabling Wi-Fi IP serving; "
                 + (TextUtils.isEmpty(ifname) ? "no interface name specified"
                                            : "specified interface: " + ifname));
@@ -1401,20 +1435,39 @@
     private void disableWifiIpServing(String ifname, int apState) {
         // Regardless of whether we requested this transition, the AP has gone
         // down.  Don't try to tether again unless we're requested to do so.
-        // TODO: Remove this altogether, once Wi-Fi reliably gives us an
-        // interface name with every broadcast.
         mWifiTetherRequested = false;
 
-        disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
+        mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
+
+        disableWifiIpServingCommon(TETHERING_WIFI, ifname);
+    }
+
+    private void enableWifiP2pIpServing(String ifname) {
+        if (TextUtils.isEmpty(ifname)) {
+            mLog.e("Cannot enable P2P IP serving with invalid interface");
+            return;
+        }
+
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable p2p regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI_P2P : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+        enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
     }
 
     private void disableWifiP2pIpServingIfNeeded(String ifname) {
         if (TextUtils.isEmpty(ifname)) return;
 
-        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+        mLog.log("Canceling P2P tethering request - interface=" + ifname);
+        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
     }
 
     private void enableWifiIpServing(String ifname, int wifiIpMode) {
+        mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
+
         // Map wifiIpMode values to IpServer.Callback serving states, inferring
         // from mWifiTetherRequested as a final "best guess".
         final int ipServingMode;
@@ -1430,13 +1483,18 @@
                 return;
         }
 
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable wifi regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+
         if (!TextUtils.isEmpty(ifname)) {
-            ensureIpServerStarted(ifname);
-            changeInterfaceState(ifname, ipServingMode);
+            enableIpServing(type, ifname, ipServingMode);
         } else {
-            mLog.e(String.format(
-                    "Cannot enable IP serving in mode %s on missing interface name",
-                    ipServingMode));
+            mLog.e("Cannot enable IP serving on missing interface name");
         }
     }
 
@@ -1488,27 +1546,6 @@
         }
     }
 
-    private void changeInterfaceState(String ifname, int requestedState) {
-        final int result;
-        switch (requestedState) {
-            case IpServer.STATE_UNAVAILABLE:
-            case IpServer.STATE_AVAILABLE:
-                result = untether(ifname);
-                break;
-            case IpServer.STATE_TETHERED:
-            case IpServer.STATE_LOCAL_ONLY:
-                result = tether(ifname, requestedState);
-                break;
-            default:
-                Log.wtf(TAG, "Unknown interface state: " + requestedState);
-                return;
-        }
-        if (result != TETHER_ERROR_NO_ERROR) {
-            Log.e(TAG, "unable start or stop tethering on iface " + ifname);
-            return;
-        }
-    }
-
     TetheringConfiguration getTetheringConfiguration() {
         return mConfig;
     }
@@ -2472,13 +2509,12 @@
                 writer, "  ");
 
         // Used for testing instead of human debug.
-        // TODO: add options to choose which map to dump.
-        if (argsContain(args, "bpfRawMap")) {
-            mBpfCoordinator.dumpRawMap(pw);
+        if (CollectionUtils.contains(args, "bpfRawMap")) {
+            mBpfCoordinator.dumpRawMap(pw, args);
             return;
         }
 
-        if (argsContain(args, "bpf")) {
+        if (CollectionUtils.contains(args, "bpf")) {
             dumpBpf(pw);
             return;
         }
@@ -2544,7 +2580,7 @@
 
         pw.println("Log:");
         pw.increaseIndent();
-        if (argsContain(args, "--short")) {
+        if (CollectionUtils.contains(args, "--short")) {
             pw.println("<log removed for brevity>");
         } else {
             mLog.dump(fd, pw, args);
@@ -2588,13 +2624,6 @@
         if (e != null) throw e;
     }
 
-    private static boolean argsContain(String[] args, String target) {
-        for (String arg : args) {
-            if (target.equals(arg)) return true;
-        }
-        return false;
-    }
-
     private void updateConnectedClients(final List<WifiClient> wifiClients) {
         if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
                 wifiClients)) {
@@ -2681,23 +2710,28 @@
         mTetherMainSM.sendMessage(which, state, 0, newLp);
     }
 
+    private boolean hasSystemFeature(final String feature) {
+        return mContext.getPackageManager().hasSystemFeature(feature);
+    }
+
+    private boolean checkTetherableType(int type) {
+        if ((type == TETHERING_WIFI || type == TETHERING_WIGIG)
+                && !hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            return false;
+        }
+
+        if (type == TETHERING_WIFI_P2P && !hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+            return false;
+        }
+
+        return type != TETHERING_INVALID;
+    }
+
     private void ensureIpServerStarted(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
-        if (interfaceType == TETHERING_INVALID) {
-            mLog.log(iface + " is not a tetherable iface, ignoring");
-            return;
-        }
-
-        final PackageManager pm = mContext.getPackageManager();
-        if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
-            mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
-            return;
-        }
-        if (interfaceType == TETHERING_WIFI_P2P
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
-            mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+        if (!checkTetherableType(interfaceType)) {
+            mLog.log(iface + " is used for " + interfaceType + " which is not tetherable");
             return;
         }
 
@@ -2715,7 +2749,7 @@
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
                              makeControlCallback(), mConfig, mPrivateAddressCoordinator,
-                             mDeps.getIpServerDependencies()), isNcm);
+                             mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
         tetherState.ipServer.start();
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index f9f3ed9..696a970 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -24,14 +24,16 @@
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.TetheringConfigurationParcel;
-import android.net.util.SharedLog;
+import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -39,6 +41,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -142,6 +145,9 @@
     public final int provisioningCheckPeriod;
     public final String provisioningResponse;
 
+    public final boolean isCarrierSupportTethering;
+    public final boolean isCarrierConfigAffirmsEntitlementCheckRequired;
+
     public final int activeDataSubId;
 
     private final boolean mEnableLegacyDhcpServer;
@@ -207,6 +213,11 @@
         provisioningResponse = getResourceString(res,
                 R.string.config_mobile_hotspot_provision_response);
 
+        PersistableBundle carrierConfigs = getCarrierConfig(ctx, activeDataSubId);
+        isCarrierSupportTethering = carrierConfigAffirmsCarrierSupport(carrierConfigs);
+        isCarrierConfigAffirmsEntitlementCheckRequired =
+                carrierConfigAffirmsEntitlementCheckRequired(carrierConfigs);
+
         mOffloadPollInterval = getResourceInteger(res,
                 R.integer.config_tether_offload_poll_interval,
                 DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -329,6 +340,10 @@
         pw.print("provisioningAppNoUi: ");
         pw.println(provisioningAppNoUi);
 
+        pw.println("isCarrierSupportTethering: " + isCarrierSupportTethering);
+        pw.println("isCarrierConfigAffirmsEntitlementCheckRequired: "
+                + isCarrierConfigAffirmsEntitlementCheckRequired);
+
         pw.print("enableBpfOffload: ");
         pw.println(mEnableBpfOffload);
 
@@ -361,6 +376,9 @@
                 toIntArray(preferredUpstreamIfaceTypes)));
         sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
         sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
+        sj.add(String.format("isCarrierSupportTethering:%s", isCarrierSupportTethering));
+        sj.add(String.format("isCarrierConfigAffirmsEntitlementCheckRequired:%s",
+                isCarrierConfigAffirmsEntitlementCheckRequired));
         sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload));
         sj.add(String.format("enableLegacyDhcpServer:%s", mEnableLegacyDhcpServer));
         return String.format("TetheringConfiguration{%s}", sj.toString());
@@ -596,6 +614,39 @@
         return result;
     }
 
+    private static boolean carrierConfigAffirmsEntitlementCheckRequired(
+            PersistableBundle carrierConfig) {
+        if (carrierConfig == null) {
+            return true;
+        }
+        return carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+    }
+
+    private static boolean carrierConfigAffirmsCarrierSupport(PersistableBundle carrierConfig) {
+        if (!SdkLevel.isAtLeastT() || carrierConfig == null) {
+            return true;
+        }
+        return carrierConfig.getBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
+    }
+
+    /**
+     * Get carrier configuration bundle.
+     */
+    public static PersistableBundle getCarrierConfig(Context context, int activeDataSubId) {
+        final CarrierConfigManager configManager =
+                context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return null;
+        }
+
+        final PersistableBundle carrierConfig = configManager.getConfigForSubId(activeDataSubId);
+        if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
+            return carrierConfig;
+        }
+        return null;
+    }
+
     /**
      * Convert this TetheringConfiguration to a TetheringConfigurationParcel.
      */
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9224213..611d1cf 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.net.INetd;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -32,8 +31,10 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 
 import java.util.ArrayList;
 
@@ -163,4 +164,11 @@
     public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
         return BluetoothPanShimImpl.newInstance(pan);
     }
+
+    /**
+     * Get a reference to the TetheringMetrics to be used by tethering.
+     */
+    public TetheringMetrics getTetheringMetrics() {
+        return new TetheringMetrics();
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 9fb61fe..f147e10 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -137,7 +137,7 @@
                 return;
             }
 
-            mTethering.startTethering(request, listener);
+            mTethering.startTethering(request, callerPkg, listener);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index f8dd673..16c031b 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -36,7 +36,6 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -46,6 +45,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.networkstack.tethering.util.PrefixUtils;
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
new file mode 100644
index 0000000..d8e631e
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -0,0 +1,194 @@
+/*
+ * 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.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Collection of utilities for tethering metrics.
+ *
+ * To see if the logs are properly sent to statsd, execute following commands
+ *
+ * $ adb shell cmd stats print-logs
+ * $ adb logcat | grep statsd OR $ adb logcat -b stats
+ *
+ * @hide
+ */
+public class TetheringMetrics {
+    private static final String TAG = TetheringMetrics.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String SETTINGS_PKG_NAME = "com.android.settings";
+    private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
+    private static final String GMS_PKG_NAME = "com.google.android.gms";
+    private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
+
+    /** Update Tethering stats about caller's package name and downstream type. */
+    public void createBuilder(final int downstreamType, final String callerPkg) {
+        NetworkTetheringReported.Builder statsBuilder =
+                    NetworkTetheringReported.newBuilder();
+        statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
+                    .setUserType(userTypeToEnum(callerPkg))
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+        mBuilderMap.put(downstreamType, statsBuilder);
+    }
+
+    /** Update error code of given downstreamType. */
+    public void updateErrorCode(final int downstreamType, final int errCode) {
+        NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        statsBuilder.setErrorCode(errorCodeToEnum(errCode));
+    }
+
+    /** Remove Tethering stats.
+     *  If Tethering stats is ready to write then write it before removing.
+     */
+    public void sendReport(final int downstreamType) {
+        final NetworkTetheringReported.Builder statsBuilder =
+                mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        write(statsBuilder.build());
+        mBuilderMap.remove(downstreamType);
+    }
+
+    /** Collect Tethering stats and write metrics data to statsd pipeline. */
+    @VisibleForTesting
+    public void write(@NonNull final NetworkTetheringReported reported) {
+        TetheringStatsLog.write(TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                reported.getErrorCode().getNumber(),
+                reported.getDownstreamType().getNumber(),
+                reported.getUpstreamType().getNumber(),
+                reported.getUserType().getNumber());
+        if (DBG) {
+            Log.d(TAG, "Write errorCode: " + reported.getErrorCode().getNumber()
+                    + ", downstreamType: " + reported.getDownstreamType().getNumber()
+                    + ", upstreamType: " + reported.getUpstreamType().getNumber()
+                    + ", userType: " + reported.getUserType().getNumber());
+        }
+    }
+
+    /** Map {@link TetheringType} to {@link DownstreamType} */
+    private DownstreamType downstreamTypeToEnum(final int ifaceType) {
+        switch(ifaceType) {
+            case TETHERING_WIFI:
+                return DownstreamType.DS_TETHERING_WIFI;
+            case TETHERING_WIFI_P2P:
+                return DownstreamType.DS_TETHERING_WIFI_P2P;
+            case TETHERING_USB:
+                return DownstreamType.DS_TETHERING_USB;
+            case TETHERING_BLUETOOTH:
+                return DownstreamType.DS_TETHERING_BLUETOOTH;
+            case TETHERING_NCM:
+                return DownstreamType.DS_TETHERING_NCM;
+            case TETHERING_ETHERNET:
+                return DownstreamType.DS_TETHERING_ETHERNET;
+            default:
+                return DownstreamType.DS_UNSPECIFIED;
+        }
+    }
+
+    /** Map {@link StartTetheringError} to {@link ErrorCode} */
+    private ErrorCode errorCodeToEnum(final int lastError) {
+        switch(lastError) {
+            case TETHER_ERROR_NO_ERROR:
+                return ErrorCode.EC_NO_ERROR;
+            case TETHER_ERROR_UNKNOWN_IFACE:
+                return ErrorCode.EC_UNKNOWN_IFACE;
+            case TETHER_ERROR_SERVICE_UNAVAIL:
+                return ErrorCode.EC_SERVICE_UNAVAIL;
+            case TETHER_ERROR_UNSUPPORTED:
+                return ErrorCode.EC_UNSUPPORTED;
+            case TETHER_ERROR_UNAVAIL_IFACE:
+                return ErrorCode.EC_UNAVAIL_IFACE;
+            case TETHER_ERROR_INTERNAL_ERROR:
+                return ErrorCode.EC_INTERNAL_ERROR;
+            case TETHER_ERROR_TETHER_IFACE_ERROR:
+                return ErrorCode.EC_TETHER_IFACE_ERROR;
+            case TETHER_ERROR_UNTETHER_IFACE_ERROR:
+                return ErrorCode.EC_UNTETHER_IFACE_ERROR;
+            case TETHER_ERROR_ENABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_ENABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_DISABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_DISABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_IFACE_CFG_ERROR:
+                return ErrorCode.EC_IFACE_CFG_ERROR;
+            case TETHER_ERROR_PROVISIONING_FAILED:
+                return ErrorCode.EC_PROVISIONING_FAILED;
+            case TETHER_ERROR_DHCPSERVER_ERROR:
+                return ErrorCode.EC_DHCPSERVER_ERROR;
+            case TETHER_ERROR_ENTITLEMENT_UNKNOWN:
+                return ErrorCode.EC_ENTITLEMENT_UNKNOWN;
+            case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION;
+            case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION;
+            default:
+                return ErrorCode.EC_UNKNOWN_TYPE;
+        }
+    }
+
+    /** Map callerPkg to {@link UserType} */
+    private UserType userTypeToEnum(final String callerPkg) {
+        if (callerPkg.equals(SETTINGS_PKG_NAME)) {
+            return UserType.USER_SETTINGS;
+        } else if (callerPkg.equals(SYSTEMUI_PKG_NAME)) {
+            return UserType.USER_SYSTEMUI;
+        } else if (callerPkg.equals(GMS_PKG_NAME)) {
+            return UserType.USER_GMS;
+        } else {
+            return UserType.USER_UNKNOWN;
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 66d67a1..e6236df 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -22,7 +22,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.net.module.util.JniUtil;
-import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.net.module.util.bpf.TetherStatsValue;
 
 import java.io.FileDescriptor;
 import java.net.Inet6Address;
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index a4d0448..ca8d3de 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -32,6 +32,7 @@
         "net-tests-utils",
         "net-utils-device-common-bpf",
         "testables",
+        "connectivity-net-module-utils-bpf",
     ],
     libs: [
         "android.test.runner",
@@ -49,7 +50,7 @@
 // Use with NetworkStackJarJarRules.
 android_library {
     name: "TetheringIntegrationTestsLatestSdkLib",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index de81a38..86dca1c 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -26,19 +26,25 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
-import static android.net.TetheringTester.RemoteResponder;
-import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpPacket;
 import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.BpfDump.BASE64_DELIMITER;
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.testutils.DeviceInfoUtils.KVersion;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -59,6 +65,7 @@
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.Log;
@@ -70,18 +77,18 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.structs.EthernetHeader;
-import com.android.net.module.util.structs.Icmpv6Header;
-import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
@@ -95,6 +102,7 @@
 
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InterfaceAddress;
 import java.net.NetworkInterface;
@@ -128,17 +136,33 @@
     // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
     // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
     private static final int UDP_STREAM_TS_MS = 2000;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 44;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+    private static final long WAIT_RA_TIMEOUT_MS = 2000;
+
     private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
     private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
     private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
+    private static final IpPrefix TEST_NAT64PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final Inet6Address REMOTE_NAT64_ADDR =
+            (Inet6Address) parseNumericAddress("64:ff9b::808:808");
     private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
-    private static final String BASE64_DELIMITER = ",";
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
     private static final String LINE_DELIMITER = "\\n";
 
+    // version=6, traffic class=0x0, flowlabel=0x0;
+    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
+    private static final short HOP_LIMIT = 0x40;
+
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
     private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -300,27 +324,13 @@
 
     }
 
-    private static boolean isRouterAdvertisement(byte[] pkt) {
-        if (pkt == null) return false;
-
-        ByteBuffer buf = ByteBuffer.wrap(pkt);
-
-        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
-        if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
-
-        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
-        if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
-
-        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
-        return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT;
-    }
-
-    private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
+    private static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
             long timeoutMs) {
         final long deadline = SystemClock.uptimeMillis() + timeoutMs;
         do {
             byte[] pkt = reader.popPacket(timeoutMs);
-            if (isRouterAdvertisement(pkt)) return;
+            if (isExpectedIcmpv6Packet(pkt, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return;
+
             timeoutMs = deadline - SystemClock.uptimeMillis();
         } while (timeoutMs > 0);
         fail("Did not receive router advertisement on " + iface + " after "
@@ -372,7 +382,7 @@
         // before the reader is started.
         mDownstreamReader = makePacketReader(mDownstreamIface);
 
-        expectRouterAdvertisement(mDownstreamReader, iface, 2000 /* timeoutMs */);
+        waitForRouterAdvertisement(mDownstreamReader, iface, WAIT_RA_TIMEOUT_MS);
         expectLocalOnlyAddresses(iface);
     }
 
@@ -748,36 +758,42 @@
         }
     }
 
-    private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses)
-            throws Exception {
+    private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses,
+            final List<InetAddress> dnses) throws Exception {
         mTm.setPreferTestNetworks(true);
 
-        return initTestNetwork(mContext, addresses, TIMEOUT_MS);
+        final LinkProperties lp = new LinkProperties();
+        lp.setLinkAddresses(addresses);
+        lp.setDnsServers(dnses);
+        lp.setNat64Prefix(TEST_NAT64PREFIX);
+
+        return initTestNetwork(mContext, lp, TIMEOUT_MS);
     }
 
     @Test
-    public void testTestNetworkUpstream() throws Exception {
-        assumeFalse(mEm.isAvailable());
+    public void testIcmpv6Echo() throws Exception {
+        runPing6Test(initTetheringTester(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
+                toList(TEST_IP4_DNS, TEST_IP6_DNS)));
+    }
 
-        // MyTetheringEventCallback currently only support await first available upstream. Tethering
-        // may select internet network as upstream if test network is not available and not be
-        // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR));
+    private void runPing6Test(TetheringTester tester) throws Exception {
+        TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
+                true /* hasIpv6 */);
+        Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
+        ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
+        tester.verifyUpload(request, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
 
-        mDownstreamIface = createTestInterface();
-        mEm.setIncludeTestInterfaces(true);
+            return isExpectedIcmpv6Packet(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
+        });
 
-        final String iface = mTetheredInterfaceRequester.getInterface();
-        assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
+        ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
+        tester.verifyDownload(reply, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
 
-        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
-                mUpstreamTracker.getNetwork());
-        assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
-                mTetheringEventCallback.awaitUpstreamChanged());
-
-        mDownstreamReader = makePacketReader(mDownstreamIface);
-        // TODO: do basic forwarding test here.
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
+        });
     }
 
     // Test network topology:
@@ -795,12 +811,11 @@
     // Used by public port and private port. Assume port 9876 has not been used yet before the
     // testing that public port and private port are the same in the testing. Note that NAT port
     // forwarding could be different between private port and public port.
+    // TODO: move to the start of test class.
     private static final short LOCAL_PORT = 9876;
     private static final short REMOTE_PORT = 433;
     private static final byte TYPE_OF_SERVICE = 0;
     private static final short ID = 27149;
-    private static final short ID2 = 27150;
-    private static final short ID3 = 27151;
     private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
     private static final byte TIME_TO_LIVE = (byte) 0x40;
     private static final ByteBuffer PAYLOAD =
@@ -810,43 +825,48 @@
     private static final ByteBuffer PAYLOAD3 =
             ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
 
-    private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
-            @NonNull final ByteBuffer payload) {
-        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
-
-        if (hasEther) {
-            final EthernetHeader etherHeader = Struct.parse(EthernetHeader.class, buf);
-            if (etherHeader == null) return false;
-        }
-
-        final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
-        if (ipv4Header == null) return false;
-
-        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
-        if (udpHeader == null) return false;
-
-        if (buf.remaining() != payload.limit()) return false;
-
-        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
-                payload.array());
-    }
-
     @NonNull
-    private ByteBuffer buildUdpv4Packet(@Nullable final MacAddress srcMac,
-            @Nullable final MacAddress dstMac, short id,
-            @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
+    private ByteBuffer buildUdpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
             short srcPort, short dstPort, @Nullable final ByteBuffer payload)
             throws Exception {
+        int ipProto;
+        short ethType;
+        if (srcIp instanceof Inet4Address && dstIp instanceof Inet4Address) {
+            ipProto = IPPROTO_IP;
+            ethType = (short) ETHER_TYPE_IPV4;
+        } else if (srcIp instanceof Inet6Address && dstIp instanceof Inet6Address) {
+            ipProto = IPPROTO_IPV6;
+            ethType = (short) ETHER_TYPE_IPV6;
+        } else {
+            fail("Unsupported conditions: srcIp " + srcIp + ", dstIp " + dstIp);
+            // Make compiler happy to the uninitialized ipProto and ethType.
+            return null;  // unreachable, the annotation @NonNull of function return value is true.
+        }
+
         final boolean hasEther = (srcMac != null && dstMac != null);
         final int payloadLen = (payload == null) ? 0 : payload.limit();
-        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_UDP,
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_UDP,
                 payloadLen);
         final PacketBuilder packetBuilder = new PacketBuilder(buffer);
 
+        // [1] Ethernet header
         if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
-        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
-                TIME_TO_LIVE, (byte) IPPROTO_UDP, srcIp, dstIp);
+
+        // [2] IP header
+        if (ipProto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) IPPROTO_UDP, (Inet4Address) srcIp, (Inet4Address) dstIp);
+        } else {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_UDP,
+                    HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
+        }
+
+        // [3] UDP header
         packetBuilder.writeUdpHeader(srcPort, dstPort);
+
+        // [4] Payload
         if (payload != null) {
             buffer.put(payload);
             // in case data might be reused by caller, restore the position and
@@ -858,38 +878,47 @@
     }
 
     @NonNull
-    private ByteBuffer buildUdpv4Packet(short id, @NonNull final Inet4Address srcIp,
-            @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+    private ByteBuffer buildUdpPacket(@NonNull final InetAddress srcIp,
+            @NonNull final InetAddress dstIp, short srcPort, short dstPort,
             @Nullable final ByteBuffer payload) throws Exception {
-        return buildUdpv4Packet(null /* srcMac */, null /* dstMac */, id, srcIp, dstIp, srcPort,
+        return buildUdpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, srcPort,
                 dstPort, payload);
     }
 
-    // TODO: remove this verification once upstream connected notification race is fixed.
-    // See #runUdp4Test.
-    private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
-            RemoteResponder remote, TetheredDevice tethered) throws Exception {
-        final ByteBuffer probePacket = buildUdpv4Packet(tethered.macAddr,
-                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+    // TODO: remove ipv4 verification (is4To6 = false) once upstream connected notification race is
+    // fixed. See #runUdp4Test.
+    //
+    // This function sends a probe packet to downstream interface and exam the result from upstream
+    // interface to make sure ipv4 tethering is ready. Return the entire packet which received from
+    // upstream interface.
+    @NonNull
+    private byte[] probeV4TetheringConnectivity(TetheringTester tester, TetheredDevice tethered,
+            boolean is4To6) throws Exception {
+        final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 TEST_REACHABILITY_PAYLOAD);
 
         // Send a UDP packet from client and check the packet can be found on upstream interface.
         for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
-            tester.sendPacket(probePacket);
-            byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
+            byte[] expectedPacket = tester.testUpload(probePacket, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, TEST_REACHABILITY_PAYLOAD);
+                // If is4To6 is true, the ipv4 probe packet would be translated to ipv6 by Clat and
+                // would see this translated ipv6 packet in upstream interface.
+                return isExpectedUdpPacket(p, false /* hasEther */, !is4To6 /* isIpv4 */,
+                        TEST_REACHABILITY_PAYLOAD);
             });
-            if (expectedPacket != null) return true;
+            if (expectedPacket != null) return expectedPacket;
         }
-        return false;
+
+        fail("Can't verify " + (is4To6 ? "ipv4 to ipv6" : "ipv4") + " tethering connectivity after "
+                + TETHER_REACHABILITY_ATTEMPTS + " attempts");
+        return null;
     }
 
-    private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
-            throws Exception {
+    private void runUdp4Test(TetheringTester tester, boolean usingBpf) throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
-                "1:2:3:4:5:6"));
+                "1:2:3:4:5:6"), false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
         // Because async upstream connected notification can't guarantee the tethering routing is
@@ -897,26 +926,26 @@
         // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
         // from upstream. That can guarantee that the routing is ready. Long term plan is that
         // refactors upstream connected notification from async to sync.
-        assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // Send a UDP packet in original direction.
-        final ByteBuffer originalPacket = buildUdpv4Packet(tethered.macAddr,
-                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+        final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
-        tester.verifyUpload(remote, originalPacket, p -> {
+        tester.verifyUpload(originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-            return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+            return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
         });
 
         // Send a UDP packet in reply direction.
         final Inet4Address publicIp4Addr = (Inet4Address) TEST_IP4_ADDR.getAddress();
-        final ByteBuffer replyPacket = buildUdpv4Packet(ID2, REMOTE_IP4_ADDR /* srcIp */,
-                publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /*dstPort */,
+        final ByteBuffer replyPacket = buildUdpPacket(REMOTE_IP4_ADDR /* srcIp */,
+                publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
-        remote.verifyDownload(tester, replyPacket, p -> {
+        tester.verifyDownload(replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
-            return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+            return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
 
         if (usingBpf) {
@@ -929,53 +958,96 @@
             // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
             // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
             Thread.sleep(UDP_STREAM_TS_MS);
-            final ByteBuffer originalPacket2 = buildUdpv4Packet(tethered.macAddr,
-                    tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
+            final ByteBuffer originalPacket2 = buildUdpPacket(tethered.macAddr,
+                    tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                     REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
-                    REMOTE_PORT /*dstPort */, PAYLOAD3 /* payload */);
-            tester.verifyUpload(remote, originalPacket2, p -> {
+                    REMOTE_PORT /* dstPort */, PAYLOAD3 /* payload */);
+            tester.verifyUpload(originalPacket2, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
+                return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD3);
             });
 
-            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
+            // [1] Verify IPv4 upstream rule map.
+            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+                    Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
             assertNotNull(upstreamMap);
             assertEquals(1, upstreamMap.size());
 
             final Map.Entry<Tether4Key, Tether4Value> rule =
                     upstreamMap.entrySet().iterator().next();
 
-            final Tether4Key key = rule.getKey();
-            assertEquals(IPPROTO_UDP, key.l4proto);
-            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
-            assertEquals(LOCAL_PORT, key.srcPort);
-            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
-            assertEquals(REMOTE_PORT, key.dstPort);
+            final Tether4Key upstream4Key = rule.getKey();
+            assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+            assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+            assertEquals(REMOTE_PORT, upstream4Key.dstPort);
 
-            final Tether4Value value = rule.getValue();
+            final Tether4Value upstream4Value = rule.getValue();
             assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
-                    InetAddress.getByAddress(value.src46).getAddress()));
-            assertEquals(LOCAL_PORT, value.srcPort);
+                    InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+            assertEquals(LOCAL_PORT, upstream4Value.srcPort);
             assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                    InetAddress.getByAddress(value.dst46).getAddress()));
-            assertEquals(REMOTE_PORT, value.dstPort);
+                    InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+            assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+            // [2] Verify stats map.
+            // Transmit packets on both direction for verifying stats. Because we only care the
+            // packet count in stats test, we just reuse the existing packets to increaes
+            // the packet count on both direction.
+
+            // Send packets on original direction.
+            for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+                tester.verifyUpload(originalPacket, p -> {
+                    Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
+                });
+            }
+
+            // Send packets on reply direction.
+            for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+                tester.verifyDownload(replyPacket, p -> {
+                    Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+                });
+            }
+
+            // Dump stats map to verify.
+            final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                    TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+            assertNotNull(statsMap);
+            assertEquals(1, statsMap.size());
+
+            final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                    statsMap.entrySet().iterator().next();
+
+            // TODO: verify the upstream index in TetherStatsKey.
+
+            final TetherStatsValue statsValue = stats.getValue();
+            assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+            assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+            assertEquals(0, statsValue.rxErrors);
+            assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+            assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+            assertEquals(0, statsValue.txErrors);
         }
     }
 
-    void initializeTethering() throws Exception {
+    private TetheringTester initTetheringTester(List<LinkAddress> upstreamAddresses,
+            List<InetAddress> upstreamDnses) throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
         // may select internet network as upstream if test network is not available and not be
         // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR));
+        mUpstreamTracker = createTestUpstream(upstreamAddresses, upstreamDnses);
 
         mDownstreamIface = createTestInterface();
         mEm.setIncludeTestInterfaces(true);
 
-        final String iface = mTetheredInterfaceRequester.getInterface();
+        // Make sure EtherentTracker use "mDownstreamIface" as server mode interface.
         assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
+                mDownstreamIface.getInterfaceName(), mTetheredInterfaceRequester.getInterface());
 
         mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
                 mUpstreamTracker.getNetwork());
@@ -984,26 +1056,74 @@
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
         mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
+        // sure tethering already have ipv6 connectivity before testing.
+        if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
+            waitForRouterAdvertisement(mDownstreamReader, mDownstreamIface.getInterfaceName(),
+                    WAIT_RA_TIMEOUT_MS);
+        }
+
+        return new TetheringTester(mDownstreamReader, mUpstreamReader);
     }
 
     @Test
-    @IgnoreAfter(Build.VERSION_CODES.Q)
-    public void testTetherUdpV4WithoutBpf() throws Exception {
-        initializeTethering();
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testTetherUdpV4UpToR() throws Exception {
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
                 false /* usingBpf */);
     }
 
+    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+                || current.isAtLeast(new KVersion(5, 4, 98));
+    }
+
+    @Test
+    public void testIsUdpOffloadSupportedByKernel() throws Exception {
+        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+    }
+
+    // TODO: refactor test testTetherUdpV4* into IPv4 UDP non-offload and offload tests.
+    // That can be easier to know which feature is verified from test results.
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testTetherUdpV4WithBpf() throws Exception {
-        initializeTethering();
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
-                true /* usingBpf */);
+    public void testTetherUdpV4AfterR() throws Exception {
+        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+        final boolean isUdpOffloadSupported = isUdpOffloadSupportedByKernel(kernelVersion);
+        if (!isUdpOffloadSupported) {
+            Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
+                    + kernelVersion);
+        }
+        final boolean isTetherConfigBpfOffloadEnabled = isTetherConfigBpfOffloadEnabled();
+        if (!isTetherConfigBpfOffloadEnabled) {
+            Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test "
+                    + "because tethering config doesn't enable BPF offload.");
+        }
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
+                isUdpOffloadSupported && isTetherConfigBpfOffloadEnabled);
     }
 
     @Nullable
-    private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
+    private <K extends Struct, V extends Struct> Pair<K, V> parseMapKeyValue(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String dumpStr) {
         Log.w(TAG, "Parsing string: " + dumpStr);
 
         String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
@@ -1016,36 +1136,38 @@
         Log.d(TAG, "keyBytes: " + dumpHexString(keyBytes));
         final ByteBuffer keyByteBuffer = ByteBuffer.wrap(keyBytes);
         keyByteBuffer.order(ByteOrder.nativeOrder());
-        final Tether4Key tether4Key = Struct.parse(Tether4Key.class, keyByteBuffer);
-        Log.w(TAG, "tether4Key: " + tether4Key);
+        final K k = Struct.parse(keyClass, keyByteBuffer);
 
         final byte[] valueBytes = Base64.decode(keyValueStrs[1], Base64.DEFAULT);
         Log.d(TAG, "valueBytes: " + dumpHexString(valueBytes));
         final ByteBuffer valueByteBuffer = ByteBuffer.wrap(valueBytes);
         valueByteBuffer.order(ByteOrder.nativeOrder());
-        final Tether4Value tether4Value = Struct.parse(Tether4Value.class, valueByteBuffer);
-        Log.w(TAG, "tether4Value: " + tether4Value);
+        final V v = Struct.parse(valueClass, valueByteBuffer);
 
-        return new Pair<>(tether4Key, tether4Value);
+        return new Pair<>(k, v);
     }
 
     @NonNull
-    private HashMap<Tether4Key, Tether4Value> dumpIpv4UpstreamMap() throws Exception {
-        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE,
-                DUMPSYS_TETHERING_RAWMAP_ARG);
-        final HashMap<Tether4Key, Tether4Value> map = new HashMap<>();
+    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args);
+        final HashMap<K, V> map = new HashMap<>();
 
         for (final String line : rawMapStr.split(LINE_DELIMITER)) {
-            final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
+            final Pair<K, V> rule = parseMapKeyValue(keyClass, valueClass, line.trim());
             map.put(rule.first, rule.second);
         }
         return map;
     }
 
     @Nullable
-    private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
+    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
         for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
             if (!map.isEmpty()) return map;
 
             Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -1055,6 +1177,85 @@
         return null;
     }
 
+    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+        final String dumpStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short");
+
+        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+        // RRO to override the enabled default value. Get the tethering config via dumpsys.
+        // $ dumpsys tethering
+        //   mIsBpfEnabled: true
+        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+        if (!enabled) {
+            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+        }
+        return enabled;
+    }
+
+    @NonNull
+    private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
+            throws Exception {
+        // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can
+        // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP
+        // packet.
+        byte[] expectedPacket = probeV4TetheringConnectivity(tester, tethered, true /* is4To6 */);
+
+        // Above has guaranteed that the found packet is an IPv6 packet without ether header.
+        return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
+    }
+
+    // Test network topology:
+    //
+    //            public network (rawip)                 private network
+    //                      |         UE (CLAT support)         |
+    // +---------------+    V    +------------+------------+    V    +------------+
+    // | NAT64 Gateway +---------+  Upstream  | Downstream +---------+   Client   |
+    // +---------------+         +------------+------------+         +------------+
+    // remote ip                 public ip                           private ip
+    // [64:ff9b::808:808]:443    [clat ipv6]:9876                    [TetheredDevice ipv4]:9876
+    //
+    // Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+    // sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+    // packet.
+    //
+    private void runClatUdpTest(TetheringTester tester) throws Exception {
+        final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
+                "1:2:3:4:5:6"), true /* hasIpv6 */);
+
+        // Get CLAT IPv6 address.
+        final Inet6Address clatAddr6 = getClatIpv6Address(tester, tethered);
+
+        // Send an IPv4 UDP packet in original direction.
+        // IPv4 packet -- CLAT translation --> IPv6 packet
+        final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
+                PAYLOAD /* payload */);
+        tester.verifyUpload(originalPacket, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */, PAYLOAD);
+        });
+
+        // Send an IPv6 UDP packet in reply direction.
+        // IPv6 packet -- CLAT translation --> IPv4 packet
+        final ByteBuffer replyPacket = buildUdpPacket(REMOTE_NAT64_ADDR /* srcIp */,
+                clatAddr6 /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
+                PAYLOAD2 /* payload */);
+        tester.verifyDownload(replyPacket, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+        });
+
+        // TODO: test CLAT bpf maps.
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherClatUdp() throws Exception {
+        // CLAT only starts on IPv6 only network.
+        runClatUdpTest(initTetheringTester(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS)));
+    }
+
     private <T> List<T> toList(T... array) {
         return Arrays.asList(array);
     }
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index d24661a..4d90d39 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -16,10 +16,24 @@
 
 package android.net;
 
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
@@ -30,13 +44,30 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.UdpHeader;
 import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Predicate;
@@ -50,31 +81,40 @@
     private static final String TAG = TetheringTester.class.getSimpleName();
     private static final int PACKET_READ_TIMEOUT_MS = 100;
     private static final int DHCP_DISCOVER_ATTEMPTS = 10;
+    private static final int READ_RA_ATTEMPTS = 10;
     private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
             DhcpPacket.DHCP_SUBNET_MASK,
             DhcpPacket.DHCP_ROUTER,
             DhcpPacket.DHCP_DNS_SERVER,
             DhcpPacket.DHCP_LEASE_TIME,
     };
+    private static final InetAddress LINK_LOCAL = parseNumericAddress("fe80::1");
 
     public static final String DHCP_HOSTNAME = "testhostname";
 
     private final ArrayMap<MacAddress, TetheredDevice> mTetheredDevices;
     private final TapPacketReader mDownstreamReader;
+    private final TapPacketReader mUpstreamReader;
 
     public TetheringTester(TapPacketReader downstream) {
+        this(downstream, null);
+    }
+
+    public TetheringTester(TapPacketReader downstream, TapPacketReader upstream) {
         if (downstream == null) fail("Downstream reader could not be NULL");
 
         mDownstreamReader = downstream;
+        mUpstreamReader = upstream;
         mTetheredDevices = new ArrayMap<>();
     }
 
-    public TetheredDevice createTetheredDevice(MacAddress macAddr) throws Exception {
+    public TetheredDevice createTetheredDevice(MacAddress macAddr, boolean hasIpv6)
+            throws Exception {
         if (mTetheredDevices.get(macAddr) != null) {
             fail("Tethered device already created");
         }
 
-        TetheredDevice tethered = new TetheredDevice(macAddr);
+        TetheredDevice tethered = new TetheredDevice(macAddr, hasIpv6);
         mTetheredDevices.put(macAddr, tethered);
 
         return tethered;
@@ -84,14 +124,15 @@
         public final MacAddress macAddr;
         public final MacAddress routerMacAddr;
         public final Inet4Address ipv4Addr;
+        public final Inet6Address ipv6Addr;
 
-        private TetheredDevice(MacAddress mac) throws Exception {
+        private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
             macAddr = mac;
-
             DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
             ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
             routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
                     dhcpResults.serverAddress);
+            ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
         }
     }
 
@@ -141,7 +182,7 @@
     }
 
     private DhcpPacket getNextDhcpPacket() throws Exception {
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             // Test whether this is DHCP packet.
             try {
                 DhcpPacket.decodeFullPacket(p, p.length, DhcpPacket.ENCAP_L2);
@@ -184,7 +225,7 @@
                     tethered.ipv4Addr.getAddress() /* sender IP */,
                     (short) ARP_REPLY);
             try {
-                sendPacket(arpReply);
+                sendUploadPacket(arpReply);
             } catch (Exception e) {
                 fail("Failed to reply ARP for " + tethered.ipv4Addr);
             }
@@ -198,9 +239,9 @@
                 tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
                 new byte[ETHER_ADDR_LEN] /* target HW address */,
                 tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
-        sendPacket(arpProbe);
+        sendUploadPacket(arpProbe);
 
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             final ArpPacket arpPacket = parseArpPacket(p);
             if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
             return arpPacket.targetIp.equals(tetherIp);
@@ -216,45 +257,205 @@
         return null;
     }
 
-    public void sendPacket(ByteBuffer packet) throws Exception {
+    private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        if (!isExpectedIcmpv6Packet(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) {
+            fail("Parsing RA packet fail");
+        }
+
+        Struct.parse(RaHeader.class, buf);
+        final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+        while (buf.position() < packet.length) {
+            final int currentPos = buf.position();
+            final int type = Byte.toUnsignedInt(buf.get());
+            final int length = Byte.toUnsignedInt(buf.get());
+            if (type == ICMPV6_ND_OPTION_PIO) {
+                final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+                        Struct.getSize(PrefixInformationOption.class));
+                final PrefixInformationOption pio =
+                        Struct.parse(PrefixInformationOption.class, pioBuf);
+                pioList.add(pio);
+
+                // Move ByteBuffer position to the next option.
+                buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+            } else {
+                buf.position(currentPos + (length * 8));
+            }
+        }
+        return pioList;
+    }
+
+    private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
+        sendRsPacket(srcMac, dstMac);
+
+        final byte[] raPacket = verifyPacketNotNull("Receive RA fail", getDownloadPacket(p -> {
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+        }));
+
+        final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
+
+        for (PrefixInformationOption pio : options) {
+            if (pio.validLifetime > 0) {
+                final byte[] addressBytes = pio.prefix;
+                // Random the last two bytes as suffix.
+                // TODO: Currently do not implmement DAD in the test. Rely the gateway ipv6 address
+                // genetrated by tethering module always has random the last byte.
+                addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+                addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+
+                return (Inet6Address) InetAddress.getByAddress(addressBytes);
+            }
+        }
+
+        fail("No available ipv6 prefix");
+        return null;
+    }
+
+    private void sendRsPacket(MacAddress srcMac, MacAddress dstMac) throws Exception {
+        Log.d(TAG, "Sending RS");
+        ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+        ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
+                IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+
+        sendUploadPacket(rs);
+    }
+
+    private void maybeReplyNa(byte[] packet) {
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+        if (ethHdr.etherType != ETHER_TYPE_IPV6) return;
+
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+        if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return;
+
+        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+        if (icmpv6Hdr.type != (short) ICMPV6_NEIGHBOR_SOLICITATION) return;
+
+        final NsHeader nsHdr = Struct.parse(NsHeader.class, buf);
+        for (int i = 0; i < mTetheredDevices.size(); i++) {
+            TetheredDevice tethered = mTetheredDevices.valueAt(i);
+            if (!nsHdr.target.equals(tethered.ipv6Addr)) continue;
+
+            final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, tethered.macAddr);
+            int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+                    | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+            ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
+                    nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
+            try {
+                sendUploadPacket(ns);
+            } catch (Exception e) {
+                fail("Failed to reply NA for " + tethered.ipv6Addr);
+            }
+
+            return;
+        }
+    }
+
+    public static boolean isExpectedIcmpv6Packet(byte[] packet, boolean hasEth, int type) {
+        final ByteBuffer buf = ByteBuffer.wrap(packet);
+        return isExpectedIcmpv6Packet(buf, hasEth, type);
+    }
+
+    private static boolean isExpectedIcmpv6Packet(ByteBuffer buf, boolean hasEth, int type) {
+        try {
+            if (hasEth && !hasExpectedEtherHeader(buf, false /* isIpv4 */)) return false;
+
+            if (!hasExpectedIpHeader(buf, false /* isIpv4 */, IPPROTO_ICMPV6)) return false;
+
+            return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not icmpv6 packet.
+        }
+
+        return false;
+    }
+
+    private static boolean hasExpectedEtherHeader(@NonNull final ByteBuffer buf, boolean isIpv4)
+            throws Exception {
+        final int expected = isIpv4 ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+
+        return Struct.parse(EthernetHeader.class, buf).etherType == expected;
+    }
+
+    private static boolean hasExpectedIpHeader(@NonNull final ByteBuffer buf, boolean isIpv4,
+            int ipProto) throws Exception {
+        if (isIpv4) {
+            return Struct.parse(Ipv4Header.class, buf).protocol == (byte) ipProto;
+        } else {
+            return Struct.parse(Ipv6Header.class, buf).nextHeader == (byte) ipProto;
+        }
+    }
+
+    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer payload) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+        try {
+            if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
+
+            if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
+
+            if (Struct.parse(UdpHeader.class, buf) == null) return false;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not udp packet.
+            return false;
+        }
+
+        if (buf.remaining() != payload.limit()) return false;
+
+        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
+                payload.array());
+    }
+
+    private void sendUploadPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
 
-    public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
+    private void sendDownloadPacket(ByteBuffer packet) throws Exception {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        mUpstreamReader.sendResponse(packet);
+    }
+
+    private byte[] getDownloadPacket(Predicate<byte[]> filter) {
         byte[] packet;
         while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
             if (filter.test(packet)) return packet;
 
             maybeReplyArp(packet);
+            maybeReplyNa(packet);
         }
 
         return null;
     }
 
-    public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
-            final Predicate<byte[]> filter) throws Exception {
-        sendPacket(packet);
-        assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
+    private byte[] getUploadPacket(Predicate<byte[]> filter) {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
     }
 
-    public static class RemoteResponder {
-        final TapPacketReader mUpstreamReader;
-        public RemoteResponder(TapPacketReader reader) {
-            mUpstreamReader = reader;
-        }
+    private @NonNull byte[] verifyPacketNotNull(String message, @Nullable byte[] packet) {
+        assertNotNull(message, packet);
 
-        public void sendPacket(ByteBuffer packet) throws Exception {
-            mUpstreamReader.sendResponse(packet);
-        }
+        return packet;
+    }
 
-        public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
-            return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
-        }
+    public byte[] testUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendUploadPacket(packet);
 
-        public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
-                final Predicate<byte[]> filter) throws Exception {
-            sendPacket(packet);
-            assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
-        }
+        return getUploadPacket(filter);
+    }
+
+    public byte[] verifyUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        return verifyPacketNotNull("Upload fail", testUpload(packet, filter));
+    }
+
+    public byte[] verifyDownload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendDownloadPacket(packet);
+
+        return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
 }
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 18fd63b..a84fdd2 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -22,7 +22,7 @@
     name: "MtsTetheringTestLatestSdk",
 
     min_sdk_version: "30",
-    target_sdk_version: "31",
+    target_sdk_version: "33",
 
     libs: [
         "android.test.base",
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index ad2faa0..68c1c57 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -352,15 +352,6 @@
         assertFalse(mTestMap.isEmpty());
         mTestMap.clear();
         assertTrue(mTestMap.isEmpty());
-
-        // Clearing an already-closed map throws.
-        mTestMap.close();
-        try {
-            mTestMap.clear();
-            fail("clearing already-closed map should throw");
-        } catch (ErrnoException expected) {
-            assertEquals(OsConstants.EBADF, expected.errno);
-        }
     }
 
     @Test
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 7ee69b2..d38a7c3 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -28,7 +28,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -38,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
 import org.junit.Before;
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d1b8380..0ee12ad 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -89,7 +89,7 @@
     static_libs: [
         "TetheringApiStableLib",
     ],
-    target_sdk_version: "31",
+    target_sdk_version: "33",
     visibility: [
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 43f1eaa..ef143de 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -86,7 +86,6 @@
 import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -101,8 +100,11 @@
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
@@ -112,10 +114,9 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -186,6 +187,7 @@
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
@@ -235,7 +237,7 @@
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         mIpServer = new IpServer(
                 IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mCallback, mTetherConfig, mAddressCoordinator, mDependencies);
+                mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
         mIpServer.start();
         mNeighborEventConsumer = neighborCaptor.getValue();
 
@@ -367,7 +369,7 @@
                 .thenReturn(mIpNeighborMonitor);
         mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
                 mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
-                mDependencies);
+                mTetheringMetrics, mDependencies);
         mIpServer.start();
         mLooper.dispatchAll();
         verify(mCallback).updateInterfaceState(
@@ -451,6 +453,9 @@
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_BLUETOOTH),
+                eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_BLUETOOTH));
         verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
     }
 
@@ -658,6 +663,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_TETHER_IFACE_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
@@ -676,6 +684,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_ENABLE_FORWARDING_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 4967d27..20a222d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -85,7 +85,6 @@
 import android.net.ip.ConntrackMonitor;
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -100,8 +99,11 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 690ff71..e4263db 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -65,7 +65,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PersistableBundle;
@@ -82,6 +81,7 @@
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.After;
@@ -304,33 +304,6 @@
     }
 
     @Test
-    public void toleratesCarrierConfigManagerMissing() {
-        setupForRequiredProvisioning();
-        mockService(Context.CARRIER_CONFIG_SERVICE, CarrierConfigManager.class, null);
-        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
-        // Therefore provisioning still be required.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
-    public void toleratesCarrierConfigMissing() {
-        setupForRequiredProvisioning();
-        when(mCarrierConfigManager.getConfig()).thenReturn(null);
-        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
-        // We still have a provisioning app configured, so still require provisioning.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
-    public void toleratesCarrierConfigNotLoaded() {
-        setupForRequiredProvisioning();
-        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
-        // We still have a provisioning app configured, so still require provisioning.
-        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
-    }
-
-    @Test
     public void provisioningNotRequiredWhenAppNotFound() {
         setupForRequiredProvisioning();
         when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
@@ -706,8 +679,8 @@
     @IgnoreUpTo(SC_V2)
     public void requestLatestTetheringEntitlementResult_carrierDoesNotSupport_noProvisionCount()
             throws Exception {
-        setupForRequiredProvisioning();
         setupCarrierConfig(false);
+        setupForRequiredProvisioning();
         mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         ResultReceiver receiver = new ResultReceiver(null) {
             @Override
@@ -735,6 +708,7 @@
         mEnMgr.notifyUpstream(false);
         mLooper.dispatchAll();
         setupCarrierConfig(false);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.reevaluateSimCardProvisioning(mConfig);
 
         // Turn on upstream.
@@ -749,8 +723,8 @@
     @IgnoreUpTo(SC_V2)
     public void startProvisioningIfNeeded_carrierUnsupport()
             throws Exception {
-        setupForRequiredProvisioning();
         setupCarrierConfig(false);
+        setupForRequiredProvisioning();
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         verify(mTetherProvisioningFailedListener, never())
                 .onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index ac5c59d..95ec38f 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -18,7 +18,8 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
+
+import com.android.net.module.util.SharedLog;
 
 /** FakeTetheringConfiguration is used to override static method for testing. */
 public class FakeTetheringConfiguration extends TetheringConfiguration {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
index f2b5314..865228a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
@@ -41,11 +41,12 @@
 import android.net.NetworkCapabilities;
 import android.net.RouteInfo;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index e9716b3..faca1c8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,6 +26,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
@@ -66,7 +67,6 @@
 import android.net.NetworkStats.Entry;
 import android.net.RouteInfo;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -78,6 +78,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.TestableNetworkStatsProviderCbBinder;
@@ -668,7 +669,7 @@
 
         if (isAtLeastT()) {
             mTetherStatsProviderCb.expectNotifyLimitReached();
-        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+        } else if (isAtLeastS()) {
             mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
         } else {
             mTetherStatsProviderCb.expectNotifyLimitReached();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index d1891ed..36b439b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -43,7 +43,6 @@
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.NativeHandle;
 import android.os.test.TestLooper;
@@ -55,6 +54,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.StructNfGenMsg;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 7fcf2b2..1a12125 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -22,10 +22,13 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -44,10 +47,11 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
 import android.os.Build;
+import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
 
@@ -56,7 +60,9 @@
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -88,6 +94,7 @@
     private static final long TEST_PACKAGE_VERSION = 1234L;
     @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
+    @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonyManager mTelephonyManager;
     @Mock private Resources mResources;
     @Mock private Resources mResourcesForSubId;
@@ -97,6 +104,7 @@
     private boolean mHasTelephonyManager;
     private MockitoSession mMockingSession;
     private MockContentResolver mContentResolver;
+    private final PersistableBundle mCarrierConfig = new PersistableBundle();
 
     private class MockTetheringConfiguration extends TetheringConfiguration {
         MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
@@ -474,6 +482,56 @@
                 PROVISIONING_APP_RESPONSE);
     }
 
+    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+        when(mMockContext.getSystemServiceName(serviceClass)).thenReturn(serviceName);
+        when(mMockContext.getSystemService(serviceName)).thenReturn(service);
+    }
+
+    @Test
+    public void testGetCarrierConfigBySubId_noCarrierConfigManager_configsAreDefault() {
+        // Act like the CarrierConfigManager is present and ready unless told otherwise.
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, null);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        assertTrue(cfg.isCarrierSupportTethering);
+        assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+    }
+
+    @Test
+    public void testGetCarrierConfigBySubId_carrierConfigMissing_configsAreDefault() {
+        // Act like the CarrierConfigManager is present and ready unless told otherwise.
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, mCarrierConfigManager);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        assertTrue(cfg.isCarrierSupportTethering);
+        assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+    }
+
+    @Test
+    public void testGetCarrierConfigBySubId_hasConfigs_carrierUnsupportAndCheckNotRequired() {
+        mockService(Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class, mCarrierConfigManager);
+        mCarrierConfig.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+        mCarrierConfig.putBoolean(KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+        mCarrierConfig.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+        final TetheringConfiguration cfg = new TetheringConfiguration(
+                mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+        if (SdkLevel.isAtLeastT()) {
+            assertFalse(cfg.isCarrierSupportTethering);
+        } else {
+            assertTrue(cfg.isCarrierSupportTethering);
+        }
+        assertFalse(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+
+    }
+
     @Test
     public void testEnableLegacyWifiP2PAddress() throws Exception {
         final TetheringConfiguration defaultCfg = new TetheringConfiguration(
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index f664d5d..9db8f16 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -275,7 +275,7 @@
         mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                 result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).startTethering(eq(request), eq(result));
+        verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 0388758..e90d27e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -45,6 +45,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
@@ -57,6 +58,7 @@
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -153,7 +155,6 @@
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
 import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
@@ -185,16 +186,21 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.MiscAsserts;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -216,6 +222,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TetheringTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final int IFINDEX_OFFSET = 100;
 
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -236,6 +244,7 @@
     private static final String TEST_WIFI_REGEX = "test_wlan\\d";
     private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
     private static final String TEST_BT_REGEX = "test_pan\\d";
+    private static final String TEST_CALLER_PKG = "com.test.tethering";
 
     private static final int CELLULAR_NETID = 100;
     private static final int WIFI_NETID = 101;
@@ -270,6 +279,7 @@
     @Mock private BluetoothPan mBluetoothPan;
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
+    @Mock private TetheringMetrics mTetheringMetrics;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -293,6 +303,7 @@
     private OffloadController mOffloadCtrl;
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private SoftApCallback mSoftApCallback;
+    private SoftApCallback mLocalOnlyHotspotCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
@@ -494,6 +505,11 @@
         }
 
         @Override
+        public TetheringMetrics getTetheringMetrics() {
+            return mTetheringMetrics;
+        }
+
+        @Override
         public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
             mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
@@ -658,6 +674,14 @@
         verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
         mSoftApCallback = softApCallbackCaptor.getValue();
 
+        if (isAtLeastT()) {
+            final ArgumentCaptor<SoftApCallback> localOnlyCallbackCaptor =
+                    ArgumentCaptor.forClass(SoftApCallback.class);
+            verify(mWifiManager).registerLocalOnlyHotspotSoftApCallback(any(),
+                    localOnlyCallbackCaptor.capture());
+            mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
+        }
+
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
     }
@@ -852,7 +876,8 @@
 
     private void prepareNcmTethering() {
         // Emulate startTethering(TETHERING_NCM) called
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
     }
@@ -860,7 +885,7 @@
     private void prepareUsbTethering() {
         // Emulate pressing the USB tethering button in Settings UI.
         final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
-        mTethering.startTethering(request, null);
+        mTethering.startTethering(request, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
 
         assertEquals(1, mTethering.getActiveTetheringRequests().size());
@@ -933,7 +958,7 @@
 
         // Emulate externally-visible WifiManager effects, when hotspot mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1430,7 +1455,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1457,7 +1483,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1506,7 +1533,7 @@
 
         // Emulate externally-visible WifiManager effects, when tethering mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1533,11 +1560,13 @@
         doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_WIFI), anyString());
 
         // Emulate externally-visible WifiManager effects, causing the
         // per-interface state machine to start up, and telling us that
@@ -1576,6 +1605,10 @@
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
 
+        verify(mTetheringMetrics, times(2)).updateErrorCode(eq(TETHERING_WIFI),
+                eq(TETHER_ERROR_INTERNAL_ERROR));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_WIFI));
+
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
     }
@@ -1867,7 +1900,8 @@
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
@@ -1889,7 +1923,13 @@
         mTethering.unregisterTetheringEventCallback(callback);
         mLooper.dispatchAll();
         mTethering.stopTethering(TETHERING_WIFI);
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+        if (isAtLeastT()) {
+            // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+            callback2.assertNoStateChangeCallback();
+            sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+                    IFACE_IP_MODE_TETHERED);
+        }
         tetherState = callback2.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
         mLooper.dispatchAll();
@@ -1964,10 +2004,12 @@
     public void testNoDuplicatedEthernetRequest() throws Exception {
         final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mEm, times(1)).requestTetheredInterface(any(), any());
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verifyNoMoreInteractions(mEm);
         mTethering.stopTethering(TETHERING_ETHERNET);
@@ -2171,14 +2213,16 @@
         final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
 
         // Enable USB tethering and check that Tethering starts USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         verifyNoMoreInteractions(mUsbManager);
 
         // Enable USB tethering again with the same request and expect no change to USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2187,7 +2231,8 @@
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2216,7 +2261,8 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
         mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
@@ -2284,7 +2330,7 @@
         final TetheringRequestParcel wifiNotExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2298,7 +2344,7 @@
         final TetheringRequestParcel wifiExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2311,14 +2357,14 @@
         // If one app enables tethering without provisioning check first, then another app enables
         // tethering of the same type but does not disable the provisioning check.
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
         assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
         reset(mEntitleMgr);
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2408,7 +2454,8 @@
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
         final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
                 ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+                TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
         TetheredInterfaceCallback ethCallback = callbackCaptor.getValue();
@@ -2490,12 +2537,11 @@
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update lease for local only tethering.
         final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
-        final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
-        p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
-                Long.MAX_VALUE, "test1"));
-        notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
-        final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
+                "192.168.50.24", 24, Long.MAX_VALUE, "test1");
+        final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+                eventCallbacks, p2pLease);
+        callback.expectTetheredClientChanged(p2pClients);
         reset(mDhcpServer);
 
         // Run wifi tethering.
@@ -2505,25 +2551,20 @@
                 any(), dhcpEventCbsCaptor.capture());
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update mac address from softAp callback before getting dhcp lease.
-        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
         final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
-        final WifiClient testClient = mock(WifiClient.class);
-        when(testClient.getMacAddress()).thenReturn(testMac2);
-        wifiClients.add(testClient);
-        mSoftApCallback.onConnectedClientsChanged(wifiClients);
-        final TetheredClient noAddrClient = new TetheredClient(testMac2,
-                Collections.emptyList() /* addresses */, TETHERING_WIFI);
-        clients.add(noAddrClient);
-        callback.expectTetheredClientChanged(clients);
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac2,
+                false /* isLocalOnly */);
+        final List<TetheredClient> p2pAndNoAddrClients = new ArrayList<>(p2pClients);
+        p2pAndNoAddrClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(p2pAndNoAddrClients);
 
         // Update dhcp lease for wifi tethering.
-        clients.remove(noAddrClient);
-        final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
-        wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
-                Long.MAX_VALUE, "test2"));
-        notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
-        clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId2", testMac2,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test2");
+        final List<TetheredClient> p2pAndWifiClients = new ArrayList<>(p2pClients);
+        p2pAndWifiClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease));
+        callback.expectTetheredClientChanged(p2pAndWifiClients);
 
         // Test onStarted callback that register second callback when tethering is running.
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -2531,18 +2572,74 @@
             mTethering.registerTetheringEventCallback(callback2);
             mLooper.dispatchAll();
         });
-        callback2.expectTetheredClientChanged(clients);
+        callback2.expectTetheredClientChanged(p2pAndWifiClients);
     }
 
-    private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
-            IDhcpEventCallbacks callback) throws Exception {
-        callback.onLeasesChanged(leaseParcelables);
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback);
+            mLooper.dispatchAll();
+        });
+        callback.expectTetheredClientChanged(Collections.emptyList());
+
+        // Run local only hotspot.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+        final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+                 ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        final IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
+        // Update mac address from softAp callback before getting dhcp lease.
+        final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac,
+                true /* isLocalOnly */);
+        final List<TetheredClient> noAddrLocalOnlyClients = new ArrayList<>();
+        noAddrLocalOnlyClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(noAddrLocalOnlyClients);
+
+        // Update dhcp lease for local only hotspot.
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", testMac,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test");
+        final List<TetheredClient> localOnlyClients = notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease);
+        callback.expectTetheredClientChanged(localOnlyClients);
+
+        // Client disconnect from local only hotspot.
+        mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
+        callback.expectTetheredClientChanged(Collections.emptyList());
+    }
+
+    private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
+            boolean isLocalOnly) throws Exception {
+        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+        final WifiClient testClient = mock(WifiClient.class);
+        when(testClient.getMacAddress()).thenReturn(mac);
+        wifiClients.add(testClient);
+        if (isLocalOnly) {
+            mLocalOnlyHotspotCallback.onConnectedClientsChanged(wifiClients);
+        } else {
+            mSoftApCallback.onConnectedClientsChanged(wifiClients);
+        }
+        return new TetheredClient(mac, Collections.emptyList() /* addresses */, TETHERING_WIFI);
+    }
+
+    private List<TetheredClient> notifyDhcpLeasesChanged(int type, IDhcpEventCallbacks callback,
+            DhcpLeaseParcelable... leases) throws Exception {
+        final List<DhcpLeaseParcelable> dhcpLeases = Arrays.asList(leases);
+        callback.onLeasesChanged(dhcpLeases);
         mLooper.dispatchAll();
+
+        return toTetheredClients(dhcpLeases, type);
     }
 
     private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
             int type) throws Exception {
-        final ArrayList<TetheredClient> leases = new ArrayList<>();
+        final ArrayList<TetheredClient> clients = new ArrayList<>();
         for (DhcpLeaseParcelable lease : leaseParcelables) {
             final LinkAddress address = new LinkAddress(
                     intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
@@ -2552,13 +2649,13 @@
             final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
 
             final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
-            leases.add(new TetheredClient(
+            clients.add(new TetheredClient(
                     macAddress,
                     Collections.singletonList(addressInfo),
                     type));
         }
 
-        return leases;
+        return clients;
     }
 
     private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
@@ -2583,7 +2680,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2618,7 +2716,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2639,7 +2738,8 @@
         // already bound.
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, secondResult);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
         secondResult.assertHasResult();
@@ -2660,7 +2760,8 @@
     public void testBluetoothServiceDisconnects() throws Exception {
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
                 true /* bindToPanService */);
@@ -2811,18 +2912,26 @@
         runNcmTethering();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_NCM), anyString());
 
         // Change the USB tethering function to NCM. Because the USB tethering function was set to
         // RNDIS (the default), tethering is stopped.
         forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
         verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+        verify(mTetheringMetrics).updateErrorCode(anyInt(), eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_NCM));
 
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
         // available.
         final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), ncmResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                ncmResult);
         mLooper.dispatchAll();
         ncmResult.assertHasResult();
+        verify(mTetheringMetrics, times(2)).createBuilder(eq(TETHERING_NCM), anyString());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_NCM),
+                eq(TETHER_ERROR_SERVICE_UNAVAIL));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_NCM));
 
         // Run TETHERING_USB with ncm configuration.
         runDualStackUsbTethering(TEST_NCM_IFNAME);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 97cebd8..9b9507b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -49,7 +49,6 @@
 import android.net.LinkProperties;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -60,6 +59,7 @@
 
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
new file mode 100644
index 0000000..6a85718
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TetheringMetricsTest {
+    private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
+    private static final String SETTINGS_PKG = "com.android.settings";
+    private static final String SYSTEMUI_PKG = "com.android.systemui";
+    private static final String GMS_PKG = "com.google.android.gms";
+    private TetheringMetrics mTetheringMetrics;
+
+    private final NetworkTetheringReported.Builder mStatsBuilder =
+            NetworkTetheringReported.newBuilder();
+
+    private class MockTetheringMetrics extends TetheringMetrics {
+        @Override
+        public void write(final NetworkTetheringReported reported) { }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTetheringMetrics = spy(new MockTetheringMetrics());
+    }
+
+    private void verifyReport(DownstreamType downstream, ErrorCode error, UserType user)
+            throws Exception {
+        final NetworkTetheringReported expectedReport =
+                mStatsBuilder.setDownstreamType(downstream)
+                .setUserType(user)
+                .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                .setErrorCode(error)
+                .build();
+        verify(mTetheringMetrics).write(expectedReport);
+    }
+
+    private void updateErrorAndSendReport(int downstream, int error) {
+        mTetheringMetrics.updateErrorCode(downstream, error);
+        mTetheringMetrics.sendReport(downstream);
+    }
+
+    private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
+            throws Exception {
+        for (Pair<Integer, DownstreamType> testPair : testPairs) {
+            final int type = testPair.first;
+            final DownstreamType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+            updateErrorAndSendReport(type, TETHER_ERROR_NO_ERROR);
+            verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testDownstreamTypes() throws Exception {
+        runDownstreamTypesTest(new Pair<>(TETHERING_WIFI, DownstreamType.DS_TETHERING_WIFI),
+                new Pair<>(TETHERING_WIFI_P2P, DownstreamType.DS_TETHERING_WIFI_P2P),
+                new Pair<>(TETHERING_BLUETOOTH, DownstreamType.DS_TETHERING_BLUETOOTH),
+                new Pair<>(TETHERING_USB, DownstreamType.DS_TETHERING_USB),
+                new Pair<>(TETHERING_NCM, DownstreamType.DS_TETHERING_NCM),
+                new Pair<>(TETHERING_ETHERNET, DownstreamType.DS_TETHERING_ETHERNET));
+    }
+
+    private void runErrorCodesTest(final Pair<Integer, ErrorCode>... testPairs)
+            throws Exception {
+        for (Pair<Integer, ErrorCode> testPair : testPairs) {
+            final int errorCode = testPair.first;
+            final ErrorCode expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
+            updateErrorAndSendReport(TETHERING_WIFI, errorCode);
+            verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testErrorCodes() throws Exception {
+        runErrorCodesTest(new Pair<>(TETHER_ERROR_NO_ERROR, ErrorCode.EC_NO_ERROR),
+                new Pair<>(TETHER_ERROR_UNKNOWN_IFACE, ErrorCode.EC_UNKNOWN_IFACE),
+                new Pair<>(TETHER_ERROR_SERVICE_UNAVAIL, ErrorCode.EC_SERVICE_UNAVAIL),
+                new Pair<>(TETHER_ERROR_UNSUPPORTED, ErrorCode.EC_UNSUPPORTED),
+                new Pair<>(TETHER_ERROR_UNAVAIL_IFACE, ErrorCode.EC_UNAVAIL_IFACE),
+                new Pair<>(TETHER_ERROR_INTERNAL_ERROR, ErrorCode.EC_INTERNAL_ERROR),
+                new Pair<>(TETHER_ERROR_TETHER_IFACE_ERROR, ErrorCode.EC_TETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_UNTETHER_IFACE_ERROR, ErrorCode.EC_UNTETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_ENABLE_FORWARDING_ERROR,
+                ErrorCode.EC_ENABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_DISABLE_FORWARDING_ERROR,
+                ErrorCode.EC_DISABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_IFACE_CFG_ERROR, ErrorCode.EC_IFACE_CFG_ERROR),
+                new Pair<>(TETHER_ERROR_PROVISIONING_FAILED, ErrorCode.EC_PROVISIONING_FAILED),
+                new Pair<>(TETHER_ERROR_DHCPSERVER_ERROR, ErrorCode.EC_DHCPSERVER_ERROR),
+                new Pair<>(TETHER_ERROR_ENTITLEMENT_UNKNOWN, ErrorCode.EC_ENTITLEMENT_UNKNOWN),
+                new Pair<>(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_UNKNOWN_TYPE, ErrorCode.EC_UNKNOWN_TYPE));
+    }
+
+    private void runUserTypesTest(final Pair<String, UserType>... testPairs)
+            throws Exception {
+        for (Pair<String, UserType> testPair : testPairs) {
+            final String callerPkg = testPair.first;
+            final UserType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+            updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+            verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testUserTypes() throws Exception {
+        runUserTypesTest(new Pair<>(TEST_CALLER_PKG, UserType.USER_UNKNOWN),
+                new Pair<>(SETTINGS_PKG, UserType.USER_SETTINGS),
+                new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
+                new Pair<>(GMS_PKG, UserType.USER_GMS));
+    }
+
+    @Test
+    public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
+        mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+        mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+        mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+
+        updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_DHCPSERVER_ERROR,
+                UserType.USER_SETTINGS);
+
+        updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_ENABLE_FORWARDING_ERROR,
+                UserType.USER_SYSTEMUI);
+
+        updateErrorAndSendReport(TETHERING_BLUETOOTH, TETHER_ERROR_TETHER_IFACE_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_BLUETOOTH, ErrorCode.EC_TETHER_IFACE_ERROR,
+                UserType.USER_GMS);
+    }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 1fe0e9a..78fca29 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -25,8 +25,14 @@
     name: "bpf_connectivity_headers",
     vendor_available: false,
     host_supported: false,
-    header_libs: ["bpf_headers"],
-    export_header_lib_headers: ["bpf_headers"],
+    header_libs: [
+        "bpf_headers",
+        "netd_mainline_headers",
+    ],
+    export_header_lib_headers: [
+        "bpf_headers",
+        "netd_mainline_headers",
+    ],
     export_include_dirs: ["."],
     cflags: [
         "-Wall",
@@ -37,11 +43,8 @@
     apex_available: [
         "//apex_available:platform",
         "com.android.tethering",
-        ],
+    ],
     visibility: [
-        // 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/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
@@ -50,7 +53,6 @@
         "//packages/modules/Connectivity/tests/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        "//system/netd/server",
         "//system/netd/tests",
     ],
 }
@@ -61,6 +63,7 @@
 bpf {
     name: "block.o",
     srcs: ["block.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -71,6 +74,7 @@
 bpf {
     name: "dscp_policy.o",
     srcs: ["dscp_policy.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -97,27 +101,25 @@
 }
 
 bpf {
-    name: "clatd.o_mainline",
+    name: "clatd.o",
     srcs: ["clatd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
-    name: "netd.o_mainline",
+    // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
+    name: "netd.o",
     srcs: ["netd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
-    sub_dir: "net_shared",
+    // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
+    sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index ddd9a1c..f2a3e62 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,6 +19,9 @@
 #include <netinet/in.h>
 #include <stdint.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 
 #define ALLOW 1
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 2ddc7b8..85b9f86 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -21,6 +21,11 @@
 #include <linux/in.h>
 #include <linux/in6.h>
 
+#ifdef __cplusplus
+#include <string_view>
+#include "XtBpfProgLocations.h"
+#endif
+
 // This header file is shared by eBPF kernel programs (C) and netd (C++) and
 // some of the maps are also accessed directly from Java mainline module code.
 //
@@ -98,30 +103,52 @@
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 2000;
 
-#define BPF_PATH "/sys/fs/bpf/"
+#ifdef __cplusplus
 
-#define BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_egress_stats"
-#define BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_denylist_xtbpf"
-#define CGROUP_SOCKET_PROG_PATH BPF_PATH "prog_netd_cgroupsock_inet_create"
+#define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
+
+#define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
+#define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
+
+#define ASSERT_STRING_EQUAL(s1, s2) \
+    static_assert(std::string_view(s1) == std::string_view(s2), "mismatch vs Android T netd")
+
+/* -=-=-=-=- WARNING -=-=-=-=-
+ *
+ * These 4 xt_bpf program paths are actually defined by:
+ *   //system/netd/include/mainline/XtBpfProgLocations.h
+ * which is intentionally a non-automerged location.
+ *
+ * They are *UNCHANGEABLE* due to being hard coded in Android T's netd binary
+ * as such we have compile time asserts that things match.
+ * (which will be validated during build on mainline-prod branch against old system/netd)
+ *
+ * If you break this, netd on T will fail to start with your tethering mainline module.
+ */
+ASSERT_STRING_EQUAL(XT_BPF_INGRESS_PROG_PATH,   BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_EGRESS_PROG_PATH,    BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_ALLOWLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH,  BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf");
+
+#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
-#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
+#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
 
-#define COOKIE_TAG_MAP_PATH BPF_PATH "map_netd_cookie_tag_map"
-#define UID_COUNTERSET_MAP_PATH BPF_PATH "map_netd_uid_counterset_map"
-#define APP_UID_STATS_MAP_PATH BPF_PATH "map_netd_app_uid_stats_map"
-#define STATS_MAP_A_PATH BPF_PATH "map_netd_stats_map_A"
-#define STATS_MAP_B_PATH BPF_PATH "map_netd_stats_map_B"
-#define IFACE_INDEX_NAME_MAP_PATH BPF_PATH "map_netd_iface_index_name_map"
-#define IFACE_STATS_MAP_PATH BPF_PATH "map_netd_iface_stats_map"
-#define CONFIGURATION_MAP_PATH BPF_PATH "map_netd_configuration_map"
-#define UID_OWNER_MAP_PATH BPF_PATH "map_netd_uid_owner_map"
-#define UID_PERMISSION_MAP_PATH BPF_PATH "map_netd_uid_permission_map"
+#define COOKIE_TAG_MAP_PATH BPF_NETD_PATH "map_netd_cookie_tag_map"
+#define UID_COUNTERSET_MAP_PATH BPF_NETD_PATH "map_netd_uid_counterset_map"
+#define APP_UID_STATS_MAP_PATH BPF_NETD_PATH "map_netd_app_uid_stats_map"
+#define STATS_MAP_A_PATH BPF_NETD_PATH "map_netd_stats_map_A"
+#define STATS_MAP_B_PATH BPF_NETD_PATH "map_netd_stats_map_B"
+#define IFACE_INDEX_NAME_MAP_PATH BPF_NETD_PATH "map_netd_iface_index_name_map"
+#define IFACE_STATS_MAP_PATH BPF_NETD_PATH "map_netd_iface_stats_map"
+#define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
+#define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
+#define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
 
+#endif // __cplusplus
+
+// LINT.IfChange(match_type)
 enum UidOwnerMatchType {
     NO_MATCH = 0,
     HAPPY_BOX_MATCH = (1 << 0),
@@ -132,7 +159,12 @@
     RESTRICTED_MATCH = (1 << 5),
     LOW_POWER_STANDBY_MATCH = (1 << 6),
     IIF_MATCH = (1 << 7),
+    LOCKDOWN_VPN_MATCH = (1 << 8),
+    OEM_DENY_1_MATCH = (1 << 9),
+    OEM_DENY_2_MATCH = (1 << 10),
+    OEM_DENY_3_MATCH = (1 << 11),
 };
+// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
 
 enum BpfPermissionMatch {
     BPF_PERMISSION_INTERNET = 1 << 2,
@@ -146,9 +178,9 @@
     SELECT_MAP_B,
 };
 
-// TODO: change the configuration object from an 8-bit bitmask to an object with clearer
+// TODO: change the configuration object from a bitmask to an object with clearer
 // semantics, like a struct.
-typedef uint8_t BpfConfig;
+typedef uint32_t BpfConfig;
 static const BpfConfig DEFAULT_CONFIG = 0;
 
 typedef struct {
@@ -159,16 +191,10 @@
 } UidOwnerValue;
 STRUCT_SIZE(UidOwnerValue, 2 * 4);  // 8
 
-#define UID_RULES_CONFIGURATION_KEY 1
-#define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
-
-#define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
-#define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
-
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_PATH CLAT_INGRESS6_PROG_ETHER_NAME
-
-#define CLAT_INGRESS6_MAP_PATH BPF_PATH "map_clatd_clat_ingress6_map"
+// Entry in the configuration map that stores which UID rules are enabled.
+#define UID_RULES_CONFIGURATION_KEY 0
+// Entry in the configuration map that stores which stats map is currently in use.
+#define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
 
 typedef struct {
     uint32_t iif;            // The input interface index
@@ -183,14 +209,6 @@
 } ClatIngress6Value;
 STRUCT_SIZE(ClatIngress6Value, 4 + 4);  // 8
 
-#define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
-#define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
-
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_PATH CLAT_EGRESS4_PROG_ETHER_NAME
-
-#define CLAT_EGRESS4_MAP_PATH BPF_PATH "map_clatd_clat_egress4_map"
-
 typedef struct {
     uint32_t iif;           // The input interface index
     struct in_addr local4;  // The source IPv4 address
diff --git a/bpf_progs/bpf_tethering.h b/bpf_progs/bpf_tethering.h
index b0ec8f6..f9ef6ef 100644
--- a/bpf_progs/bpf_tethering.h
+++ b/bpf_progs/bpf_tethering.h
@@ -73,10 +73,6 @@
 #define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
 
 
-#define BPF_PATH_TETHER BPF_PATH "tethering/"
-
-#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
-
 typedef uint32_t TetherStatsKey;  // upstream ifindex
 
 typedef struct {
@@ -89,19 +85,9 @@
 } TetherStatsValue;
 STRUCT_SIZE(TetherStatsValue, 6 * 8);  // 48
 
-#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
-
 typedef uint32_t TetherLimitKey;    // upstream ifindex
 typedef uint64_t TetherLimitValue;  // in bytes
 
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
-
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
-
 // For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
 // because all downstream types that are currently supported (WiFi, USB, Bluetooth and
 // Ethernet) have 6-byte MAC addresses.
@@ -121,8 +107,6 @@
 } Tether6Value;
 STRUCT_SIZE(Tether6Value, 4 + 14 + 2);  // 20
 
-#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -146,14 +130,6 @@
 } TetherDownstream64Value;
 STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8);  // 40
 
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
-
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -162,23 +138,6 @@
 } TetherUpstream6Key;
 STRUCT_SIZE(TetherUpstream6Key, 12);
 
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
-
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
-
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -202,16 +161,4 @@
 } Tether4Value;
 STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8);  // 64
 
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
-
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
-
 #undef STRUCT_SIZE
diff --git a/bpf_progs/clat_mark.h b/bpf_progs/clat_mark.h
new file mode 100644
index 0000000..874d6ae
--- /dev/null
+++ b/bpf_progs/clat_mark.h
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/* -=-=-=-=-= WARNING -=-=-=-=-=-
+ *
+ * DO *NOT* *EVER* CHANGE THIS CONSTANT
+ *
+ * This is aidl::android::net::INetd::CLAT_MARK but we can't use that from
+ * pure C code (ie. the eBPF clat program).
+ *
+ * It must match the iptables rules setup by netd on Android T.
+ *
+ * This mark value is used by the eBPF clatd program to mark ingress non-offloaded clat
+ * packets for later dropping in ip6tables bw_raw_PREROUTING.
+ * They need to be dropped *after* the clat daemon (via receive on an AF_PACKET socket)
+ * sees them and thus cannot be dropped from the bpf program itself.
+ */
+static const uint32_t CLAT_MARK = 0xDEADC1A7;
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 9a9d337..66e9616 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,19 +30,17 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "bpf_shared.h"
+#include "clat_mark.h"
 
 // From kernel:include/net/ip.h
 #define IP_DF 0x4000  // Flag: "Don't Fragment"
 
-// Used for iptables drops ingress clat packet. Beware of clat mark change may break the device
-// which is using the old clat mark in netd platform code. The reason is that the clat mark is a
-// mainline constant since T+ but netd iptable rules (ex: bandwidth control, firewall, and so on)
-// are set in stone.
-#define CLAT_MARK 0xdeadc1a7
-
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
index 9989e6b..92ea0e2 100644
--- a/bpf_progs/dscp_policy.c
+++ b/bpf_progs/dscp_policy.c
@@ -14,262 +14,301 @@
  * limitations under the License.
  */
 
-#include <linux/types.h>
 #include <linux/bpf.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
 #include <linux/ip.h>
 #include <linux/ipv6.h>
-#include <linux/if_ether.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
-#include <stdint.h>
+#include <linux/types.h>
 #include <netinet/in.h>
 #include <netinet/udp.h>
+#include <stdint.h>
 #include <string.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
+#include "dscp_policy.h"
 
-#define MAX_POLICIES 16
-#define MAP_A 1
-#define MAP_B 2
+#define ECN_MASK 3
+#define IP4_OFFSET(field, header) (header + offsetof(struct iphdr, field))
+#define UPDATE_TOS(dscp, tos) (dscp << 2) | (tos & ECN_MASK)
+#define UPDATE_PRIORITY(dscp) ((dscp >> 2) + 0x60)
+#define UPDATE_FLOW_LABEL(dscp, flow_lbl) ((dscp & 0xf) << 6) + (flow_lbl >> 6)
 
-#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
-
-// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
-// should they be moved to common location?
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
-                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
-                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-
-typedef struct {
-    // Add family here to match __sk_buff ?
-    struct in_addr srcIp;
-    struct in_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPort;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t pad[2];
-} Ipv4RuleEntry;
-STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2);  // 16, 4 for in_addr
-
-#define SRC_IP_MASK     1
-#define DST_IP_MASK     2
-#define SRC_PORT_MASK   4
-#define DST_PORT_MASK   8
-#define PROTO_MASK      16
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    uint8_t pad[3];
-} Ipv4Policy;
-STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    // should we override this struct to include the param bitmask for linear search?
-    // For mapping socket to policies, all the params should match exactly since we can
-    // pull any missing from the sock itself.
-} Ipv6RuleEntry;
-STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-// TODO: move to using 1 map. Map v4 address to 0xffff::v4
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
 DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
 
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
 
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
-                     schedcls_set_dscp, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
-    int one = 0;
-    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4, bool is_eth) {
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+
+    const int l2_header_size = is_eth ? sizeof(struct ethhdr) : 0;
+    struct ethhdr* eth = is_eth ? data : NULL;
+
+    if (data + l2_header_size > data_end) return;
+
+    int zero = 0;
+    int hdr_size = 0;
+    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&zero);
 
     // use this with HASH map so map lookup only happens once policies have been added?
     if (!selectedMap) {
-        return TC_ACT_PIPE;
+        return;
     }
 
     // used for map lookup
     uint64_t cookie = bpf_get_socket_cookie(skb);
+    if (!cookie) return;
 
-    // Do we need separate maps for ipv4/ipv6
-    if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
-        Ipv4RuleEntry* v4Policy;
-        if (*selectedMap == MAP_A) {
-            v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
-        } else {
-            v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
-        }
-
-        // How to use bitmask here to compare params efficiently?
-        // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
-
-        void* data = (void*)(long)skb->data;
-        const void* data_end = (void*)(long)skb->data_end;
-        const struct iphdr* const iph = data;
-
+    uint16_t sport = 0;
+    uint16_t dport = 0;
+    uint8_t protocol = 0;  // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+    struct in6_addr srcIp = {};
+    struct in6_addr dstIp = {};
+    uint8_t tos = 0;       // Only used for IPv4
+    uint8_t priority = 0;  // Only used for IPv6
+    uint8_t flow_lbl = 0;  // Only used for IPv6
+    if (ipv4) {
+        const struct iphdr* const iph = is_eth ? (void*)(eth + 1) : data;
+        hdr_size = l2_header_size + sizeof(struct iphdr);
         // Must have ipv4 header
-        if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+        if (data + hdr_size > data_end) return;
 
         // IP version must be 4
-        if (iph->version != 4) return TC_ACT_PIPE;
+        if (iph->version != 4) return;
 
         // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-        if (iph->ihl != 5) return TC_ACT_PIPE;
+        if (iph->ihl != 5) return;
 
-        if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+        // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+        srcIp.s6_addr32[2] = htonl(0x0000ffff);
+        dstIp.s6_addr32[2] = htonl(0x0000ffff);
 
-        struct udphdr *udp;
-        udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+        // Copy IPv4 address into in6_addr for easy comparison below.
+        srcIp.s6_addr32[3] = iph->saddr;
+        dstIp.s6_addr32[3] = iph->daddr;
+        protocol = iph->protocol;
+        tos = iph->tos;
+    } else {
+        struct ipv6hdr* ip6h = is_eth ? (void*)(eth + 1) : data;
+        hdr_size = l2_header_size + sizeof(struct ipv6hdr);
+        // Must have ipv6 header
+        if (data + hdr_size > data_end) return;
 
-        if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+        if (ip6h->version != 6) return;
 
-        // Source/destination port in udphdr are stored in be16, need to convert to le16.
-        // This can be done via ntohs or htons. Is there a more preferred way?
-        // Cached policy was found.
-        if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
-                    iph->daddr == v4Policy->dstIp.s_addr &&
-                    ntohs(udp->source) == v4Policy->srcPort &&
-                    ntohs(udp->dest) == v4Policy->dstPort &&
-                    iph->protocol == v4Policy->proto) {
-            // set dscpVal in packet. Least sig 2 bits of TOS
-            // reference ipv4_change_dsfield()
+        srcIp = ip6h->saddr;
+        dstIp = ip6h->daddr;
+        protocol = ip6h->nexthdr;
+        priority = ip6h->priority;
+        flow_lbl = ip6h->flow_lbl[0];
+    }
 
-            // TODO: fix checksum...
-            int ecn = iph->tos & 3;
-            uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
-            int oldDscpVal = iph->tos >> 2;
-            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
-            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
-            return TC_ACT_PIPE;
+    switch (protocol) {
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE: {
+            struct udphdr* udp;
+            udp = data + hdr_size;
+            if ((void*)(udp + 1) > data_end) return;
+            sport = udp->source;
+            dport = udp->dest;
+        } break;
+        case IPPROTO_TCP: {
+            struct tcphdr* tcp;
+            tcp = data + hdr_size;
+            if ((void*)(tcp + 1) > data_end) return;
+            sport = tcp->source;
+            dport = tcp->dest;
+        } break;
+        default:
+            return;
+    }
+
+    RuleEntry* existingRule;
+    if (ipv4) {
+        if (*selectedMap == MAP_A) {
+            existingRule = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existingRule = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    } else {
+        if (*selectedMap == MAP_A) {
+            existingRule = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existingRule = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    }
+
+    if (existingRule && v6_equal(srcIp, existingRule->srcIp) &&
+        v6_equal(dstIp, existingRule->dstIp) && skb->ifindex == existingRule->ifindex &&
+        ntohs(sport) == htons(existingRule->srcPort) &&
+        ntohs(dport) == htons(existingRule->dstPort) && protocol == existingRule->proto) {
+        if (ipv4) {
+            uint8_t newTos = UPDATE_TOS(existingRule->dscpVal, tos);
+            bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
+                                sizeof(uint16_t));
+            bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
+        } else {
+            uint8_t new_priority = UPDATE_PRIORITY(existingRule->dscpVal);
+            uint8_t new_flow_label = UPDATE_FLOW_LABEL(existingRule->dscpVal, flow_lbl);
+            bpf_skb_store_bytes(skb, 0 + l2_header_size, &new_priority, sizeof(uint8_t), 0);
+            bpf_skb_store_bytes(skb, 1 + l2_header_size, &new_flow_label, sizeof(uint8_t), 0);
+        }
+        return;
+    }
+
+    // Linear scan ipv4_dscp_policies_map since no stored params match skb.
+    int bestScore = -1;
+    uint32_t bestMatch = 0;
+
+    for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+        int score = 0;
+        uint8_t tempMask = 0;
+        // Using a uint64 in for loop prevents infinite loop during BPF load,
+        // but the key is uint32, so convert back.
+        uint32_t key = i;
+
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
         }
 
-        // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
-        int bestScore = -1;
-        uint32_t bestMatch = 0;
+        // If the policy lookup failed, presentFields is 0, or iface index does not match
+        // index on skb buff, then we can continue to next policy.
+        if (!policy || policy->presentFields == 0 || policy->ifindex != skb->ifindex) continue;
 
-        for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
-            int score = 0;
-            uint8_t tempMask = 0;
-            // Using a uint62 in for loop prevents infinite loop during BPF load,
-            // but the key is uint32, so convert back.
-            uint32_t key = i;
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        if ((policy->presentFields & SRC_IP_MASK_FLAG) == SRC_IP_MASK_FLAG &&
+            v6_equal(srcIp, policy->srcIp)) {
+            score++;
+            tempMask |= SRC_IP_MASK_FLAG;
+        }
+        if ((policy->presentFields & DST_IP_MASK_FLAG) == DST_IP_MASK_FLAG &&
+            v6_equal(dstIp, policy->dstIp)) {
+            score++;
+            tempMask |= DST_IP_MASK_FLAG;
+        }
+        if ((policy->presentFields & SRC_PORT_MASK_FLAG) == SRC_PORT_MASK_FLAG &&
+            ntohs(sport) == htons(policy->srcPort)) {
+            score++;
+            tempMask |= SRC_PORT_MASK_FLAG;
+        }
+        if ((policy->presentFields & DST_PORT_MASK_FLAG) == DST_PORT_MASK_FLAG &&
+            ntohs(dport) >= htons(policy->dstPortStart) &&
+            ntohs(dport) <= htons(policy->dstPortEnd)) {
+            score++;
+            tempMask |= DST_PORT_MASK_FLAG;
+        }
+        if ((policy->presentFields & PROTO_MASK_FLAG) == PROTO_MASK_FLAG &&
+            protocol == policy->proto) {
+            score++;
+            tempMask |= PROTO_MASK_FLAG;
+        }
 
-            // if mask is 0 continue, key does not have corresponding policy value
-            if (policy && policy->mask != 0) {
-                if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
-                        iph->saddr == policy->srcIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= SRC_IP_MASK;
-                }
-                if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
-                        iph->daddr == policy->dstIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= DST_IP_MASK;
-                }
-                if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
-                        ntohs(udp->source) == htons(policy->srcPort)) {
-                    score++;
-                    tempMask |= SRC_PORT_MASK;
-                }
-                if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
-                        ntohs(udp->dest) >= htons(policy->dstPortStart) &&
-                        ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
-                    score++;
-                    tempMask |= DST_PORT_MASK;
-                }
-                if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
-                        iph->protocol == policy->proto) {
-                    score++;
-                    tempMask |= PROTO_MASK;
-                }
+        if (score > bestScore && tempMask == policy->presentFields) {
+            bestMatch = i;
+            bestScore = score;
+        }
+    }
 
-                if (score > bestScore && tempMask == policy->mask) {
-                    bestMatch = i;
-                    bestScore = score;
-                }
+    uint8_t new_tos = 0;  // Can 0 be used as default forwarding value?
+    uint8_t new_dscp = 0;
+    uint8_t new_priority = 0;
+    uint8_t new_flow_lbl = 0;
+    if (bestScore > 0) {
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&bestMatch);
+        }
+
+        if (policy) {
+            new_dscp = policy->dscpVal;
+            if (ipv4) {
+                new_tos = UPDATE_TOS(new_dscp, tos);
+            } else {
+                new_priority = UPDATE_PRIORITY(new_dscp);
+                new_flow_lbl = UPDATE_FLOW_LABEL(new_dscp, flow_lbl);
             }
         }
+    } else
+        return;
 
-        uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
-        uint8_t curDscp = iph->tos & 252;
-        if (bestScore > 0) {
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
-            if (policy) {
-                // TODO: if DSCP value is already set ignore?
-                // TODO: update checksum, for testing increment counter...
-                int ecn = iph->tos & 3;
-                newDscpVal = (policy->dscpVal << 2) + ecn;
-            }
-        }
+    RuleEntry value = {
+        .srcIp = srcIp,
+        .dstIp = dstIp,
+        .ifindex = skb->ifindex,
+        .srcPort = sport,
+        .dstPort = dport,
+        .proto = protocol,
+        .dscpVal = new_dscp,
+    };
 
-        Ipv4RuleEntry value = {
-            .srcIp.s_addr = iph->saddr,
-            .dstIp.s_addr = iph->daddr,
-            .srcPort = udp->source,
-            .dstPort = udp->dest,
-            .proto = iph->protocol,
-            .dscpVal = newDscpVal,
-        };
-
-        if (!cookie)
-            return TC_ACT_PIPE;
-
-        // Update map
+    // Update map with new policy.
+    if (ipv4) {
         if (*selectedMap == MAP_A) {
             bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
         } else {
             bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
         }
-
-        // Need to store bytes after updating map or program will not load.
-        if (newDscpVal != curDscp) {
-            // 1 is the offset (Version/Header length)
-            int oldDscpVal = iph->tos >> 2;
-            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
-            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
-        }
-
-    } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
-        Ipv6RuleEntry* v6Policy;
+    } else {
         if (*selectedMap == MAP_A) {
-            v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
         } else {
-            v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
         }
+    }
 
-        if (!v6Policy)
-            return TC_ACT_PIPE;
+    // Need to store bytes after updating map or program will not load.
+    if (ipv4 && new_tos != (tos & 252)) {
+        bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
+        bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
+    } else if (!ipv4 && (new_priority != priority || new_flow_lbl != flow_lbl)) {
+        bpf_skb_store_bytes(skb, l2_header_size, &new_priority, sizeof(new_priority), 0);
+        bpf_skb_store_bytes(skb, l2_header_size + 1, &new_flow_lbl, sizeof(new_flow_lbl), 0);
+    }
+    return;
+}
 
-        // TODO: Add code to process IPv6 packet.
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_ether, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+    if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, true);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, true);
+    }
+
+    // Always return TC_ACT_PIPE
+    return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_raw_ip", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_raw_ip, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, false);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, false);
     }
 
     // Always return TC_ACT_PIPE
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscp_policy.h
new file mode 100644
index 0000000..1637f7a
--- /dev/null
+++ b/bpf_progs/dscp_policy.h
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define SRC_IP_MASK_FLAG     1
+#define DST_IP_MASK_FLAG     2
+#define SRC_PORT_MASK_FLAG   4
+#define DST_PORT_MASK_FLAG   8
+#define PROTO_MASK_FLAG      16
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+#define v6_equal(a, b) \
+    (((a.s6_addr32[0] ^ b.s6_addr32[0]) | \
+      (a.s6_addr32[1] ^ b.s6_addr32[1]) | \
+      (a.s6_addr32[2] ^ b.s6_addr32[2]) | \
+      (a.s6_addr32[3] ^ b.s6_addr32[3])) == 0)
+
+// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
+// smove to common location in future.
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_skb_ecn_set_ce;
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    uint32_t ifindex;
+    __be16 srcPort;
+    __be16 dstPortStart;
+    __be16 dstPortEnd;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t presentFields;
+    uint8_t pad[3];
+} DscpPolicy;
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3);  // 48
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    __u32 ifindex;
+    __be16 srcPort;
+    __be16 dstPort;
+    __u8 proto;
+    __u8 dscpVal;
+    __u8 pad[2];
+} RuleEntry;
+STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2);  // 44
\ No newline at end of file
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index fe9a871..44f76de 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// The resulting .o needs to load on the Android T Beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include <bpf_helpers.h>
 #include <linux/bpf.h>
 #include <linux/if.h>
@@ -25,7 +28,6 @@
 #include <linux/ipv6.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
-#include <netdutils/UidConstants.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include "bpf_net_helpers.h"
@@ -49,28 +51,57 @@
 #define TCP_FLAG_OFF 13
 #define RST_OFFSET 2
 
-DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+// For maps netd does not need to access
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
+
+// For maps netd only needs read only access to
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
+
+// For maps netd needs to be able to read and write
+#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0660)
+
+// Bpf map arrays on creation are preinitialized to 0 and do not support deletion of a key,
+// see: kernel/bpf/arraymap.c array_map_delete_elem() returns -EINVAL (from both syscall and ebpf)
+// Additionally on newer kernels the bpf jit can optimize out the lookups.
+// only valid indexes are [0..CONFIGURATION_MAP_SIZE-1]
+DEFINE_BPF_MAP_RO_NETD(configuration_map, ARRAY, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE)
+
+DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
 
 /* never actually used from ebpf */
-DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
-                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
+
+// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
+#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+
+// programs that need to be usable by netd, but not by netutils_wrappers
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_netd_readonly", "")
+
+// programs that only need to be usable by the system server
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_net_shared", "")
 
 static __always_inline int is_system_uid(uint32_t uid) {
-    return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+    // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+    // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+    return (uid < AID_APP_START);
 }
 
 /*
@@ -186,6 +217,11 @@
     return *config;
 }
 
+// DROP_IF_SET is set of rules that BPF_DROP if rule is globally enabled, and per-uid bit is set
+#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
+// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+
 static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) {
     if (skip_owner_match(skb)) return BPF_PASS;
 
@@ -194,29 +230,26 @@
     BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
 
     UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
-    uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+    uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
 
-    if (enabledRules) {
-        if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & LOW_POWER_STANDBY_MATCH) && !(uidRules & LOW_POWER_STANDBY_MATCH)) {
-            return BPF_DROP;
-        }
-    }
-    if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
-        // Drops packets not coming from lo nor the allowlisted interface
-        if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+    // Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
+    // check whether the rules are globally enabled, and if so whether the rules are
+    // set/unset for the specific uid.  BPF_DROP if that is the case for ANY of the rules.
+    // We achieve this by masking out only the bits/rules we're interested in checking,
+    // and negating (via bit-wise xor) the bits/rules that should drop if unset.
+    if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return BPF_DROP;
+
+    if (direction == BPF_INGRESS && skb->ifindex != 1) {
+        if (uidRules & IIF_MATCH) {
+            if (allowed_iif && skb->ifindex != allowed_iif) {
+                // Drops packets not coming from lo nor the allowed interface
+                // allowed interface=0 is a wildcard and does not drop packets
+                return BPF_DROP_UNLESS_DNS;
+            }
+        } else if (uidRules & LOCKDOWN_VPN_MATCH) {
+            // Drops packets not coming from lo and rule does not have IIF_MATCH but has
+            // LOCKDOWN_VPN_MATCH
             return BPF_DROP_UNLESS_DNS;
         }
     }
@@ -224,7 +257,7 @@
 }
 
 static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
-                                                            StatsKey* key, uint8_t selectedMap) {
+                                                            StatsKey* key, uint32_t selectedMap) {
     if (selectedMap == SELECT_MAP_A) {
         update_stats_map_A(skb, direction, key);
     } else if (selectedMap == SELECT_MAP_B) {
@@ -276,7 +309,7 @@
     if (counterSet) key.counterSet = (uint32_t)*counterSet;
 
     uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
-    uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+    uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
 
     // Use asm("%0 &= 1" : "+r"(match)) before return match,
     // to help kernel's bpf verifier, so that it can be 100% certain
@@ -297,17 +330,18 @@
     return match;
 }
 
-DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
+DEFINE_NETD_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_INGRESS);
 }
 
-DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
+DEFINE_NETD_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_EGRESS);
 }
 
-DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
     // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -326,7 +360,8 @@
     return BPF_MATCH;
 }
 
-DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
     // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -338,7 +373,8 @@
     return BPF_MATCH;
 }
 
-DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+                    tc_bpf_ingress_account_prog)
 (struct __sk_buff* skb) {
     if (is_received_skb(skb)) {
         // Account for ingress traffic before tc drops it.
@@ -348,7 +384,8 @@
     return TC_ACT_UNSPEC;
 }
 
-DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     if (is_system_uid(sock_uid)) return BPF_MATCH;
@@ -365,7 +402,8 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -373,8 +411,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
-                     KVER(4, 14, 0))
+DEFINE_NETD_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
 (struct bpf_sock* sk) {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
@@ -383,7 +420,7 @@
      * user at install time so we only check the appId part of a request uid at
      * run time. See UserHandle#isSameApp for detail.
      */
-    uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+    uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET;  // == PER_USER_RANGE == 100000
     uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
     if (!permissions) {
         // UID not in map. Default to just INTERNET permission.
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 92a774c..2ec0792 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,8 +24,8 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -355,88 +355,10 @@
 
 DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const bool updatetime) {
-    // Require ethernet dst mac address to be our unicast address.
-    if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
-
-    // Must be meta-ethernet IPv4 frame
-    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
-    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-
-    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
-    // not trigger and thus we need to manually make sure we can read packet headers via DPA.
-    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
-    // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
-
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
-    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
-
-    // Must have (ethernet and) ipv4 header
-    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
-
-    // Ethertype - if present - must be IPv4
-    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
-
-    // IP version must be 4
-    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
-
-    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
-
-    // Calculate the IPv4 one's complement checksum of the IPv4 header.
-    __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
-        sum4 += ((__u16*)ip)[i];
-    }
-    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
-    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
-    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
-
-    // Minimum IPv4 total length is the size of the header
-    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
-
-    // We are incapable of dealing with IPv4 fragments
-    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
-
-    // Cannot decrement during forward if already zero or would be zero,
-    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
-    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
-
-    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
-    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
-    // in such a situation we can only support TCP.  This also has the added nice benefit of
-    // using a separate error counter, and thus making it obvious which version of the program
-    // is loaded.
-    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
-
-    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
-    // but no need to check this if !updatetime due to check immediately above.
-    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
-        TC_PUNT(NON_TCP_UDP);
-
-    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
-    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
-    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
-
-    // This is a bit of a hack to make things easier on the bpf verifier.
-    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
-    // what offsets into the packet are valid and can spuriously reject the program, this is
-    // because it fails to realize that is_tcp && !is_tcp is impossible)
-    //
-    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
-    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
-    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
-    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
-    // As such we *always* need access to at least 8 bytes.
-    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
-
+static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
+        const int l2_header_size, void* data, const void* data_end,
+        struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
+        const bool downstream, const bool updatetime, const bool is_tcp) {
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
 
@@ -625,6 +547,102 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+        const bool downstream, const bool updatetime) {
+    // Require ethernet dst mac address to be our unicast address.
+    if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
+
+    // Must be meta-ethernet IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+    // not trigger and thus we need to manually make sure we can read packet headers via DPA.
+    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
+    // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
+    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+    // Must have (ethernet and) ipv4 header
+    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
+
+    // Ethertype - if present - must be IPv4
+    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
+
+    // IP version must be 4
+    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+    // Calculate the IPv4 one's complement checksum of the IPv4 header.
+    __wsum sum4 = 0;
+    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+        sum4 += ((__u16*)ip)[i];
+    }
+    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
+    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+    // Minimum IPv4 total length is the size of the header
+    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+    // We are incapable of dealing with IPv4 fragments
+    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+    // Cannot decrement during forward if already zero or would be zero,
+    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+    // in such a situation we can only support TCP.  This also has the added nice benefit of
+    // using a separate error counter, and thus making it obvious which version of the program
+    // is loaded.
+    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+    // but no need to check this if !updatetime due to check immediately above.
+    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+        TC_PUNT(NON_TCP_UDP);
+
+    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
+    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+    // This is a bit of a hack to make things easier on the bpf verifier.
+    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+    // what offsets into the packet are valid and can spuriously reject the program, this is
+    // because it fails to realize that is_tcp && !is_tcp is impossible)
+    //
+    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
+    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
+    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+    // As such we *always* need access to at least 8 bytes.
+    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+    // We're forcing the compiler to emit two copies of the following code, optimized
+    // separately for is_tcp being true or false.  This simplifies the resulting bpf
+    // byte code sufficiently that the 4.14 bpf verifier is able to keep track of things.
+    // Without this (updatetime == true) case would fail to bpf verify on 4.14 even
+    // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
+    if (is_tcp) {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ true);
+    } else {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ false);
+    }
+}
+
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index c9c73f1..f2fcc8c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,8 +18,8 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
diff --git a/common/Android.bp b/common/Android.bp
new file mode 100644
index 0000000..729ef32
--- /dev/null
+++ b/common/Android.bp
@@ -0,0 +1,45 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "connectivity-net-module-utils-bpf",
+    srcs: [
+        "src/com/android/net/module/util/bpf/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "29",
+    visibility: [
+        // Do not add any lib. This library is only shared inside connectivity module
+        // and its tests.
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
+    ],
+    static_libs: [
+        "net-utils-device-common-struct",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+    lint: { strict_updatability_linting: true },
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
new file mode 100644
index 0000000..12200ec
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
@@ -0,0 +1,37 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Key type for clat egress IPv4 maps. */
+public class ClatEgress4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif; // The input interface index
+
+    @Field(order = 1, type = Type.Ipv4Address)
+    public final Inet4Address local4; // The source IPv4 address
+
+    public ClatEgress4Key(final long iif, final Inet4Address local4) {
+        this.iif = iif;
+        this.local4 = local4;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
new file mode 100644
index 0000000..c10cb4d
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
@@ -0,0 +1,46 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Value type for clat egress IPv4 maps. */
+public class ClatEgress4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif; // The output interface to redirect to
+
+    @Field(order = 1, type = Type.Ipv6Address)
+    public final Inet6Address local6; // The full 128-bits of the source IPv6 address
+
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address pfx96; // The destination /96 nat64 prefix, bottom 32 bits must be 0
+
+    @Field(order = 3, type = Type.U8, padding = 3)
+    public final short oifIsEthernet; // Whether the output interface requires ethernet header
+
+    public ClatEgress4Value(final long oif, final Inet6Address local6, final Inet6Address pfx96,
+            final short oifIsEthernet) {
+        this.oif = oif;
+        this.local6 = local6;
+        this.pfx96 = pfx96;
+        this.oifIsEthernet = oifIsEthernet;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
new file mode 100644
index 0000000..1e2f4e0
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Key type for clat ingress IPv6 maps. */
+public class ClatIngress6Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif; // The input interface index
+
+    @Field(order = 1, type = Type.Ipv6Address)
+    public final Inet6Address pfx96; // The source /96 nat64 prefix, bottom 32 bits must be 0
+
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address local6; // The full 128-bits of the destination IPv6 address
+
+    public ClatIngress6Key(final long iif, final Inet6Address pfx96, final Inet6Address local6) {
+        this.iif = iif;
+        this.pfx96 = pfx96;
+        this.local6 = local6;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
new file mode 100644
index 0000000..bfec44f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
@@ -0,0 +1,37 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Value type for clat ingress IPv6 maps. */
+public class ClatIngress6Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif; // The output interface to redirect to (0 means don't redirect)
+
+    @Field(order = 1, type = Type.Ipv4Address)
+    public final Inet4Address local4; // The destination IPv4 address
+
+    public ClatIngress6Value(final long oif, final Inet4Address local4) {
+        this.oif = oif;
+        this.local4 = local4;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Key.java b/common/src/com/android/net/module/util/bpf/Tether4Key.java
new file mode 100644
index 0000000..638576f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress dstMac;
+
+    @Field(order = 2, type = Type.U8, padding = 1)
+    public final short l4proto;
+
+    @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 6, type = Type.UBE16)
+    public final int dstPort;
+
+    public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort) {
+        Objects.requireNonNull(dstMac);
+
+        this.iif = iif;
+        this.dstMac = dstMac;
+        this.l4proto = l4proto;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+                            + "srcPort: %d, dstPort: %d",
+                    iif, dstMac, l4proto,
+                    Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Value.java b/common/src/com/android/net/module/util/bpf/Tether4Value.java
new file mode 100644
index 0000000..de98766
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif;
+
+    // The ethhdr struct which is defined in uapi/linux/if_ether.h
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress ethDstMac;
+    @Field(order = 2, type = Type.EUI48)
+    public final MacAddress ethSrcMac;
+    @Field(order = 3, type = Type.UBE16)
+    public final int ethProto;  // Packet type ID field.
+
+    @Field(order = 4, type = Type.U16)
+    public final int pmtu;
+
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 7, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 8, type = Type.UBE16)
+    public final int dstPort;
+
+    // TODO: consider using U64.
+    @Field(order = 9, type = Type.U63)
+    public final long lastUsed;
+
+    public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+            @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+            final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPort, final long lastUsed) {
+        Objects.requireNonNull(ethDstMac);
+        Objects.requireNonNull(ethSrcMac);
+
+        this.oif = oif;
+        this.ethDstMac = ethDstMac;
+        this.ethSrcMac = ethSrcMac;
+        this.ethProto = ethProto;
+        this.pmtu = pmtu;
+        this.src46 = src46;
+        this.dst46 = dst46;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.lastUsed = lastUsed;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+                            + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+                            + "lastUsed: %d",
+                    oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+                    InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+                    lastUsed);
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
similarity index 96%
rename from Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
rename to common/src/com/android/net/module/util/bpf/TetherStatsKey.java
index 5442480..c6d595b 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.tethering;
+package com.android.net.module.util.bpf;
 
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.Field;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
similarity index 98%
rename from Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
rename to common/src/com/android/net/module/util/bpf/TetherStatsValue.java
index 844d2e8..028d217 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.tethering;
+package com.android.net.module.util.bpf;
 
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.Field;
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 1b47481..eb77288 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -3,7 +3,7 @@
 
   public final class NetworkStats implements java.lang.AutoCloseable {
     method public void close();
-    method public boolean getNextBucket(android.app.usage.NetworkStats.Bucket);
+    method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
     method public boolean hasNextBucket();
   }
 
@@ -40,21 +40,21 @@
   }
 
   public class NetworkStatsManager {
-    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, String, long, long, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, String, long, long, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, String, long, long, int, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback);
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
-    method public void unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback);
+    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, @Nullable String, long, long, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, @Nullable String, long, long, int, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, @Nullable String, long, long, int, int, int) throws java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
+    method public void unregisterUsageCallback(@NonNull android.app.usage.NetworkStatsManager.UsageCallback);
   }
 
   public abstract static class NetworkStatsManager.UsageCallback {
     ctor public NetworkStatsManager.UsageCallback();
-    method public abstract void onThresholdReached(int, String);
+    method public abstract void onThresholdReached(int, @Nullable String);
   }
 
 }
@@ -173,12 +173,12 @@
     method public static void incrementOperationCount(int, int);
     method public static void setThreadStatsTag(int);
     method public static void setThreadStatsUid(int);
-    method public static void tagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void tagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void tagSocket(java.net.Socket) throws java.net.SocketException;
-    method public static void untagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void untagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void untagSocket(java.net.Socket) throws java.net.SocketException;
+    method public static void tagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void tagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void tagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
+    method public static void untagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void untagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void untagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
     field public static final int UNSUPPORTED = -1; // 0xffffffff
   }
 
diff --git a/framework-t/api/lint-baseline.txt b/framework-t/api/lint-baseline.txt
index 53e1beb..2996a3e 100644
--- a/framework-t/api/lint-baseline.txt
+++ b/framework-t/api/lint-baseline.txt
@@ -41,86 +41,18 @@
     android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
 
 
-MissingNullability: android.app.usage.NetworkStats#getNextBucket(android.app.usage.NetworkStats.Bucket) parameter #0:
-    Missing nullability on parameter `bucketOut` in method `getNextBucket`
 MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
     Missing nullability on method `queryDetails` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetails`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
-    Missing nullability on method `queryDetailsForUid` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUid`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
-    Missing nullability on method `queryDetailsForUidTag` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTag`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
-    Missing nullability on method `queryDetailsForUidTagState` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTagState`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
     Missing nullability on method `querySummary` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummary`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
     Missing nullability on method `querySummaryForDevice` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForDevice`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
     Missing nullability on method `querySummaryForUser` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForUser`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback) parameter #0:
-    Missing nullability on parameter `callback` in method `unregisterUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager.UsageCallback#onThresholdReached(int, String) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `onThresholdReached`
 MissingNullability: android.net.IpSecAlgorithm#writeToParcel(android.os.Parcel, int) parameter #0:
     Missing nullability on parameter `out` in method `writeToParcel`
 MissingNullability: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
     Missing nullability on method `getFileDescriptor` return
-MissingNullability: android.net.TrafficStats#tagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagDatagramSocket`
-MissingNullability: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tagFileDescriptor`
-MissingNullability: android.net.TrafficStats#tagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagSocket`
-MissingNullability: android.net.TrafficStats#untagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagDatagramSocket`
-MissingNullability: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untagFileDescriptor`
-MissingNullability: android.net.TrafficStats#untagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagSocket`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #0:
-    Missing nullability on parameter `basePath` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #1:
-    Missing nullability on parameter `prefix` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#dumpAll(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `os` in method `dumpAll`
-MissingNullability: com.android.internal.util.FileRotator#readMatching(com.android.internal.util.FileRotator.Reader, long, long) parameter #0:
-    Missing nullability on parameter `reader` in method `readMatching`
-MissingNullability: com.android.internal.util.FileRotator#rewriteActive(com.android.internal.util.FileRotator.Rewriter, long) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteActive`
-MissingNullability: com.android.internal.util.FileRotator#rewriteAll(com.android.internal.util.FileRotator.Rewriter) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteAll`
-MissingNullability: com.android.internal.util.FileRotator.Reader#read(java.io.InputStream) parameter #0:
-    Missing nullability on parameter `in` in method `read`
-MissingNullability: com.android.internal.util.FileRotator.Writer#write(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `out` in method `write`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#kernelToTag(String) parameter #0:
-    Missing nullability on parameter `string` in method `kernelToTag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untag`
 
 
 RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 658c625..c1f7b39 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -4,6 +4,8 @@
   public class NetworkStatsManager {
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void forceUpdate();
     method public static int getCollapsedRatType(int);
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void noteUidForeground(int, boolean);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void notifyNetworkStatus(@NonNull java.util.List<android.net.Network>, @NonNull java.util.List<android.net.NetworkStateSnapshot>, @Nullable String, @NonNull java.util.List<android.net.UnderlyingNetworkInfo>);
     method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForDevice(@NonNull android.net.NetworkTemplate, long, long);
@@ -182,6 +184,7 @@
   public class TrafficStats {
     method public static void attachSocketTagger();
     method public static void init(@NonNull android.content.Context);
+    method public static void setThreadStatsTagDownload();
   }
 
   public final class UnderlyingNetworkInfo implements android.os.Parcelable {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 0f37b6f..6460fed 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -2,10 +2,8 @@
 package android.app.usage {
 
   public class NetworkStatsManager {
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
   }
 
 }
@@ -113,7 +111,6 @@
   public class TrafficStats {
     method public static void setThreadStatsTagApp();
     method public static void setThreadStatsTagBackup();
-    method public static void setThreadStatsTagDownload();
     method public static void setThreadStatsTagRestore();
     field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = -113; // 0xffffff8f
     field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = -128; // 0xffffff80
diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java
index 2b6570a..74fe4bd 100644
--- a/framework-t/src/android/app/usage/NetworkStats.java
+++ b/framework-t/src/android/app/usage/NetworkStats.java
@@ -17,6 +17,7 @@
 package android.app.usage;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.INetworkStatsService;
 import android.net.INetworkStatsSession;
@@ -474,10 +475,11 @@
 
     /**
      * Fills the recycled bucket with data of the next bin in the enumeration.
-     * @param bucketOut Bucket to be filled with data.
+     * @param bucketOut Bucket to be filled with data. If null, the method does
+     *                  nothing and returning false.
      * @return true if successfully filled the bucket, false otherwise.
      */
-    public boolean getNextBucket(Bucket bucketOut) {
+    public boolean getNextBucket(@Nullable Bucket bucketOut) {
         if (mSummary != null) {
             return getNextSummaryBucket(bucketOut);
         } else {
@@ -651,7 +653,7 @@
      * @param bucketOut Next item will be set here.
      * @return true if a next item could be set.
      */
-    private boolean getNextSummaryBucket(Bucket bucketOut) {
+    private boolean getNextSummaryBucket(@Nullable Bucket bucketOut) {
         if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
             mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
             fillBucketFromSummaryEntry(bucketOut);
@@ -678,7 +680,7 @@
      * @param bucketOut Next item will be set here.
      * @return true if a next item could be set.
      */
-    private boolean getNextHistoryBucket(Bucket bucketOut) {
+    private boolean getNextHistoryBucket(@Nullable Bucket bucketOut) {
         if (bucketOut != null && mHistory != null) {
             if (mEnumerationIndex < mHistory.size()) {
                 mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index bf518b2..f41475b 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -290,7 +290,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public Bucket querySummaryForDevice(int networkType, String subscriberId,
+    public Bucket querySummaryForDevice(int networkType, @Nullable String subscriberId,
             long startTime, long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -335,8 +335,8 @@
      *         statistics collection.
      */
     @WorkerThread
-    public Bucket querySummaryForUser(int networkType, String subscriberId, long startTime,
-            long endTime) throws SecurityException, RemoteException {
+    public Bucket querySummaryForUser(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
             template = createTemplate(networkType, subscriberId);
@@ -384,7 +384,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public NetworkStats querySummary(int networkType, String subscriberId, long startTime,
+    public NetworkStats querySummary(int networkType, @Nullable String subscriberId, long startTime,
             long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -508,15 +508,17 @@
      *
      * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUid(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUid(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid) throws SecurityException {
         return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
             NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
     }
 
     /** @hide */
-    public NetworkStats queryDetailsForUid(NetworkTemplate template,
+    @NonNull
+    public NetworkStats queryDetailsForUid(@NonNull NetworkTemplate template,
             long startTime, long endTime, int uid) throws SecurityException {
         return queryDetailsForUidTagState(template, startTime, endTime, uid,
                 NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
@@ -524,23 +526,59 @@
 
     /**
      * Query network usage statistics details for a given uid and tag.
+     *
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+     * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+     * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
      * This may take a long time, and apps should avoid calling this on their main thread.
      *
-     * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+     *            across all the tags.
+     * @return Statistics which is described above.
+     * @throws SecurityException if permissions are insufficient to read network statistics.
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUidTag(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid, int tag) throws SecurityException {
         return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
             tag, NetworkStats.Bucket.STATE_ALL);
     }
 
     /**
-     * Query network usage statistics details for a given uid, tag, and state. Only usable for uids
-     * belonging to calling user. Result is not aggregated over time. This means buckets' start and
-     * end timestamps are going to be between 'startTime' and 'endTime' parameters. The uid is going
-     * to be the same as the 'uid' parameter, the tag the same as the 'tag' parameter, and the state
-     * the same as the 'state' parameter.
+     * Query network usage statistics details for a given uid, tag, and state.
+     *
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
      * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
      * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
      * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
@@ -572,11 +610,12 @@
      *            across all the tags.
      * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
      *            traffic from all states.
-     * @return Statistics object or null if an error happened during statistics collection.
+     * @return Statistics which is described above.
      * @throws SecurityException if permissions are insufficient to read network statistics.
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUidTagState(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
         NetworkTemplate template;
         template = createTemplate(networkType, subscriberId);
@@ -669,7 +708,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public NetworkStats queryDetails(int networkType, String subscriberId, long startTime,
+    public NetworkStats queryDetails(int networkType, @Nullable String subscriberId, long startTime,
             long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -698,7 +737,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK})
@@ -724,7 +763,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK})
@@ -785,10 +824,28 @@
     /**
      * Registers to receive notifications about data usage on specified networks.
      *
-     * @see #registerUsageCallback(int, String, long, UsageCallback, Handler)
+     * <p>The callbacks will continue to be called as long as the process is live or
+     * {@link #unregisterUsageCallback} is called.
+     *
+     * @param networkType Type of network to monitor. Either
+    {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when registering for the mobile network type to receive
+     *                     notifications for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param thresholdBytes Threshold in bytes to be notified on.
+     * @param callback The {@link UsageCallback} that the system will call when data usage
+     *            has exceeded the specified threshold.
      */
-    public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
-            UsageCallback callback) {
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback) {
         registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
                 null /* handler */);
     }
@@ -818,8 +875,8 @@
      * @param handler to dispatch callback events through, otherwise if {@code null} it uses
      *            the calling thread.
      */
-    public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
-            UsageCallback callback, @Nullable Handler handler) {
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback, @Nullable Handler handler) {
         NetworkTemplate template = createTemplate(networkType, subscriberId);
         if (DBG) {
             Log.d(TAG, "registerUsageCallback called with: {"
@@ -839,7 +896,7 @@
      *
      * @param callback The {@link UsageCallback} used when registering.
      */
-    public void unregisterUsageCallback(UsageCallback callback) {
+    public void unregisterUsageCallback(@NonNull UsageCallback callback) {
         if (callback == null || callback.request == null
                 || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
             throw new IllegalArgumentException("Invalid UsageCallback");
@@ -880,7 +937,7 @@
         /**
          * Called when data usage has reached the given threshold.
          */
-        public abstract void onThresholdReached(int networkType, String subscriberId);
+        public abstract void onThresholdReached(int networkType, @Nullable String subscriberId);
 
         /**
          * @hide used for internal bookkeeping
@@ -924,7 +981,7 @@
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_STATS_PROVIDER,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
-    @NonNull public void registerNetworkStatsProvider(
+    public void registerNetworkStatsProvider(
             @NonNull String tag,
             @NonNull NetworkStatsProvider provider) {
         try {
@@ -950,7 +1007,7 @@
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_STATS_PROVIDER,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
-    @NonNull public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
+    public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
         try {
             provider.getProviderCallbackBinderOrThrow().unregister();
         } catch (RemoteException e) {
@@ -958,7 +1015,7 @@
         }
     }
 
-    private static NetworkTemplate createTemplate(int networkType, String subscriberId) {
+    private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) {
         final NetworkTemplate template;
         switch (networkType) {
             case ConnectivityManager.TYPE_MOBILE:
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index 2b76dd9..b8070f0 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -22,23 +22,21 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
+import android.util.ArrayMap;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.BackgroundThread;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -56,37 +54,12 @@
 
     private final IEthernetManager mService;
     @GuardedBy("mListenerLock")
-    private final ArrayList<ListenerInfo<InterfaceStateListener>> mIfaceListeners =
-            new ArrayList<>();
+    private final ArrayMap<InterfaceStateListener, IEthernetServiceListener>
+            mIfaceServiceListeners = new ArrayMap<>();
     @GuardedBy("mListenerLock")
-    private final ArrayList<ListenerInfo<IntConsumer>> mEthernetStateListeners =
-            new ArrayList<>();
+    private final ArrayMap<IntConsumer, IEthernetServiceListener> mStateServiceListeners =
+            new ArrayMap<>();
     final Object mListenerLock = new Object();
-    private final IEthernetServiceListener.Stub mServiceListener =
-            new IEthernetServiceListener.Stub() {
-                @Override
-                public void onEthernetStateChanged(int state) {
-                    synchronized (mListenerLock) {
-                        for (ListenerInfo<IntConsumer> li : mEthernetStateListeners) {
-                            li.executor.execute(() -> {
-                                li.listener.accept(state);
-                            });
-                        }
-                    }
-                }
-
-                @Override
-                public void onInterfaceStateChanged(String iface, int state, int role,
-                        IpConfiguration configuration) {
-                    synchronized (mListenerLock) {
-                        for (ListenerInfo<InterfaceStateListener> li : mIfaceListeners) {
-                            li.executor.execute(() ->
-                                    li.listener.onInterfaceStateChanged(iface, state, role,
-                                            configuration));
-                        }
-                    }
-                }
-            };
 
     /**
      * Indicates that Ethernet is disabled.
@@ -104,18 +77,6 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int ETHERNET_STATE_ENABLED  = 1;
 
-    private static class ListenerInfo<T> {
-        @NonNull
-        public final Executor executor;
-        @NonNull
-        public final T listener;
-
-        private ListenerInfo(@NonNull Executor executor, @NonNull T listener) {
-            this.executor = executor;
-            this.listener = listener;
-        }
-    }
-
     /**
      * The interface is absent.
      * @hide
@@ -323,18 +284,28 @@
         if (listener == null || executor == null) {
             throw new NullPointerException("listener and executor must not be null");
         }
+
+        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+            @Override
+            public void onEthernetStateChanged(int state) {}
+
+            @Override
+            public void onInterfaceStateChanged(String iface, int state, int role,
+                    IpConfiguration configuration) {
+                executor.execute(() ->
+                        listener.onInterfaceStateChanged(iface, state, role, configuration));
+            }
+        };
         synchronized (mListenerLock) {
-            maybeAddServiceListener();
-            mIfaceListeners.add(new ListenerInfo<InterfaceStateListener>(executor, listener));
+            addServiceListener(serviceListener);
+            mIfaceServiceListeners.put(listener, serviceListener);
         }
     }
 
     @GuardedBy("mListenerLock")
-    private void maybeAddServiceListener() {
-        if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
-
+    private void addServiceListener(@NonNull final IEthernetServiceListener listener) {
         try {
-            mService.addListener(mServiceListener);
+            mService.addListener(listener);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -364,17 +335,16 @@
     public void removeInterfaceStateListener(@NonNull InterfaceStateListener listener) {
         Objects.requireNonNull(listener);
         synchronized (mListenerLock) {
-            mIfaceListeners.removeIf(l -> l.listener == listener);
-            maybeRemoveServiceListener();
+            maybeRemoveServiceListener(mIfaceServiceListeners.remove(listener));
         }
     }
 
     @GuardedBy("mListenerLock")
-    private void maybeRemoveServiceListener() {
-        if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
+    private void maybeRemoveServiceListener(@Nullable final IEthernetServiceListener listener) {
+        if (listener == null) return;
 
         try {
-            mService.removeListener(mServiceListener);
+            mService.removeListener(listener);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -601,7 +571,6 @@
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
-    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
     public void enableInterface(
             @NonNull String iface,
             @Nullable @CallbackExecutor Executor executor,
@@ -610,7 +579,7 @@
         final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                 executor, callback);
         try {
-            mService.connectNetwork(iface, proxy);
+            mService.enableInterface(iface, proxy);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -638,7 +607,6 @@
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
-    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
     public void disableInterface(
             @NonNull String iface,
             @Nullable @CallbackExecutor Executor executor,
@@ -647,7 +615,7 @@
         final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                 executor, callback);
         try {
-            mService.disconnectNetwork(iface, proxy);
+            mService.disableInterface(iface, proxy);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -687,9 +655,19 @@
             @NonNull IntConsumer listener) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(listener);
+        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+            @Override
+            public void onEthernetStateChanged(int state) {
+                executor.execute(() -> listener.accept(state));
+            }
+
+            @Override
+            public void onInterfaceStateChanged(String iface, int state, int role,
+                    IpConfiguration configuration) {}
+        };
         synchronized (mListenerLock) {
-            maybeAddServiceListener();
-            mEthernetStateListeners.add(new ListenerInfo<IntConsumer>(executor, listener));
+            addServiceListener(serviceListener);
+            mStateServiceListeners.put(listener, serviceListener);
         }
     }
 
@@ -705,8 +683,7 @@
     public void removeEthernetStateListener(@NonNull IntConsumer listener) {
         Objects.requireNonNull(listener);
         synchronized (mListenerLock) {
-            mEthernetStateListeners.removeIf(l -> l.listener == listener);
-            maybeRemoveServiceListener();
+            maybeRemoveServiceListener(mStateServiceListeners.remove(listener));
         }
     }
 
diff --git a/framework-t/src/android/net/IEthernetManager.aidl b/framework-t/src/android/net/IEthernetManager.aidl
index 42e4c1a..c1efc29 100644
--- a/framework-t/src/android/net/IEthernetManager.aidl
+++ b/framework-t/src/android/net/IEthernetManager.aidl
@@ -43,8 +43,8 @@
     void releaseTetheredInterface(in ITetheredInterfaceCallback callback);
     void updateConfiguration(String iface, in EthernetNetworkUpdateRequest request,
         in INetworkInterfaceOutcomeReceiver listener);
-    void connectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
-    void disconnectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void enableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void disableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
     void setEthernetEnabled(boolean enabled);
     List<String> getInterfaceList();
 }
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 9cb0947..9cceac2 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -817,10 +817,10 @@
          * </ol>
          *
          * @param underlyingNetwork the new {@link Network} that will carry traffic for this tunnel.
-         *     This network MUST never be the network exposing this IpSecTunnelInterface, otherwise
-         *     this method will throw an {@link IllegalArgumentException}. If the
-         *     IpSecTunnelInterface is later added to this network, all outbound traffic will be
-         *     blackholed.
+         *     This network MUST be a functional {@link Network} with valid {@link LinkProperties},
+         *     and MUST never be the network exposing this IpSecTunnelInterface, otherwise this
+         *     method will throw an {@link IllegalArgumentException}. If the IpSecTunnelInterface is
+         *     later added to this network, all outbound traffic will be blackholed.
          */
         // TODO: b/169171001 Update the documentation when transform migration is supported.
         // The purpose of making updating network and applying transforms separate is to leave open
@@ -962,7 +962,6 @@
      * IP header and IPsec Header on all inbound traffic).
      * <p>Applications should probably not use this API directly.
      *
-     *
      * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
      *        transform.
      * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
diff --git a/framework-t/src/android/net/NetworkIdentitySet.java b/framework-t/src/android/net/NetworkIdentitySet.java
index ad3a958..d88408e 100644
--- a/framework-t/src/android/net/NetworkIdentitySet.java
+++ b/framework-t/src/android/net/NetworkIdentitySet.java
@@ -206,6 +206,7 @@
     public static int compare(@NonNull NetworkIdentitySet left, @NonNull NetworkIdentitySet right) {
         Objects.requireNonNull(left);
         Objects.requireNonNull(right);
+        if (left.isEmpty() && right.isEmpty()) return 0;
         if (left.isEmpty()) return -1;
         if (right.isEmpty()) return 1;
 
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 06f2a62..0bb98f8 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -124,7 +124,6 @@
     public @Nullable static final String[] INTERFACES_ALL = null;
 
     /** {@link #tag} value for total data across all tags. */
-    // TODO: Rename TAG_NONE to TAG_ALL.
     public static final int TAG_NONE = 0;
 
     /** {@link #metered} value to account for all metered states. */
@@ -412,21 +411,24 @@
         /**
          * @return the metered state.
          */
-        @Meteredness public int getMetered() {
+        @Meteredness
+        public int getMetered() {
             return metered;
         }
 
         /**
          * @return the roaming state.
          */
-        @Roaming public int getRoaming() {
+        @Roaming
+        public int getRoaming() {
             return roaming;
         }
 
         /**
          * @return the default network state.
          */
-        @DefaultNetwork public int getDefaultNetwork() {
+        @DefaultNetwork
+        public int getDefaultNetwork() {
             return defaultNetwork;
         }
 
@@ -1298,6 +1300,17 @@
     }
 
     /**
+     * Removes the interface name from all entries.
+     * This mutates the original structure in place.
+     * @hide
+     */
+    public void clearInterfaces() {
+        for (int i = 0; i < size; i++) {
+            iface[i] = null;
+        }
+    }
+
+    /**
      * Only keep entries that match all specified filters.
      *
      * <p>This mutates the original structure in place. After this method is called,
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index e385b33..6a1d2dd 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -694,6 +694,26 @@
         }
     }
 
+    /**
+     * Remove histories which contains or is before the cutoff timestamp.
+     * @hide
+     */
+    public void removeHistoryBefore(long cutoffMillis) {
+        final ArrayList<Key> knownKeys = new ArrayList<>();
+        knownKeys.addAll(mStats.keySet());
+
+        for (Key key : knownKeys) {
+            final NetworkStatsHistory history = mStats.get(key);
+            if (history.getStart() > cutoffMillis) continue;
+
+            history.removeBucketsStartingBefore(cutoffMillis);
+            if (history.size() == 0) {
+                mStats.remove(key);
+            }
+            mDirty = true;
+        }
+    }
+
     private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
         if (startMillis < mStartMillis) mStartMillis = startMillis;
         if (endMillis > mEndMillis) mEndMillis = endMillis;
@@ -776,7 +796,7 @@
             if (!templateMatches(groupTemplate, key.ident)) continue;
             if (key.set >= NetworkStats.SET_DEBUG_START) continue;
 
-            final Key groupKey = new Key(null, key.uid, key.set, key.tag);
+            final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag);
             NetworkStatsHistory groupHistory = grouped.get(groupKey);
             if (groupHistory == null) {
                 groupHistory = new NetworkStatsHistory(value.getBucketDuration());
@@ -845,6 +865,9 @@
          * Add association of the history with the specified key in this map.
          *
          * @param key The object used to identify a network, see {@link Key}.
+         *            If history already exists for this key, then the passed-in history is appended
+         *            to the previously-passed in history. The caller must ensure that the history
+         *            passed-in timestamps are greater than all previously-passed-in timestamps.
          * @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
          * @return The builder object.
          */
@@ -854,9 +877,21 @@
             Objects.requireNonNull(key);
             Objects.requireNonNull(history);
             final List<Entry> historyEntries = history.getEntries();
+            final NetworkStatsHistory existing = mEntries.get(key);
 
+            final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
             final NetworkStatsHistory.Builder historyBuilder =
-                    new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size());
+                    new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
+
+            // TODO: this simply appends the entries to any entries that were already present in
+            // the builder, which requires the caller to pass in entries in order. We might be
+            // able to do better with something like recordHistory.
+            if (existing != null) {
+                for (Entry entry : existing.getEntries()) {
+                    historyBuilder.addEntry(entry);
+                }
+            }
+
             for (Entry entry : historyEntries) {
                 historyBuilder.addEntry(entry);
             }
diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java
index 301fef9..738e9cc 100644
--- a/framework-t/src/android/net/NetworkStatsHistory.java
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -32,6 +32,7 @@
 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
@@ -57,6 +58,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
+import java.util.TreeMap;
 
 /**
  * Collection of historical network statistics, recorded into equally-sized
@@ -252,6 +254,28 @@
                     + ", operations=" + operations
                     + "}";
         }
+
+        /**
+         * Add the given {@link Entry} with this instance and return a new {@link Entry}
+         * instance as the result.
+         *
+         * @hide
+         */
+        @NonNull
+        public Entry plus(@NonNull Entry another, long bucketDuration) {
+            if (this.bucketStart != another.bucketStart) {
+                throw new IllegalArgumentException("bucketStart " + this.bucketStart
+                        + " is not equal to " + another.bucketStart);
+            }
+            return new Entry(this.bucketStart,
+                    // Active time should not go over bucket duration.
+                    Math.min(this.activeTime + another.activeTime, bucketDuration),
+                    this.rxBytes + another.rxBytes,
+                    this.rxPackets + another.rxPackets,
+                    this.txBytes + another.txBytes,
+                    this.txPackets + another.txPackets,
+                    this.operations + another.operations);
+        }
     }
 
     /** @hide */
@@ -680,19 +704,21 @@
     }
 
     /**
-     * Remove buckets older than requested cutoff.
+     * Remove buckets that start older than requested cutoff.
+     *
+     * This method will remove any bucket that contains any data older than the requested
+     * cutoff, even if that same bucket includes some data from after the cutoff.
+     *
      * @hide
      */
-    public void removeBucketsBefore(long cutoff) {
+    public void removeBucketsStartingBefore(final long cutoff) {
         // TODO: Consider use getIndexBefore.
         int i;
         for (i = 0; i < bucketCount; i++) {
             final long curStart = bucketStart[i];
-            final long curEnd = curStart + bucketDuration;
 
-            // cutoff happens before or during this bucket; everything before
-            // this bucket should be removed.
-            if (curEnd > cutoff) break;
+            // This bucket starts after or at the cutoff, so it should be kept.
+            if (curStart >= cutoff) break;
         }
 
         if (i > 0) {
@@ -947,6 +973,25 @@
         return writer.toString();
     }
 
+    /**
+     * Same as "equals", but not actually called equals as this would affect public API behavior.
+     * @hide
+     */
+    @Nullable
+    public boolean isSameAs(NetworkStatsHistory other) {
+        return bucketCount == other.bucketCount
+                && Arrays.equals(bucketStart, other.bucketStart)
+                // Don't check activeTime since it can change on import due to the importer using
+                // recordHistory. It's also not exposed by the APIs or present in dumpsys or
+                // toString().
+                && Arrays.equals(rxBytes, other.rxBytes)
+                && Arrays.equals(rxPackets, other.rxPackets)
+                && Arrays.equals(txBytes, other.txBytes)
+                && Arrays.equals(txPackets, other.txPackets)
+                && Arrays.equals(operations, other.operations)
+                && totalBytes == other.totalBytes;
+    }
+
     @UnsupportedAppUsage
     public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
         @Override
@@ -1087,14 +1132,8 @@
      * Builder class for {@link NetworkStatsHistory}.
      */
     public static final class Builder {
+        private final TreeMap<Long, Entry> mEntries;
         private final long mBucketDuration;
-        private final List<Long> mBucketStart;
-        private final List<Long> mActiveTime;
-        private final List<Long> mRxBytes;
-        private final List<Long> mRxPackets;
-        private final List<Long> mTxBytes;
-        private final List<Long> mTxPackets;
-        private final List<Long> mOperations;
 
         /**
          * Creates a new Builder with given bucket duration and initial capacity to construct
@@ -1105,36 +1144,31 @@
          */
         public Builder(long bucketDuration, int initialCapacity) {
             mBucketDuration = bucketDuration;
-            mBucketStart = new ArrayList<>(initialCapacity);
-            mActiveTime = new ArrayList<>(initialCapacity);
-            mRxBytes = new ArrayList<>(initialCapacity);
-            mRxPackets = new ArrayList<>(initialCapacity);
-            mTxBytes = new ArrayList<>(initialCapacity);
-            mTxPackets = new ArrayList<>(initialCapacity);
-            mOperations = new ArrayList<>(initialCapacity);
+            // Create a collection that is always sorted and can deduplicate items by the timestamp.
+            mEntries = new TreeMap<>();
         }
 
         /**
-         * Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
+         * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. If the timestamp
+         * already exists, the given {@link Entry} will be combined into existing entry.
          *
          * @param entry The target {@link Entry} object.
          * @return The builder object.
          */
         @NonNull
         public Builder addEntry(@NonNull Entry entry) {
-            mBucketStart.add(entry.bucketStart);
-            mActiveTime.add(entry.activeTime);
-            mRxBytes.add(entry.rxBytes);
-            mRxPackets.add(entry.rxPackets);
-            mTxBytes.add(entry.txBytes);
-            mTxPackets.add(entry.txPackets);
-            mOperations.add(entry.operations);
+            final Entry existing = mEntries.get(entry.bucketStart);
+            if (existing != null) {
+                mEntries.put(entry.bucketStart, existing.plus(entry, mBucketDuration));
+            } else {
+                mEntries.put(entry.bucketStart, entry);
+            }
             return this;
         }
 
-        private static long sum(@NonNull List<Long> list) {
-            long sum = 0;
-            for (long entry : list) {
+        private static long sum(@NonNull long[] array) {
+            long sum = 0L;
+            for (long entry : array) {
                 sum += entry;
             }
             return sum;
@@ -1147,16 +1181,30 @@
          */
         @NonNull
         public NetworkStatsHistory build() {
-            return new NetworkStatsHistory(mBucketDuration,
-                    CollectionUtils.toLongArray(mBucketStart),
-                    CollectionUtils.toLongArray(mActiveTime),
-                    CollectionUtils.toLongArray(mRxBytes),
-                    CollectionUtils.toLongArray(mRxPackets),
-                    CollectionUtils.toLongArray(mTxBytes),
-                    CollectionUtils.toLongArray(mTxPackets),
-                    CollectionUtils.toLongArray(mOperations),
-                    mBucketStart.size(),
-                    sum(mRxBytes) + sum(mTxBytes));
+            int size = mEntries.size();
+            final long[] bucketStart = new long[size];
+            final long[] activeTime = new long[size];
+            final long[] rxBytes = new long[size];
+            final long[] rxPackets = new long[size];
+            final long[] txBytes = new long[size];
+            final long[] txPackets = new long[size];
+            final long[] operations = new long[size];
+
+            int i = 0;
+            for (Entry entry : mEntries.values()) {
+                bucketStart[i] = entry.bucketStart;
+                activeTime[i] = entry.activeTime;
+                rxBytes[i] = entry.rxBytes;
+                rxPackets[i] = entry.rxPackets;
+                txBytes[i] = entry.txBytes;
+                txPackets[i] = entry.txPackets;
+                operations[i] = entry.operations;
+                i++;
+            }
+
+            return new NetworkStatsHistory(mBucketDuration, bucketStart, activeTime,
+                    rxBytes, rxPackets, txBytes, txPackets, operations,
+                    size, sum(rxBytes) + sum(txBytes));
         }
     }
 }
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index 7b5afd7..b82a126 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -393,8 +393,9 @@
         //constructor passes METERED_YES for these types.
         this(matchRule, subscriberId, matchSubscriberIds,
                 wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0],
-                (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD) ? METERED_YES
-                : METERED_ALL , ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
+                        || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
                 OEM_MANAGED_ALL, NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
     }
 
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index bc836d8..dc4ac55 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -205,7 +205,7 @@
      *                server context.
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SystemApi(client = MODULE_LIBRARIES)
     @SuppressLint("VisiblySynchronized")
     public static synchronized void init(@NonNull final Context context) {
         if (sStatsService != null) {
@@ -376,7 +376,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     public static void setThreadStatsTagDownload() {
         setThreadStatsTag(TAG_SYSTEM_DOWNLOAD);
     }
@@ -468,7 +468,7 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagSocket(Socket socket) throws SocketException {
+    public static void tagSocket(@NonNull Socket socket) throws SocketException {
         SocketTagger.get().tag(socket);
     }
 
@@ -483,7 +483,7 @@
      * calling {@code untagSocket()} before sending the socket to another
      * process.
      */
-    public static void untagSocket(Socket socket) throws SocketException {
+    public static void untagSocket(@NonNull Socket socket) throws SocketException {
         SocketTagger.get().untag(socket);
     }
 
@@ -496,14 +496,14 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagDatagramSocket(DatagramSocket socket) throws SocketException {
+    public static void tagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
         SocketTagger.get().tag(socket);
     }
 
     /**
      * Remove any statistics parameters from the given {@link DatagramSocket}.
      */
-    public static void untagDatagramSocket(DatagramSocket socket) throws SocketException {
+    public static void untagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
         SocketTagger.get().untag(socket);
     }
 
@@ -516,7 +516,7 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagFileDescriptor(FileDescriptor fd) throws IOException {
+    public static void tagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
         SocketTagger.get().tag(fd);
     }
 
@@ -524,7 +524,7 @@
      * Remove any statistics parameters from the given {@link FileDescriptor}
      * socket.
      */
-    public static void untagFileDescriptor(FileDescriptor fd) throws IOException {
+    public static void untagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
         SocketTagger.get().untag(fd);
     }
 
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 33b44c8..fad63e5 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -175,6 +175,7 @@
      *
      * @see #ACTION_NSD_STATE_CHANGED
      */
+    // TODO: Deprecate this since NSD service is never disabled.
     public static final int NSD_STATE_DISABLED = 1;
 
     /**
@@ -230,17 +231,12 @@
     public static final int DAEMON_STARTUP                          = 19;
 
     /** @hide */
-    public static final int ENABLE                                  = 20;
-    /** @hide */
-    public static final int DISABLE                                 = 21;
+    public static final int MDNS_SERVICE_EVENT                      = 20;
 
     /** @hide */
-    public static final int MDNS_SERVICE_EVENT                      = 22;
-
+    public static final int REGISTER_CLIENT                         = 21;
     /** @hide */
-    public static final int REGISTER_CLIENT                         = 23;
-    /** @hide */
-    public static final int UNREGISTER_CLIENT                       = 24;
+    public static final int UNREGISTER_CLIENT                       = 22;
 
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -266,8 +262,6 @@
         EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
         EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
         EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
-        EVENT_NAMES.put(ENABLE, "ENABLE");
-        EVENT_NAMES.put(DISABLE, "DISABLE");
         EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
     }
 
@@ -312,9 +306,12 @@
             @Override
             public void onAvailable(@NonNull Network network) {
                 final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener(
-                        network, mBaseListener);
+                        network, mBaseListener, mBaseExecutor);
                 mPerNetworkListeners.put(network, wrappedListener);
-                discoverServices(mServiceType, mProtocolType, network, mBaseExecutor,
+                // Run discovery callbacks inline on the service handler thread, which is the
+                // same thread used by this NetworkCallback, but DelegatingDiscoveryListener will
+                // use the base executor to run the wrapped callbacks.
+                discoverServices(mServiceType, mProtocolType, network, Runnable::run,
                         wrappedListener);
             }
 
@@ -334,7 +331,8 @@
         public void start(@NonNull NetworkRequest request) {
             final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
             cm.registerNetworkCallback(request, mNetworkCb, mHandler);
-            mHandler.post(() -> mBaseListener.onDiscoveryStarted(mServiceType));
+            mHandler.post(() -> mBaseExecutor.execute(() ->
+                    mBaseListener.onDiscoveryStarted(mServiceType)));
         }
 
         /**
@@ -351,7 +349,7 @@
                 final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
                 cm.unregisterNetworkCallback(mNetworkCb);
                 if (mPerNetworkListeners.size() == 0) {
-                    mBaseListener.onDiscoveryStopped(mServiceType);
+                    mBaseExecutor.execute(() -> mBaseListener.onDiscoveryStopped(mServiceType));
                     return;
                 }
                 for (int i = 0; i < mPerNetworkListeners.size(); i++) {
@@ -399,14 +397,23 @@
             }
         }
 
+        /**
+         * A listener wrapping calls to an app-provided listener, while keeping track of found
+         * services, so they can all be reported lost when the underlying network is lost.
+         *
+         * This should be registered to run on the service handler.
+         */
         private class DelegatingDiscoveryListener implements DiscoveryListener {
             private final Network mNetwork;
             private final DiscoveryListener mWrapped;
+            private final Executor mWrappedExecutor;
             private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>();
 
-            private DelegatingDiscoveryListener(Network network, DiscoveryListener listener) {
+            private DelegatingDiscoveryListener(Network network, DiscoveryListener listener,
+                    Executor executor) {
                 mNetwork = network;
                 mWrapped = listener;
+                mWrappedExecutor = executor;
             }
 
             void notifyAllServicesLost() {
@@ -415,7 +422,7 @@
                     final NsdServiceInfo serviceInfo = new NsdServiceInfo(
                             trackedInfo.mServiceName, trackedInfo.mServiceType);
                     serviceInfo.setNetwork(mNetwork);
-                    mWrapped.onServiceLost(serviceInfo);
+                    mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
                 }
             }
 
@@ -444,7 +451,7 @@
                     // Do not report onStopDiscoveryFailed when some underlying listeners failed:
                     // this does not mean that all listeners did, and onStopDiscoveryFailed is not
                     // actionable anyway. Just report that discovery stopped.
-                    mWrapped.onDiscoveryStopped(serviceType);
+                    mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
                 }
             }
 
@@ -452,20 +459,20 @@
             public void onDiscoveryStopped(String serviceType) {
                 mPerNetworkListeners.remove(mNetwork);
                 if (mStopRequested && mPerNetworkListeners.size() == 0) {
-                    mWrapped.onDiscoveryStopped(serviceType);
+                    mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
                 }
             }
 
             @Override
             public void onServiceFound(NsdServiceInfo serviceInfo) {
                 mFoundInfo.add(new TrackedNsdInfo(serviceInfo));
-                mWrapped.onServiceFound(serviceInfo);
+                mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo));
             }
 
             @Override
             public void onServiceLost(NsdServiceInfo serviceInfo) {
                 mFoundInfo.remove(new TrackedNsdInfo(serviceInfo));
-                mWrapped.onServiceLost(serviceInfo);
+                mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
             }
         }
     }
@@ -648,8 +655,12 @@
 
         @Override
         public void handleMessage(Message message) {
+            // Do not use message in the executor lambdas, as it will be recycled once this method
+            // returns. Keep references to its content instead.
             final int what = message.what;
+            final int errorCode = message.arg1;
             final int key = message.arg2;
+            final Object obj = message.obj;
             final Object listener;
             final NsdServiceInfo ns;
             final Executor executor;
@@ -659,7 +670,7 @@
                 executor = mExecutorMap.get(key);
             }
             if (listener == null) {
-                Log.d(TAG, "Stale key " + message.arg2);
+                Log.d(TAG, "Stale key " + key);
                 return;
             }
             if (DBG) {
@@ -667,28 +678,28 @@
             }
             switch (what) {
                 case DISCOVER_SERVICES_STARTED:
-                    final String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
+                    final String s = getNsdServiceInfoType((NsdServiceInfo) obj);
                     executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
                     break;
                 case DISCOVER_SERVICES_FAILED:
                     removeListener(key);
                     executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
-                            getNsdServiceInfoType(ns), message.arg1));
+                            getNsdServiceInfoType(ns), errorCode));
                     break;
                 case SERVICE_FOUND:
                     executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
-                            (NsdServiceInfo) message.obj));
+                            (NsdServiceInfo) obj));
                     break;
                 case SERVICE_LOST:
                     executor.execute(() -> ((DiscoveryListener) listener).onServiceLost(
-                            (NsdServiceInfo) message.obj));
+                            (NsdServiceInfo) obj));
                     break;
                 case STOP_DISCOVERY_FAILED:
                     // TODO: failure to stop discovery should be internal and retried internally, as
                     // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
                     removeListener(key);
                     executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
-                            getNsdServiceInfoType(ns), message.arg1));
+                            getNsdServiceInfoType(ns), errorCode));
                     break;
                 case STOP_DISCOVERY_SUCCEEDED:
                     removeListener(key);
@@ -698,33 +709,33 @@
                 case REGISTER_SERVICE_FAILED:
                     removeListener(key);
                     executor.execute(() -> ((RegistrationListener) listener).onRegistrationFailed(
-                            ns, message.arg1));
+                            ns, errorCode));
                     break;
                 case REGISTER_SERVICE_SUCCEEDED:
                     executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered(
-                            (NsdServiceInfo) message.obj));
+                            (NsdServiceInfo) obj));
                     break;
                 case UNREGISTER_SERVICE_FAILED:
                     removeListener(key);
                     executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed(
-                            ns, message.arg1));
+                            ns, errorCode));
                     break;
                 case UNREGISTER_SERVICE_SUCCEEDED:
                     // TODO: do not unregister listener until service is unregistered, or provide
                     // alternative way for unregistering ?
-                    removeListener(message.arg2);
+                    removeListener(key);
                     executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered(
                             ns));
                     break;
                 case RESOLVE_SERVICE_FAILED:
                     removeListener(key);
                     executor.execute(() -> ((ResolveListener) listener).onResolveFailed(
-                            ns, message.arg1));
+                            ns, errorCode));
                     break;
                 case RESOLVE_SERVICE_SUCCEEDED:
                     removeListener(key);
                     executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
-                            (NsdServiceInfo) message.obj));
+                            (NsdServiceInfo) obj));
                     break;
                 default:
                     Log.d(TAG, "Ignored " + message);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2621594..200c808 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -53,6 +53,8 @@
     @Nullable
     private Network mNetwork;
 
+    private int mInterfaceIndex;
+
     public NsdServiceInfo() {
     }
 
@@ -312,8 +314,11 @@
     /**
      * Get the network where the service can be found.
      *
-     * This is never null if this {@link NsdServiceInfo} was obtained from
-     * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}.
+     * This is set if this {@link NsdServiceInfo} was obtained from
+     * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service
+     * was found on a network interface that does not have a {@link Network} (such as a tethering
+     * downstream, where services are advertised from devices connected to this device via
+     * tethering).
      */
     @Nullable
     public Network getNetwork() {
@@ -329,6 +334,26 @@
         mNetwork = network;
     }
 
+    /**
+     * Get the index of the network interface where the service was found.
+     *
+     * This is only set when the service was found on an interface that does not have a usable
+     * Network, in which case {@link #getNetwork()} returns null.
+     * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+     * @hide
+     */
+    public int getInterfaceIndex() {
+        return mInterfaceIndex;
+    }
+
+    /**
+     * Set the index of the network interface where the service was found.
+     * @hide
+     */
+    public void setInterfaceIndex(int interfaceIndex) {
+        mInterfaceIndex = interfaceIndex;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -375,6 +400,7 @@
         }
 
         dest.writeParcelable(mNetwork, 0);
+        dest.writeInt(mInterfaceIndex);
     }
 
     /** Implement the Parcelable interface */
@@ -405,6 +431,7 @@
                     info.mTxtRecord.put(in.readString(), valueArray);
                 }
                 info.mNetwork = in.readParcelable(null, Network.class);
+                info.mInterfaceIndex = in.readInt();
                 return info;
             }
 
diff --git a/framework/Android.bp b/framework/Android.bp
index d7de439..6350e14 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,7 +64,6 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
-        ":framework-connectivity-javastream-protos",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -90,8 +89,10 @@
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "framework-connectivity-javastream-protos",
     ],
     libs: [
+        "androidx.annotation_annotation",
         "app-compat-annotations",
         "framework-connectivity-t.stubs.module_lib",
         "unsupportedappusage",
@@ -197,28 +198,16 @@
     visibility: ["//frameworks/base"],
 }
 
-gensrcs {
+java_library {
     name: "framework-connectivity-javastream-protos",
-    depfile: true,
-
-    tools: [
-        "aprotoc",
-        "protoc-gen-javastream",
-        "soong_zip",
+    proto: {
+        type: "stream",
+    },
+    srcs: [":framework-connectivity-protos"],
+    installable: false,
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
     ],
-
-    cmd: "mkdir -p $(genDir)/$(in) " +
-        "&& $(location aprotoc) " +
-        "  --plugin=$(location protoc-gen-javastream) " +
-        "  --dependency_out=$(depfile) " +
-        "  --javastream_out=$(genDir)/$(in) " +
-        "  -Iexternal/protobuf/src " +
-        "  -I . " +
-        "  $(in) " +
-        "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
-    srcs: [
-        ":framework-connectivity-protos",
-    ],
-    output_extension: "srcjar",
 }
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index ddac19d..a2a1ac0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,6 +51,9 @@
     field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
     field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
     field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+    field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
+    field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
+    field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
     field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
     field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
     field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
@@ -197,6 +200,8 @@
     method public int describeContents();
     method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
     method @NonNull public String getInterfaceName();
+    method @Nullable public android.net.MacAddress getMacAddress();
+    method public int getMtu();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
   }
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index db1d7e9..f1298ce 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -249,10 +249,10 @@
     method public void onValidationStatus(int, @Nullable android.net.Uri);
     method @NonNull public android.net.Network register();
     method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
+    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
+    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
+    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
     method public final void sendQosCallbackError(int, int);
     method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
     method public final void sendQosSessionLost(int, int, int);
@@ -262,7 +262,7 @@
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
     method public void setLingerDuration(@NonNull java.time.Duration);
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
     method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
     field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
new file mode 100644
index 0000000..1311765
--- /dev/null
+++ b/framework/jarjar-excludes.txt
@@ -0,0 +1,25 @@
+# INetworkStatsProvider / INetworkStatsProviderCallback are referenced from net-tests-utils, which
+# may be used by tests that do not apply connectivity jarjar rules.
+# TODO: move files to a known internal package (like android.net.connectivity.visiblefortesting)
+# so that they do not need jarjar
+android\.net\.netstats\.provider\.INetworkStatsProvider(\$.+)?
+android\.net\.netstats\.provider\.INetworkStatsProviderCallback(\$.+)?
+
+# INetworkAgent / INetworkAgentRegistry are used in NetworkAgentTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.INetworkAgent(\$.+)?
+android\.net\.INetworkAgentRegistry(\$.+)?
+
+# IConnectivityDiagnosticsCallback used in ConnectivityDiagnosticsManagerTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+
+
+# KeepaliveUtils is used by ConnectivityManager CTS
+# TODO: move into service-connectivity so framework-connectivity stops using
+# ServiceConnectivityResources (callers need high permissions to find/query the resource apk anyway)
+# and have a ConnectivityManager test API instead
+android\.net\.util\.KeepaliveUtils(\$.+)?
+
+# TODO (b/217115866): add jarjar rules for Nearby
+android\.nearby\..+
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a174fe3..eeedfd1 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,30 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
+    /**
+     * Firewall chain used for OEM-specific application restrictions.
+     * Denylist of apps that will not have network access due to OEM-specific restrictions.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7;
+
+    /**
+     * Firewall chain used for OEM-specific application restrictions.
+     * Denylist of apps that will not have network access due to OEM-specific restrictions.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8;
+
+    /**
+     * Firewall chain used for OEM-specific application restrictions.
+     * Denylist of apps that will not have network access due to OEM-specific restrictions.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,7 +1013,10 @@
         FIREWALL_CHAIN_STANDBY,
         FIREWALL_CHAIN_POWERSAVE,
         FIREWALL_CHAIN_RESTRICTED,
-        FIREWALL_CHAIN_LOW_POWER_STANDBY
+        FIREWALL_CHAIN_LOW_POWER_STANDBY,
+        FIREWALL_CHAIN_OEM_DENY_1,
+        FIREWALL_CHAIN_OEM_DENY_2,
+        FIREWALL_CHAIN_OEM_DENY_3
     })
     public @interface FirewallChain {}
     // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
@@ -2589,9 +2616,24 @@
      * {@hide}
      */
     public ConnectivityManager(Context context, IConnectivityManager service) {
+        this(context, service, true /* newStatic */);
+    }
+
+    private ConnectivityManager(Context context, IConnectivityManager service, boolean newStatic) {
         mContext = Objects.requireNonNull(context, "missing context");
         mService = Objects.requireNonNull(service, "missing IConnectivityManager");
-        sInstance = this;
+        // sInstance is accessed without a lock, so it may actually be reassigned several times with
+        // different ConnectivityManager, but that's still OK considering its usage.
+        if (sInstance == null && newStatic) {
+            final Context appContext = mContext.getApplicationContext();
+            // Don't create static ConnectivityManager instance again to prevent infinite loop.
+            // If the application context is null, we're either in the system process or
+            // it's the application context very early in app initialization. In both these
+            // cases, the passed-in Context will not be freed, so it's safe to pass it to the
+            // service. http://b/27532714 .
+            sInstance = new ConnectivityManager(appContext != null ? appContext : context, service,
+                    false /* newStatic */);
+        }
     }
 
     /** {@hide} */
@@ -5861,6 +5903,7 @@
      *
      * @param chain target chain.
      * @param enable whether the chain should be enabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws IllegalStateException if enabling or disabling the firewall chain failed.
      * @hide
      */
@@ -5879,6 +5922,29 @@
     }
 
     /**
+     * Get the specified firewall chain's status.
+     *
+     * @param chain target chain.
+     * @return {@code true} if chain is enabled, {@code false} if chain is disabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public boolean getFirewallChainEnabled(@FirewallChain final int chain) {
+        try {
+            return mService.getFirewallChainEnabled(chain);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Replaces the contents of the specified UID-based firewall chain.
      *
      * @param chain target chain to replace.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index bc73769..29fea00 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -244,5 +244,7 @@
 
     void setFirewallChainEnabled(int chain, boolean enable);
 
+    boolean getFirewallChainEnabled(int chain);
+
     void replaceFirewallChain(int chain, in int[] uids);
 }
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 847f14e..d18b931 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,7 +29,10 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs);
+    TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+            in LinkAddress[] addrs, in @nullable String iface);
+
+    void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
 
     void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
             in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 8782b33..a8f707e 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -64,7 +64,7 @@
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Switch to S_V2 when it is available.
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2)
     @VisibleForTesting
     public static final long EXCLUDED_ROUTES = 186082280;
 
@@ -1366,6 +1366,21 @@
     }
 
     /**
+     * Returns true if this link has a throw route.
+     *
+     * @return {@code true} if there is an exclude route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasExcludeRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.getType() == RouteInfo.RTN_THROW) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Compares this {@code LinkProperties} interface name against the target
      *
      * @param target LinkProperties to compare.
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 29add1c..5659a35 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -913,7 +913,7 @@
      * Must be called by the agent when the network's {@link LinkProperties} change.
      * @param linkProperties the new LinkProperties.
      */
-    public final void sendLinkProperties(@NonNull LinkProperties linkProperties) {
+    public void sendLinkProperties(@NonNull LinkProperties linkProperties) {
         Objects.requireNonNull(linkProperties);
         final LinkProperties lp = new LinkProperties(linkProperties);
         queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
@@ -938,7 +938,7 @@
      * @param underlyingNetworks the new list of underlying networks.
      * @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])}
      */
-    public final void setUnderlyingNetworks(
+    public void setUnderlyingNetworks(
             @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
         final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
                 ? new ArrayList<>(underlyingNetworks) : null;
@@ -1076,18 +1076,19 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public final void sendNetworkInfo(NetworkInfo networkInfo) {
-        queueOrSendNetworkInfo(new NetworkInfo(networkInfo));
+        queueOrSendNetworkInfo(networkInfo);
     }
 
     private void queueOrSendNetworkInfo(NetworkInfo networkInfo) {
-        queueOrSendMessage(reg -> reg.sendNetworkInfo(networkInfo));
+        final NetworkInfo ni = new NetworkInfo(networkInfo);
+        queueOrSendMessage(reg -> reg.sendNetworkInfo(ni));
     }
 
     /**
      * Must be called by the agent when the network's {@link NetworkCapabilities} change.
      * @param networkCapabilities the new NetworkCapabilities.
      */
-    public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+    public void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
         Objects.requireNonNull(networkCapabilities);
         mBandwidthUpdatePending.set(false);
         mLastBwRefreshTime = System.currentTimeMillis();
@@ -1101,7 +1102,7 @@
      *
      * @param score the new score.
      */
-    public final void sendNetworkScore(@NonNull NetworkScore score) {
+    public void sendNetworkScore(@NonNull NetworkScore score) {
         Objects.requireNonNull(score);
         queueOrSendMessage(reg -> reg.sendScore(score));
     }
@@ -1112,7 +1113,7 @@
      * @param score the new score, between 0 and 99.
      * deprecated use sendNetworkScore(NetworkScore) TODO : remove in S.
      */
-    public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
+    public void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
         sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build());
     }
 
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 97b1f32..dbb05a9 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -3043,7 +3043,7 @@
          * <p>
          * This list cannot be null, but it can be empty to mean that no UID without the
          * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission
-         * gets to access this network.
+         * can access this network.
          *
          * @param uids the list of UIDs that can always access this network
          * @return this builder
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
index 0665af5..3615075 100644
--- a/framework/src/android/net/NetworkProvider.java
+++ b/framework/src/android/net/NetworkProvider.java
@@ -192,21 +192,36 @@
     private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub {
         @NonNull public final NetworkOfferCallback callback;
         @NonNull private final Executor mExecutor;
+        /**
+         * Boolean flag that prevents onNetworkNeeded / onNetworkUnneeded callbacks from being
+         * propagated after unregisterNetworkOffer has been called. Since unregisterNetworkOffer
+         * runs on the CS handler thread, it will not go into effect immediately.
+         */
+        private volatile boolean mIsStale;
 
         NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback,
                 @NonNull final Executor executor) {
             this.callback = callback;
             this.mExecutor = executor;
+            this.mIsStale = false;
         }
 
         @Override
         public void onNetworkNeeded(final @NonNull NetworkRequest request) {
-            mExecutor.execute(() -> callback.onNetworkNeeded(request));
+            mExecutor.execute(() -> {
+                if (!mIsStale) callback.onNetworkNeeded(request);
+            });
         }
 
         @Override
         public void onNetworkUnneeded(final @NonNull NetworkRequest request) {
-            mExecutor.execute(() -> callback.onNetworkUnneeded(request));
+            mExecutor.execute(() -> {
+                if (!mIsStale) callback.onNetworkUnneeded(request);
+            });
+        }
+
+        public void markStale() {
+            mIsStale = true;
         }
     }
 
@@ -326,7 +341,10 @@
     public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) {
         final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
         if (null == proxy) return;
-        mProxies.remove(proxy);
+        synchronized (mProxies) {
+            proxy.markStale();
+            mProxies.remove(proxy);
+        }
         mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy);
     }
 }
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index fb271e3..fdcab02 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -120,8 +120,8 @@
     public String toString() {
         return "ProfileNetworkPreference{"
                 + "mPreference=" + getPreference()
-                + "mIncludedUids=" + mIncludedUids.toString()
-                + "mExcludedUids=" + mExcludedUids.toString()
+                + "mIncludedUids=" + Arrays.toString(mIncludedUids)
+                + "mExcludedUids=" + Arrays.toString(mExcludedUids)
                 + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
                 + '}';
     }
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index ed6eb15..b80cff4 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -46,8 +46,10 @@
             EX_TYPE_FILTER_NONE,
             EX_TYPE_FILTER_NETWORK_RELEASED,
             EX_TYPE_FILTER_SOCKET_NOT_BOUND,
+            EX_TYPE_FILTER_SOCKET_NOT_CONNECTED,
             EX_TYPE_FILTER_NOT_SUPPORTED,
             EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED,
+            EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ExceptionType {}
@@ -65,10 +67,16 @@
     public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
+    public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 3;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
+    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 4;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 5;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED = 6;
 
     /**
      * Creates exception based off of a type and message.  Not all types of exceptions accept a
@@ -83,12 +91,17 @@
                 return new QosCallbackException(new NetworkReleasedException());
             case EX_TYPE_FILTER_SOCKET_NOT_BOUND:
                 return new QosCallbackException(new SocketNotBoundException());
+            case EX_TYPE_FILTER_SOCKET_NOT_CONNECTED:
+                return new QosCallbackException(new SocketNotConnectedException());
             case EX_TYPE_FILTER_NOT_SUPPORTED:
                 return new QosCallbackException(new UnsupportedOperationException(
                         "This device does not support the specified filter"));
             case EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED:
                 return new QosCallbackException(
                         new SocketLocalAddressChangedException());
+            case EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED:
+                return new QosCallbackException(
+                        new SocketRemoteAddressChangedException());
             default:
                 Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'");
                 return new QosCallbackException(
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index 5c1c3cc..b432644 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -90,5 +90,15 @@
      */
     public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
             int startPort, int endPort);
+
+    /**
+     * Determines whether or not the parameter will be matched with this filter.
+     *
+     * @param protocol the protocol such as TCP or UDP included in IP packet filter set of a QoS
+     *                 flow assigned on {@link Network}.
+     * @return whether the parameters match the socket type of the filter
+     * @hide
+     */
+    public abstract boolean matchesProtocol(int protocol);
 }
 
diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java
index da3b2cf..6e71fa3 100644
--- a/framework/src/android/net/QosFilterParcelable.java
+++ b/framework/src/android/net/QosFilterParcelable.java
@@ -104,7 +104,7 @@
         if (mQosFilter instanceof QosSocketFilter) {
             dest.writeInt(QOS_SOCKET_FILTER);
             final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter;
-            qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0);
+            qosSocketFilter.getQosSocketInfo().writeToParcelWithoutFd(dest, 0);
             return;
         }
         dest.writeInt(NO_FILTER_PRESENT);
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
index 69da7f4..5ceeb67 100644
--- a/framework/src/android/net/QosSocketFilter.java
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -18,6 +18,13 @@
 
 import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
 import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -74,19 +81,34 @@
      * 2. In the scenario that the socket is now bound to a different local address, which can
      *    happen in the case of UDP, then
      *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED} is returned.
+     * 3. In the scenario that the UDP socket changed remote address, then
+     *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED} is returned.
+     *
      * @return validation error code
      */
     @Override
     public int validate() {
-        final InetSocketAddress sa = getAddressFromFileDescriptor();
-        if (sa == null) {
-            return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+        final InetSocketAddress sa = getLocalAddressFromFileDescriptor();
+
+        if (sa == null || (sa.getAddress().isAnyLocalAddress() && sa.getPort() == 0)) {
+            return EX_TYPE_FILTER_SOCKET_NOT_BOUND;
         }
 
         if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) {
             return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
         }
 
+        if (mQosSocketInfo.getRemoteSocketAddress() != null) {
+            final InetSocketAddress da = getRemoteAddressFromFileDescriptor();
+            if (da == null) {
+                return EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+            }
+
+            if (!da.equals(mQosSocketInfo.getRemoteSocketAddress())) {
+                return EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+            }
+        }
+
         return EX_TYPE_FILTER_NONE;
     }
 
@@ -98,17 +120,14 @@
      * @return the local address
      */
     @Nullable
-    private InetSocketAddress getAddressFromFileDescriptor() {
+    private InetSocketAddress getLocalAddressFromFileDescriptor() {
         final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
-        if (parcelFileDescriptor == null) return null;
-
         final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
-        if (fd == null) return null;
 
         final SocketAddress address;
         try {
             address = Os.getsockname(fd);
-        } catch (final ErrnoException e) {
+        } catch (ErrnoException e) {
             Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e);
             return null;
         }
@@ -119,6 +138,31 @@
     }
 
     /**
+     * The remote address of the socket's connected.
+     *
+     * <p>Note: If the socket is no longer connected, null is returned.
+     *
+     * @return the remote address
+     */
+    @Nullable
+    private InetSocketAddress getRemoteAddressFromFileDescriptor() {
+        final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
+        final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
+
+        final SocketAddress address;
+        try {
+            address = Os.getpeername(fd);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "getAddressFromFileDescriptor: getRemoteAddress exception", e);
+            return null;
+        }
+        if (address instanceof InetSocketAddress) {
+            return (InetSocketAddress) address;
+        }
+        return null;
+    }
+
+    /**
      * The network used with this filter.
      *
      * @return the registered {@link Network}
@@ -156,6 +200,18 @@
     }
 
     /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean matchesProtocol(final int protocol) {
+        if ((mQosSocketInfo.getSocketType() == SOCK_STREAM && protocol == IPPROTO_TCP)
+                || (mQosSocketInfo.getSocketType() == SOCK_DGRAM && protocol == IPPROTO_UDP)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
      * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
      * filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
@@ -174,6 +230,7 @@
             final int startPort, final int endPort) {
         return startPort <= filterSocketAddress.getPort()
                 && endPort >= filterSocketAddress.getPort()
-                && filterSocketAddress.getAddress().equals(address);
+                && (address.isAnyLocalAddress()
+                        || filterSocketAddress.getAddress().equals(address));
     }
 }
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 39c2f33..49ac22b 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -165,25 +165,28 @@
     /* Parcelable methods */
     private QosSocketInfo(final Parcel in) {
         mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
-        mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        final boolean withFd = in.readBoolean();
+        if (withFd) {
+            mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        } else {
+            mParcelFileDescriptor = null;
+        }
 
-        final int localAddressLength = in.readInt();
-        mLocalSocketAddress = readSocketAddress(in, localAddressLength);
-
-        final int remoteAddressLength = in.readInt();
-        mRemoteSocketAddress = remoteAddressLength == 0 ? null
-                : readSocketAddress(in, remoteAddressLength);
+        mLocalSocketAddress = readSocketAddress(in);
+        mRemoteSocketAddress = readSocketAddress(in);
 
         mSocketType = in.readInt();
     }
 
-    private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
-        final byte[] address = new byte[addressLength];
-        in.readByteArray(address);
+    private InetSocketAddress readSocketAddress(final Parcel in) {
+        final byte[] addrBytes = in.createByteArray();
+        if (addrBytes == null) {
+            return null;
+        }
         final int port = in.readInt();
 
         try {
-            return new InetSocketAddress(InetAddress.getByAddress(address), port);
+            return new InetSocketAddress(InetAddress.getByAddress(addrBytes), port);
         } catch (final UnknownHostException e) {
             /* This can never happen. UnknownHostException will never be thrown
                since the address provided is numeric and non-null. */
@@ -198,20 +201,35 @@
 
     @Override
     public void writeToParcel(@NonNull final Parcel dest, final int flags) {
-        mNetwork.writeToParcel(dest, 0);
-        mParcelFileDescriptor.writeToParcel(dest, 0);
+        writeToParcelInternal(dest, flags, /*includeFd=*/ true);
+    }
 
-        final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
-        dest.writeInt(localAddress.length);
-        dest.writeByteArray(localAddress);
+    /**
+     * Used when sending QosSocketInfo to telephony, which does not need access to the socket FD.
+     * @hide
+     */
+    public void writeToParcelWithoutFd(@NonNull final Parcel dest, final int flags) {
+        writeToParcelInternal(dest, flags, /*includeFd=*/ false);
+    }
+
+    private void writeToParcelInternal(
+            @NonNull final Parcel dest, final int flags, boolean includeFd) {
+        mNetwork.writeToParcel(dest, 0);
+
+        if (includeFd) {
+            dest.writeBoolean(true);
+            mParcelFileDescriptor.writeToParcel(dest, 0);
+        } else {
+            dest.writeBoolean(false);
+        }
+
+        dest.writeByteArray(mLocalSocketAddress.getAddress().getAddress());
         dest.writeInt(mLocalSocketAddress.getPort());
 
         if (mRemoteSocketAddress == null) {
-            dest.writeInt(0);
+            dest.writeByteArray(null);
         } else {
-            final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
-            dest.writeInt(remoteAddress.length);
-            dest.writeByteArray(remoteAddress);
+            dest.writeByteArray(mRemoteSocketAddress.getAddress().getAddress());
             dest.writeInt(mRemoteSocketAddress.getPort());
         }
         dest.writeInt(mSocketType);
diff --git a/framework/src/android/net/SocketNotConnectedException.java b/framework/src/android/net/SocketNotConnectedException.java
new file mode 100644
index 0000000..fa2a615
--- /dev/null
+++ b/framework/src/android/net/SocketNotConnectedException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.net;
+
+/**
+ * Thrown when a previously bound socket becomes unbound.
+ *
+ * @hide
+ */
+public class SocketNotConnectedException extends Exception {
+    /** @hide */
+    public SocketNotConnectedException() {
+        super("The socket is not connected");
+    }
+}
diff --git a/framework/src/android/net/SocketRemoteAddressChangedException.java b/framework/src/android/net/SocketRemoteAddressChangedException.java
new file mode 100644
index 0000000..ecaeebc
--- /dev/null
+++ b/framework/src/android/net/SocketRemoteAddressChangedException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.net;
+
+/**
+ * Thrown when the local address of the socket has changed.
+ *
+ * @hide
+ */
+public class SocketRemoteAddressChangedException extends Exception {
+    /** @hide */
+    public SocketRemoteAddressChangedException() {
+        super("The remote address of the socket changed");
+    }
+}
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
index 4449ff8..26200e1 100644
--- a/framework/src/android/net/TestNetworkInterface.java
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -16,22 +16,32 @@
 package android.net;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
 
 /**
- * This class is used to return the interface name and fd of the test interface
+ * This class is used to return the interface name, fd, MAC, and MTU of the test interface
  *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public final class TestNetworkInterface implements Parcelable {
+    private static final String TAG = "TestNetworkInterface";
+
     @NonNull
     private final ParcelFileDescriptor mFileDescriptor;
     @NonNull
     private final String mInterfaceName;
+    @Nullable
+    private final MacAddress mMacAddress;
+    private final int mMtu;
 
     @Override
     public int describeContents() {
@@ -40,18 +50,41 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeParcelable(mFileDescriptor, flags);
         out.writeString(mInterfaceName);
+        out.writeParcelable(mMacAddress, flags);
+        out.writeInt(mMtu);
     }
 
     public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
         mFileDescriptor = pfd;
         mInterfaceName = intf;
+
+        MacAddress macAddress = null;
+        int mtu = 1500;
+        try {
+            // This constructor is called by TestNetworkManager which runs inside the system server,
+            // which has permission to read the MacAddress.
+            NetworkInterface nif = NetworkInterface.getByName(mInterfaceName);
+
+            // getHardwareAddress() returns null for tun interfaces.
+            byte[] hardwareAddress = nif.getHardwareAddress();
+            if (hardwareAddress != null) {
+                macAddress = MacAddress.fromBytes(nif.getHardwareAddress());
+            }
+            mtu = nif.getMTU();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to fetch MacAddress or MTU size from NetworkInterface", e);
+        }
+        mMacAddress = macAddress;
+        mMtu = mtu;
     }
 
     private TestNetworkInterface(@NonNull Parcel in) {
         mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
         mInterfaceName = in.readString();
+        mMacAddress = in.readParcelable(MacAddress.class.getClassLoader());
+        mMtu = in.readInt();
     }
 
     @NonNull
@@ -64,6 +97,15 @@
         return mInterfaceName;
     }
 
+    @Nullable
+    public MacAddress getMacAddress() {
+        return mMacAddress;
+    }
+
+    public int getMtu() {
+        return mMtu;
+    }
+
     @NonNull
     public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
             new Parcelable.Creator<TestNetworkInterface>() {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 280e497..788834a 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -45,6 +45,12 @@
      */
     public static final String TEST_TAP_PREFIX = "testtap";
 
+    /**
+     * Prefix for clat interfaces.
+     * @hide
+     */
+    public static final String CLAT_INTERFACE_PREFIX = "v4-";
+
     @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName();
 
     @NonNull private final ITestNetworkManager mService;
@@ -52,6 +58,7 @@
     private static final boolean TAP = false;
     private static final boolean TUN = true;
     private static final boolean BRING_UP = true;
+    private static final boolean CARRIER_UP = true;
     private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
 
     /** @hide */
@@ -160,7 +167,8 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr));
+            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
+                    null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -178,7 +186,25 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, BRING_UP, NO_ADDRS);
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface for testing purposes
+     *
+     * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
+     * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
+     *     ParcelFileDescriptor to tear down the TAP interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(@NonNull LinkAddress[] linkAddrs) {
+        try {
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, linkAddrs, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -197,7 +223,66 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with a given interface name for testing purposes
+     *
+     * @param bringUp whether to bring up the interface before returning it.
+     * @param iface interface name to be assigned, so far only interface name which starts with
+     *              "v4-testtap" or "v4-testtun" is allowed to be created. If it's null, then use
+     *              the default name(e.g. testtap or testtun).
+     *
+     * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
+     *     TAP interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
+        try {
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with or without carrier for testing purposes.
+     *
+     * Note: setting carrierUp = false is not supported until kernel version 5.0.
+     *
+     * @param carrierUp whether the created interface has a carrier or not.
+     * @param bringUp whether to bring up the interface before returning it.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
+        try {
+            return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable / disable carrier on TestNetworkInterface
+     *
+     * Note: TUNSETCARRIER is not supported until kernel version 5.0.
+     *
+     * @param iface the interface to configure.
+     * @param enabled true to turn carrier on, false to turn carrier off.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        try {
+            mService.setCarrierEnabled(iface, enabled);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/netd/Android.bp b/netd/Android.bp
index 5ac02d3..c731b8b 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -55,7 +55,8 @@
 cc_test {
     name: "netd_updatable_unit_test",
     defaults: ["netd_defaults"],
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,  // required by setrlimitForTest()
     header_libs: [
         "bpf_connectivity_headers",
@@ -72,6 +73,7 @@
         "liblog",
         "libnetdutils",
     ],
+    compile_multilib: "both",
     multilib: {
         lib32: {
             suffix: "32",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index f3dfb57..fad6bbb 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -73,16 +73,7 @@
     }
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
-
-    // For the devices that support cgroup socket filter, the socket filter
-    // should be loaded successfully by bpfloader. So we attach the filter to
-    // cgroup if the program is pinned properly.
-    // TODO: delete the if statement once all devices should support cgroup
-    // socket filter (ie. the minimum kernel version required is 4.14).
-    if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
-        RETURN_IF_NOT_OK(
-                attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
-    }
+    RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
     return netdutils::status::ok;
 }
 
@@ -110,9 +101,8 @@
     RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
     RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
     RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
-    RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
-                                                  BPF_ANY));
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -207,6 +197,7 @@
 
     BpfMap<StatsKey, StatsValue>& currentMap =
             (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
+    // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
     base::Result<void> res = currentMap.iterate(countUidStatsEntries);
     if (!res.ok()) {
         ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
@@ -242,7 +233,7 @@
     if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
     base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
     if (!res.ok()) {
-        ALOGE("Failed to untag socket: %s\n", strerror(res.error().code()));
+        ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
     return 0;
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 2ede1c1..5ee04d1 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -23,6 +23,7 @@
 #include "bpf_shared.h"
 
 using android::bpf::BpfMap;
+using android::bpf::BpfMapRO;
 
 namespace android {
 namespace net {
@@ -61,8 +62,8 @@
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
     BpfMap<StatsKey, StatsValue> mStatsMapA;
-    BpfMap<StatsKey, StatsValue> mStatsMapB;
-    BpfMap<uint32_t, uint8_t> mConfigurationMap;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapB;
+    BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
     std::mutex mMutex;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index cd6b565..99160da 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -21,6 +21,7 @@
 
 #include <gtest/gtest.h>
 
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "BpfHandler.h"
 
 using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
@@ -48,44 +49,36 @@
     BpfHandler mBh;
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+    BpfMapRO<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
         std::lock_guard guard(mBh.mMutex);
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
-                                          TEST_MAP_SIZE, 0));
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeCookieTagMap);
 
-        mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
-                                       TEST_MAP_SIZE, 0));
+        mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidPermissionMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidPermissionMap);
 
-        mBh.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+        mBh.mCookieTagMap = mFakeCookieTagMap;
         ASSERT_VALID(mBh.mCookieTagMap);
-        mBh.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+        mBh.mStatsMapA = mFakeStatsMapA;
         ASSERT_VALID(mBh.mStatsMapA);
-        mBh.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+        mBh.mConfigurationMap = mFakeConfigurationMap;
         ASSERT_VALID(mBh.mConfigurationMap);
         // Always write to stats map A by default.
-        ASSERT_RESULT_OK(mBh.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
-                                                          SELECT_MAP_A, BPF_ANY));
-        mBh.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
-        ASSERT_VALID(mBh.mUidPermissionMap);
-    }
+        static_assert(SELECT_MAP_A == 0, "bpf map arrays are zero-initialized");
 
-    int dupFd(const android::base::unique_fd& mapFd) {
-        return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+        mBh.mUidPermissionMap = mFakeUidPermissionMap;
+        ASSERT_VALID(mBh.mUidPermissionMap);
     }
 
     int setUpSocketAndTag(int protocol, uint64_t* cookie, uint32_t tag, uid_t uid,
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index bf56fd5..5b3d314 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -48,7 +48,8 @@
 
 cc_test {
     name: "libnetworkstats_test",
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,  // required by setrlimitForTest()
     header_libs: ["bpf_connectivity_headers"],
     srcs: [
@@ -68,4 +69,13 @@
         "libbase",
         "liblog",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
 }
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 4d605ce..6605428 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -58,13 +58,7 @@
 }
 
 int bpfGetUidStats(uid_t uid, Stats* stats) {
-    BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
-
-    if (!appUidStatsMap.isValid()) {
-        int ret = -errno;
-        ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
     return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
 }
 
@@ -100,19 +94,8 @@
 }
 
 int bpfGetIfaceStats(const char* iface, Stats* stats) {
-    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    int ret;
-    if (!ifaceStatsMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
-        return ret;
-    }
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
 }
 
@@ -186,32 +169,21 @@
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
                                const std::vector<std::string>& limitIfaces, int limitTag,
                                int limitUid) {
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
-
-    BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
-    if (!configurationMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get configuration map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
     auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
     if (!configuration.ok()) {
         ALOGE("Cannot read the old configuration from map: %s",
               configuration.error().message().c_str());
         return -configuration.error().code();
     }
-    const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
-    BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
-    if (!statsMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
-        return ret;
+    if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+        ALOGE("%s unknown configuration value: %d", __func__, configuration.value());
+        return -EINVAL;
     }
+    const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+    // TODO: fix this to not constantly reopen the bpf map
+    BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
 
     // It is safe to read and clear the old map now since the
     // networkStatsFactory should call netd to swap the map in advance already.
@@ -262,20 +234,8 @@
 }
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
-    int ret = 0;
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
-
-    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    if (!ifaceStatsMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
     return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
 }
 
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 4974b96..6f9c8c2 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -33,6 +33,7 @@
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "bpf/BpfMap.h"
 #include "bpf/BpfUtils.h"
 #include "netdbpf/BpfNetworkStats.h"
@@ -80,19 +81,19 @@
         ASSERT_EQ(0, setrlimitForTest());
 
         mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+        ASSERT_TRUE(mFakeCookieTagMap.isValid());
 
         mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+        ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
 
         mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeStatsMap.getMap());
+        ASSERT_TRUE(mFakeStatsMap.isValid());
 
         mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+        ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
 
         mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+        ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
     }
 
     void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index 4bc40ea..16b9f1e 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -1452,6 +1452,11 @@
         final ConnectivityManager connectivityManager =
                 mContext.getSystemService(ConnectivityManager.class);
         final LinkProperties lp = connectivityManager.getLinkProperties(underlyingNetwork);
+        if (lp == null) {
+            throw new IllegalArgumentException(
+                    "LinkProperties is null. The underlyingNetwork may not be functional");
+        }
+
         if (tunnelInterfaceInfo.getInterfaceName().equals(lp.getInterfaceName())) {
             throw new IllegalArgumentException(
                     "Underlying network cannot be the network being exposed by this tunnel");
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 4086e4e..7115720 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
+import android.net.INetd;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.mdns.aidl.DiscoveryInfo;
@@ -100,7 +101,6 @@
     private class NsdStateMachine extends StateMachine {
 
         private final DefaultState mDefaultState = new DefaultState();
-        private final DisabledState mDisabledState = new DisabledState();
         private final EnabledState mEnabledState = new EnabledState();
 
         @Override
@@ -151,7 +151,6 @@
         NsdStateMachine(String name, Handler handler) {
             super(name, handler);
             addState(mDefaultState);
-                addState(mDisabledState, mDefaultState);
                 addState(mEnabledState, mDefaultState);
             State initialState = mEnabledState;
             setInitialState(initialState);
@@ -249,25 +248,6 @@
             }
         }
 
-        class DisabledState extends State {
-            @Override
-            public void enter() {
-                sendNsdStateChangeBroadcast(false);
-            }
-
-            @Override
-            public boolean processMessage(Message msg) {
-                switch (msg.what) {
-                    case NsdManager.ENABLE:
-                        transitionTo(mEnabledState);
-                        break;
-                    default:
-                        return NOT_HANDLED;
-                }
-                return HANDLED;
-            }
-        }
-
         class EnabledState extends State {
             @Override
             public void enter() {
@@ -311,10 +291,6 @@
                 final int clientId = msg.arg2;
                 final ListenerArgs args;
                 switch (msg.what) {
-                    case NsdManager.DISABLE:
-                        //TODO: cleanup clients
-                        transitionTo(mDisabledState);
-                        break;
                     case NsdManager.DISCOVER_SERVICES:
                         if (DBG) Log.d(TAG, "Discover services");
                         args = (ListenerArgs) msg.obj;
@@ -466,7 +442,7 @@
                             // interfaces that do not have an associated Network.
                             break;
                         }
-                        servInfo.setNetwork(new Network(foundNetId));
+                        setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
                         clientInfo.onServiceFound(clientId, servInfo);
                         break;
                     }
@@ -476,10 +452,11 @@
                         final String type = info.registrationType;
                         final int lostNetId = info.netId;
                         servInfo = new NsdServiceInfo(name, type);
-                        // The network could be null if it was torn down when the service is lost
-                        // TODO: avoid returning null in that case, possibly by remembering found
-                        // services on the same interface index and their network at the time
-                        servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
+                        // The network could be set to null (netId 0) if it was torn down when the
+                        // service is lost
+                        // TODO: avoid returning null in that case, possibly by remembering
+                        // found services on the same interface index and their network at the time
+                        setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
                         clientInfo.onServiceLost(clientId, servInfo);
                         break;
                     }
@@ -513,7 +490,7 @@
                             break;
                         }
 
-                        String name = fullName.substring(0, index);
+                        String name = unescape(fullName.substring(0, index));
                         String rest = fullName.substring(index);
                         String type = rest.replace(".local.", "");
 
@@ -557,7 +534,6 @@
                         final GetAddressInfo info = (GetAddressInfo) obj;
                         final String address = info.address;
                         final int netId = info.netId;
-                        final Network network = netId == NETID_UNSET ? null : new Network(netId);
                         InetAddress serviceHost = null;
                         try {
                             serviceHost = InetAddress.getByName(address);
@@ -568,9 +544,10 @@
                         // If the resolved service is on an interface without a network, consider it
                         // as a failure: it would not be usable by apps as they would need
                         // privileged permissions.
-                        if (network != null && serviceHost != null) {
+                        if (netId != NETID_UNSET && serviceHost != null) {
                             clientInfo.mResolvedService.setHost(serviceHost);
-                            clientInfo.mResolvedService.setNetwork(network);
+                            setServiceNetworkForCallback(clientInfo.mResolvedService,
+                                    netId, info.interfaceIdx);
                             clientInfo.onResolveServiceSucceeded(
                                     clientId, clientInfo.mResolvedService);
                         } else {
@@ -590,6 +567,55 @@
        }
     }
 
+    private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
+        switch (netId) {
+            case NETID_UNSET:
+                info.setNetwork(null);
+                break;
+            case INetd.LOCAL_NET_ID:
+                // Special case for LOCAL_NET_ID: Networks on netId 99 are not generally
+                // visible / usable for apps, so do not return it. Store the interface
+                // index instead, so at least if the client tries to resolve the service
+                // with that NsdServiceInfo, it will be done on the same interface.
+                // If they recreate the NsdServiceInfo themselves, resolution would be
+                // done on all interfaces as before T, which should also work.
+                info.setNetwork(null);
+                info.setInterfaceIndex(ifaceIdx);
+                break;
+            default:
+                info.setNetwork(new Network(netId));
+        }
+    }
+
+    // The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
+    // for passing to standard system DNS APIs such as res_query() . Thus, make the service name
+    // unescape for getting right service address. See "Notes on DNS Name Escaping" on
+    // external/mdnsresponder/mDNSShared/dns_sd.h for more details.
+    private String unescape(String s) {
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); ++i) {
+            char c = s.charAt(i);
+            if (c == '\\') {
+                if (++i >= s.length()) {
+                    Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                    break;
+                }
+                c = s.charAt(i);
+                if (c != '.' && c != '\\') {
+                    if (i + 2 >= s.length()) {
+                        Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                        break;
+                    }
+                    c = (char) ((c - '0') * 100 + (s.charAt(i + 1) - '0') * 10
+                            + (s.charAt(i + 2) - '0'));
+                    i += 2;
+                }
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
     @VisibleForTesting
     NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
         mCleanupDelayMs = cleanupDelayMs;
@@ -738,7 +764,12 @@
         String type = service.getServiceType();
         int port = service.getPort();
         byte[] textRecord = service.getTxtRecord();
-        return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY);
+        final int registerInterface = getNetworkInterfaceIndex(service);
+        if (service.getNetwork() != null && registerInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to register service on not found");
+            return false;
+        }
+        return mMDnsManager.registerService(regId, name, type, port, textRecord, registerInterface);
     }
 
     private boolean unregisterService(int regId) {
@@ -746,10 +777,9 @@
     }
 
     private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
-        final Network network = serviceInfo.getNetwork();
         final String type = serviceInfo.getServiceType();
-        final int discoverInterface = getNetworkInterfaceIndex(network);
-        if (network != null && discoverInterface == IFACE_IDX_ANY) {
+        final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
+        if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
             Log.e(TAG, "Interface to discover service on not found");
             return false;
         }
@@ -763,9 +793,8 @@
     private boolean resolveService(int resolveId, NsdServiceInfo service) {
         final String name = service.getServiceName();
         final String type = service.getServiceType();
-        final Network network = service.getNetwork();
-        final int resolveInterface = getNetworkInterfaceIndex(network);
-        if (network != null && resolveInterface == IFACE_IDX_ANY) {
+        final int resolveInterface = getNetworkInterfaceIndex(service);
+        if (service.getNetwork() != null && resolveInterface == IFACE_IDX_ANY) {
             Log.e(TAG, "Interface to resolve service on not found");
             return false;
         }
@@ -781,8 +810,17 @@
      * this is to support the legacy mdnsresponder implementation, which historically resolved
      * services on an unspecified network.
      */
-    private int getNetworkInterfaceIndex(Network network) {
-        if (network == null) return IFACE_IDX_ANY;
+    private int getNetworkInterfaceIndex(NsdServiceInfo serviceInfo) {
+        final Network network = serviceInfo.getNetwork();
+        if (network == null) {
+            // Fallback to getInterfaceIndex if present (typically if the NsdServiceInfo was
+            // provided by NsdService from discovery results, and the service was found on an
+            // interface that has no app-usable Network).
+            if (serviceInfo.getInterfaceIndex() != 0) {
+                return serviceInfo.getInterfaceIndex();
+            }
+            return IFACE_IDX_ANY;
+        }
 
         final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
         if (cm == null) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
index 6b623f4..6006539 100644
--- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -16,23 +16,37 @@
 
 package com.android.server.ethernet;
 
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
 import android.annotation.Nullable;
+import android.content.ApexEnvironment;
 import android.net.IpConfiguration;
 import android.os.Environment;
 import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.net.IpConfigStore;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 /**
  * This class provides an API to store and manage Ethernet network configuration.
  */
 public class EthernetConfigStore {
-    private static final String ipConfigFile = Environment.getDataDirectory() +
-            "/misc/ethernet/ipconfig.txt";
+    private static final String TAG = EthernetConfigStore.class.getSimpleName();
+    private static final String CONFIG_FILE = "ipconfig.txt";
+    private static final String FILE_PATH = "/misc/ethernet/";
+    private static final String LEGACY_IP_CONFIG_FILE_PATH = Environment.getDataDirectory()
+            + FILE_PATH;
+    private static final String APEX_IP_CONFIG_FILE_PATH = ApexEnvironment.getApexEnvironment(
+            TETHERING_MODULE_NAME).getDeviceProtectedDataDir() + FILE_PATH;
 
     private IpConfigStore mStore = new IpConfigStore();
-    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private final ArrayMap<String, IpConfiguration> mIpConfigurations;
     private IpConfiguration mIpConfigurationForDefaultInterface;
     private final Object mSync = new Object();
 
@@ -40,22 +54,70 @@
         mIpConfigurations = new ArrayMap<>(0);
     }
 
-    public void read() {
-        synchronized (mSync) {
-            ArrayMap<String, IpConfiguration> configs =
-                    IpConfigStore.readIpConfigurations(ipConfigFile);
+    private static boolean doesConfigFileExist(final String filepath) {
+        return new File(filepath).exists();
+    }
 
-            // This configuration may exist in old file versions when there was only a single active
-            // Ethernet interface.
-            if (configs.containsKey("0")) {
-                mIpConfigurationForDefaultInterface = configs.remove("0");
+    private void writeLegacyIpConfigToApexPath(final String newFilePath, final String oldFilePath,
+            final String filename) {
+        final File directory = new File(newFilePath);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
+
+        // Write the legacy IP config to the apex file path.
+        FileOutputStream fos = null;
+        final AtomicFile dst = new AtomicFile(new File(newFilePath + filename));
+        final AtomicFile src = new AtomicFile(new File(oldFilePath + filename));
+        try {
+            final byte[] raw = src.readFully();
+            if (raw.length > 0) {
+                fos = dst.startWrite();
+                fos.write(raw);
+                fos.flush();
+                dst.finishWrite(fos);
             }
-
-            mIpConfigurations = configs;
+        } catch (IOException e) {
+            Log.e(TAG, "Fail to sync the legacy IP config to the apex file path.");
+            dst.failWrite(fos);
         }
     }
 
+    public void read() {
+        read(APEX_IP_CONFIG_FILE_PATH, LEGACY_IP_CONFIG_FILE_PATH, CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void read(final String newFilePath, final String oldFilePath, final String filename) {
+        synchronized (mSync) {
+            // Attempt to read the IP configuration from apex file path first.
+            if (doesConfigFileExist(newFilePath + filename)) {
+                loadConfigFileLocked(newFilePath + filename);
+                return;
+            }
+
+            // If the config file doesn't exist in the apex file path, attempt to read it from
+            // the legacy file path, if config file exists, write the legacy IP configuration to
+            // apex config file path, this should just happen on the first boot. New or updated
+            // config entries are only written to the apex config file later.
+            if (!doesConfigFileExist(oldFilePath + filename)) return;
+            loadConfigFileLocked(oldFilePath + filename);
+            writeLegacyIpConfigToApexPath(newFilePath, oldFilePath, filename);
+        }
+    }
+
+    private void loadConfigFileLocked(final String filepath) {
+        final ArrayMap<String, IpConfiguration> configs =
+                IpConfigStore.readIpConfigurations(filepath);
+        mIpConfigurations.putAll(configs);
+    }
+
     public void write(String iface, IpConfiguration config) {
+        write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void write(String iface, IpConfiguration config, String filepath) {
         boolean modified;
 
         synchronized (mSync) {
@@ -67,7 +129,7 @@
             }
 
             if (modified) {
-                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+                mStore.writeIpConfigurations(filepath, mIpConfigurations);
             }
         }
     }
@@ -80,9 +142,6 @@
 
     @Nullable
     public IpConfiguration getIpConfigurationForDefaultInterface() {
-        synchronized (mSync) {
-            return mIpConfigurationForDefaultInterface == null
-                    ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
-        }
+        return null;
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index eb22f78..cedffe1 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
@@ -32,10 +31,9 @@
 import android.net.LinkProperties;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
-import android.net.NetworkFactory;
 import android.net.NetworkProvider;
 import android.net.NetworkRequest;
-import android.net.NetworkSpecifier;
+import android.net.NetworkScore;
 import android.net.ip.IIpClient;
 import android.net.ip.IpClientCallbacks;
 import android.net.ip.IpClientManager;
@@ -47,6 +45,7 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -57,27 +56,25 @@
 
 import java.io.FileDescriptor;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * {@link NetworkFactory} that represents Ethernet networks.
+ * Class that manages NetworkOffers for Ethernet networks.
  *
- * This class reports a static network score of 70 when it is tracking an interface and that
- * interface's link is up, and a score of 0 otherwise.
+ * TODO: this class should be merged into EthernetTracker.
  */
-public class EthernetNetworkFactory extends NetworkFactory {
+public class EthernetNetworkFactory {
     private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
     final static boolean DBG = true;
 
-    private final static int NETWORK_SCORE = 70;
     private static final String NETWORK_TYPE = "Ethernet";
-    private static final String LEGACY_TCP_BUFFER_SIZES =
-            "524288,1048576,3145728,524288,1048576,2097152";
 
     private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
             new ConcurrentHashMap<>();
     private final Handler mHandler;
     private final Context mContext;
+    private final NetworkProvider mProvider;
     final Dependencies mDeps;
 
     public static class Dependencies {
@@ -99,25 +96,9 @@
             return InterfaceParams.getByName(name);
         }
 
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String getPlatformTcpBufferSizes(Context context) {
-            final Resources r = context.getResources();
-            final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string",
-                    context.getPackageName());
-            return r.getString(resId);
-        }
-
         public String getTcpBufferSizesFromResource(Context context) {
-            final String tcpBufferSizes;
-            final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context);
-            if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) {
-                // Platform resource is not the historical default: use the overlay.
-                tcpBufferSizes = platformTcpBufferSizes;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers);
-            }
-            return tcpBufferSizes;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(R.string.config_ethernet_tcp_buffers);
         }
     }
 
@@ -128,54 +109,24 @@
     }
 
     public EthernetNetworkFactory(Handler handler, Context context) {
-        this(handler, context, new Dependencies());
+        this(handler, context, new NetworkProvider(context, handler.getLooper(), TAG),
+            new Dependencies());
     }
 
     @VisibleForTesting
-    EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
-        super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
-
+    EthernetNetworkFactory(Handler handler, Context context, NetworkProvider provider,
+            Dependencies deps) {
         mHandler = handler;
         mContext = context;
+        mProvider = provider;
         mDeps = deps;
-
-        setScoreFilter(NETWORK_SCORE);
     }
 
-    @Override
-    public boolean acceptRequest(NetworkRequest request) {
-        if (DBG) {
-            Log.d(TAG, "acceptRequest, request: " + request);
-        }
-
-        return networkForRequest(request) != null;
-    }
-
-    @Override
-    protected void needNetworkFor(NetworkRequest networkRequest) {
-        NetworkInterfaceState network = networkForRequest(networkRequest);
-
-        if (network == null) {
-            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
-            return;
-        }
-
-        if (++network.refCount == 1) {
-            network.start();
-        }
-    }
-
-    @Override
-    protected void releaseNetworkFor(NetworkRequest networkRequest) {
-        NetworkInterfaceState network = networkForRequest(networkRequest);
-        if (network == null) {
-            Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
-            return;
-        }
-
-        if (--network.refCount == 0) {
-            network.stop();
-        }
+    /**
+     * Registers the network provider with the system.
+     */
+    public void register() {
+        mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
     }
 
     /**
@@ -213,9 +164,8 @@
         }
 
         final NetworkInterfaceState iface = new NetworkInterfaceState(
-                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, mProvider, mDeps);
         mTrackingInterfaces.put(ifaceName, iface);
-        updateCapabilityFilter();
     }
 
     @VisibleForTesting
@@ -256,7 +206,6 @@
         final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
         iface.updateInterface(ipConfig, capabilities, listener);
         mTrackingInterfaces.put(ifaceName, iface);
-        updateCapabilityFilter();
     }
 
     private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
@@ -267,16 +216,6 @@
        return builder.build();
     }
 
-    private void updateCapabilityFilter() {
-        NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
-        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
-            capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
-        }
-
-        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
-        setCapabilityFilter(capabilitiesFilter);
-    }
-
     private static NetworkCapabilities createDefaultNetworkCapabilities() {
         return NetworkCapabilities.Builder
                 .withoutDefaultCapabilities()
@@ -284,14 +223,17 @@
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    protected void removeInterface(String interfaceName) {
+    protected boolean removeInterface(String interfaceName) {
         NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
         if (iface != null) {
-            iface.maybeSendNetworkManagementCallbackForAbort();
-            iface.stop();
+            iface.unregisterNetworkOfferAndStop();
+            return true;
         }
-
-        updateCapabilityFilter();
+        // TODO(b/236892130): if an interface is currently in server mode, it may not be properly
+        // removed.
+        // TODO: when false is returned, do not send a STATE_ABSENT callback.
+        Log.w(TAG, interfaceName + " is not tracked and cannot be removed");
+        return false;
     }
 
     /** Returns true if state has been modified */
@@ -323,37 +265,6 @@
         return mTrackingInterfaces.containsKey(ifaceName);
     }
 
-    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
-        String requestedIface = null;
-
-        NetworkSpecifier specifier = request.getNetworkSpecifier();
-        if (specifier instanceof EthernetNetworkSpecifier) {
-            requestedIface = ((EthernetNetworkSpecifier) specifier)
-                .getInterfaceName();
-        }
-
-        NetworkInterfaceState network = null;
-        if (!TextUtils.isEmpty(requestedIface)) {
-            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
-            if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
-                network = n;
-            }
-        } else {
-            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
-                if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
-                    network = n;
-                    break;
-                }
-            }
-        }
-
-        if (DBG) {
-            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
-        }
-
-        return network;
-    }
-
     private static void maybeSendNetworkManagementCallback(
             @Nullable final INetworkInterfaceOutcomeReceiver listener,
             @Nullable final String iface,
@@ -380,14 +291,16 @@
         private final String mHwAddress;
         private final Handler mHandler;
         private final Context mContext;
-        private final NetworkFactory mNetworkFactory;
+        private final NetworkProvider mNetworkProvider;
         private final Dependencies mDeps;
+        private final NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
 
         private static String sTcpBufferSizes = null;  // Lazy initialized.
 
         private boolean mLinkUp;
         private int mLegacyType;
         private LinkProperties mLinkProperties = new LinkProperties();
+        private final Set<Integer> mRequestIds = new ArraySet<>();
 
         private volatile @Nullable IpClientManager mIpClient;
         private @NonNull NetworkCapabilities mCapabilities;
@@ -416,8 +329,6 @@
                     ConnectivityManager.TYPE_NONE);
         }
 
-        long refCount = 0;
-
         private class EthernetIpClientCallback extends IpClientCallbacks {
             private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
             private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
@@ -488,17 +399,51 @@
             }
         }
 
+        private class EthernetNetworkOfferCallback implements NetworkProvider.NetworkOfferCallback {
+            @Override
+            public void onNetworkNeeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG, String.format("%s: onNetworkNeeded for request: %s", name, request));
+                }
+                // When the network offer is first registered, onNetworkNeeded is called with all
+                // existing requests.
+                // ConnectivityService filters requests for us based on the NetworkCapabilities
+                // passed in the registerNetworkOffer() call.
+                mRequestIds.add(request.requestId);
+                // if the network is already started, this is a no-op.
+                start();
+            }
+
+            @Override
+            public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG,
+                            String.format("%s: onNetworkUnneeded for request: %s", name, request));
+                }
+                if (!mRequestIds.remove(request.requestId)) {
+                    // This can only happen if onNetworkNeeded was not called for a request or if
+                    // the requestId changed. Both should *never* happen.
+                    Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+                }
+                if (mRequestIds.isEmpty()) {
+                    // not currently serving any requests, stop the network.
+                    stop();
+                }
+            }
+        }
+
         NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
                 @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
-                NetworkFactory networkFactory, Dependencies deps) {
+                NetworkProvider networkProvider, Dependencies deps) {
             name = ifaceName;
             mIpConfig = Objects.requireNonNull(ipConfig);
             mCapabilities = Objects.requireNonNull(capabilities);
             mLegacyType = getLegacyType(mCapabilities);
             mHandler = handler;
             mContext = context;
-            mNetworkFactory = networkFactory;
+            mNetworkProvider = networkProvider;
             mDeps = deps;
+            mNetworkOfferCallback = new EthernetNetworkOfferCallback();
             mHwAddress = hwAddress;
         }
 
@@ -521,9 +466,19 @@
                     + "transport type.");
         }
 
+        private static NetworkScore getNetworkScore() {
+            return new NetworkScore.Builder().build();
+        }
+
         private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
             mCapabilities = new NetworkCapabilities(capabilities);
             mLegacyType = getLegacyType(mCapabilities);
+
+            if (mLinkUp) {
+                // registering a new network offer will update the existing one, not install a
+                // new one.
+                registerNetworkOffer();
+            }
         }
 
         void updateInterface(@Nullable final IpConfiguration ipConfig,
@@ -543,8 +498,6 @@
             if (null != capabilities) {
                 setCapabilities(capabilities);
             }
-            // Send an abort callback if a request is filed before the previous one has completed.
-            maybeSendNetworkManagementCallbackForAbort();
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
@@ -594,7 +547,7 @@
                     .setLegacyExtraInfo(mHwAddress)
                     .build();
             mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
-                    mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+                    mCapabilities, mLinkProperties, config, mNetworkProvider,
                     new EthernetNetworkAgent.Callbacks() {
                         @Override
                         public void onNetworkUnwanted() {
@@ -685,26 +638,27 @@
             mLinkUp = up;
 
             if (!up) { // was up, goes down
-                // Send an abort on a provisioning request callback if necessary before stopping.
-                maybeSendNetworkManagementCallbackForAbort();
-                stop();
+                // retract network offer and stop IpClient.
+                unregisterNetworkOfferAndStop();
                 // If only setting the interface down, send a callback to signal completion.
                 EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
             } else { // was down, goes up
-                stop();
-                start(listener);
+                // register network offer
+                registerNetworkOffer();
             }
 
             return true;
         }
 
-        void stop() {
+        private void stop() {
             // Invalidate all previous start requests
             if (mIpClient != null) {
                 mIpClient.shutdown();
                 mIpClientCallback.awaitIpClientShutdown();
                 mIpClient = null;
             }
+            // Send an abort callback if an updateInterface request was in progress.
+            maybeSendNetworkManagementCallbackForAbort();
             mIpClientCallback = null;
 
             if (mNetworkAgent != null) {
@@ -714,6 +668,18 @@
             mLinkProperties.clear();
         }
 
+        private void registerNetworkOffer() {
+            mNetworkProvider.registerNetworkOffer(getNetworkScore(),
+                    new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+                    mNetworkOfferCallback);
+        }
+
+        public void unregisterNetworkOfferAndStop() {
+            mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+            stop();
+            mRequestIds.clear();
+        }
+
         private static void provisionIpClient(@NonNull final IpClientManager ipClient,
                 @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
             if (config.getProxySettings() == ProxySettings.STATIC ||
@@ -753,7 +719,6 @@
         @Override
         public String toString() {
             return getClass().getSimpleName() + "{ "
-                    + "refCount: " + refCount + ", "
                     + "iface: " + name + ", "
                     + "up: " + mLinkUp + ", "
                     + "hwAddress: " + mHwAddress + ", "
@@ -766,7 +731,6 @@
     }
 
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
-        super.dump(fd, pw, args);
         pw.println(getClass().getSimpleName());
         pw.println("Tracking interfaces:");
         pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 5e830ad..f058f94 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -16,19 +16,22 @@
 
 package com.android.server.ethernet;
 
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.EthernetNetworkUpdateRequest;
 import android.net.IEthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.ITetheredInterfaceCallback;
-import android.net.EthernetNetworkUpdateRequest;
 import android.net.IpConfiguration;
 import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -216,19 +219,39 @@
                 "EthernetServiceImpl");
     }
 
-    private void maybeValidateTestCapabilities(final String iface,
-            @Nullable final NetworkCapabilities nc) {
+    private void validateOrSetNetworkSpecifier(String iface, NetworkCapabilities nc) {
+        final NetworkSpecifier spec = nc.getNetworkSpecifier();
+        if (spec == null) {
+            nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));
+            return;
+        }
+        if (!(spec instanceof EthernetNetworkSpecifier)) {
+            throw new IllegalArgumentException("Invalid specifier type for request.");
+        }
+        if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {
+            throw new IllegalArgumentException("Invalid interface name set on specifier.");
+        }
+    }
+
+    private void maybeValidateTestCapabilities(String iface, NetworkCapabilities nc) {
         if (!mTracker.isValidTestInterface(iface)) {
             return;
         }
-        // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
-        // allowed.
-        if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+        if (!nc.hasTransport(TRANSPORT_TEST)) {
             throw new IllegalArgumentException(
                     "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
         }
     }
 
+    private void maybeValidateEthernetTransport(String iface, NetworkCapabilities nc) {
+        if (mTracker.isValidTestInterface(iface)) {
+            return;
+        }
+        if (!nc.hasSingleTransport(TRANSPORT_ETHERNET)) {
+            throw new IllegalArgumentException("Invalid transport type for request.");
+        }
+    }
+
     private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
             final String logMessage) {
         if (mTracker.isValidTestInterface(iface)) {
@@ -251,36 +274,41 @@
 
         // TODO: validate that iface is listed in overlay config_ethernet_interfaces
         // only automotive devices are allowed to set the NetworkCapabilities using this API
-        enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
-                "updateConfiguration() with non-null capabilities");
-        maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+        final NetworkCapabilities nc = request.getNetworkCapabilities();
+        enforceAdminPermission(
+                iface, nc != null, "updateConfiguration() with non-null capabilities");
+        if (nc != null) {
+            validateOrSetNetworkSpecifier(iface, nc);
+            maybeValidateTestCapabilities(iface, nc);
+            maybeValidateEthernetTransport(iface, nc);
+        }
 
         mTracker.updateConfiguration(
-                iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+                iface, request.getIpConfiguration(), nc, listener);
     }
 
     @Override
-    public void connectNetwork(@NonNull final String iface,
+    public void enableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
-        enforceAdminPermission(iface, true, "connectNetwork()");
+        enforceAdminPermission(iface, false, "enableInterface()");
 
-        mTracker.connectNetwork(iface, listener);
+        mTracker.enableInterface(iface, listener);
     }
 
     @Override
-    public void disconnectNetwork(@NonNull final String iface,
+    public void disableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
-        enforceAdminPermission(iface, true, "connectNetwork()");
+        enforceAdminPermission(iface, false, "disableInterface()");
 
-        mTracker.disconnectNetwork(iface, listener);
+        mTracker.disableInterface(iface, listener);
     }
 
     @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 693d91a..3e71093 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -25,7 +25,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
@@ -86,7 +85,6 @@
     private static final boolean DBG = EthernetNetworkFactory.DBG;
 
     private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+";
-    private static final String LEGACY_IFACE_REGEXP = "eth\\d";
 
     /**
      * Interface names we track. This is a product-dependent regular expression, plus,
@@ -134,48 +132,16 @@
     }
 
     public static class Dependencies {
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String getPlatformRegexResource(Context context) {
-            final Resources r = context.getResources();
-            final int resId =
-                r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName());
-            return r.getString(resId);
-        }
-
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String[] getPlatformInterfaceConfigs(Context context) {
-            final Resources r = context.getResources();
-            final int resId = r.getIdentifier("config_ethernet_interfaces", "array",
-                    context.getPackageName());
-            return r.getStringArray(resId);
-        }
-
         public String getInterfaceRegexFromResource(Context context) {
-            final String platformRegex = getPlatformRegexResource(context);
-            final String match;
-            if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) {
-                // Platform resource is not the historical default: use the overlay
-                match = platformRegex;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                match = resources.get().getString(
-                        com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
-            }
-            return match;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(
+                    com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
         }
 
         public String[] getInterfaceConfigFromResource(Context context) {
-            final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context);
-            final String[] interfaceConfigs;
-            if (platformInterfaceConfigs.length != 0) {
-                // Platform resource is not the historical default: use the overlay
-                interfaceConfigs = platformInterfaceConfigs;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                interfaceConfigs = resources.get().getStringArray(
-                        com.android.connectivity.resources.R.array.config_ethernet_interfaces);
-            }
-            return interfaceConfigs;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getStringArray(
+                    com.android.connectivity.resources.R.array.config_ethernet_interfaces);
         }
     }
 
@@ -262,7 +228,7 @@
      */
     protected void broadcastInterfaceStateChange(@NonNull String iface) {
         ensureRunningOnEthernetServiceThread();
-        final int state = mFactory.getInterfaceState(iface);
+        final int state = getInterfaceState(iface);
         final int role = getInterfaceRole(iface);
         final IpConfiguration config = getIpConfigurationForCallback(iface, state);
         final int n = mListeners.beginBroadcast();
@@ -303,6 +269,8 @@
                     + ", ipConfig: " + ipConfig);
         }
 
+        // TODO: do the right thing if the interface was in server mode: either fail this operation,
+        // or take the interface out of server mode.
         final IpConfiguration localIpConfig = ipConfig == null
                 ? null : new IpConfiguration(ipConfig);
         if (ipConfig != null) {
@@ -319,13 +287,13 @@
     }
 
     @VisibleForTesting(visibility = PACKAGE)
-    protected void connectNetwork(@NonNull final String iface,
+    protected void enableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
         mHandler.post(() -> updateInterfaceState(iface, true, listener));
     }
 
     @VisibleForTesting(visibility = PACKAGE)
-    protected void disconnectNetwork(@NonNull final String iface,
+    protected void disableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
         mHandler.post(() -> updateInterfaceState(iface, false, listener));
     }
@@ -469,15 +437,34 @@
         if (mDefaultInterface != null) {
             removeInterface(mDefaultInterface);
             addInterface(mDefaultInterface);
+            // when this broadcast is sent, any calls to notifyTetheredInterfaceAvailable or
+            // notifyTetheredInterfaceUnavailable have already happened
+            broadcastInterfaceStateChange(mDefaultInterface);
         }
     }
 
+    private int getInterfaceState(final String iface) {
+        if (mFactory.hasInterface(iface)) {
+            return mFactory.getInterfaceState(iface);
+        }
+        if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+            // server mode interfaces are not tracked by the factory.
+            // TODO(b/234743836): interface state for server mode interfaces is not tracked
+            // properly; just return link up.
+            return EthernetManager.STATE_LINK_UP;
+        }
+        return EthernetManager.STATE_ABSENT;
+    }
+
     private int getInterfaceRole(final String iface) {
-        if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE;
-        final int mode = getInterfaceMode(iface);
-        return (mode == INTERFACE_MODE_CLIENT)
-                ? EthernetManager.ROLE_CLIENT
-                : EthernetManager.ROLE_SERVER;
+        if (mFactory.hasInterface(iface)) {
+            // only client mode interfaces are tracked by the factory.
+            return EthernetManager.ROLE_CLIENT;
+        }
+        if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+            return EthernetManager.ROLE_SERVER;
+        }
+        return EthernetManager.ROLE_NONE;
     }
 
     private int getInterfaceMode(final String iface) {
@@ -594,8 +581,8 @@
         }
         if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
 
-        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
-        if (mDefaultInterface == null) {
+        // Do not make an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null && !mNetworkCapabilities.containsKey(iface)) {
             mDefaultInterface = iface;
         }
 
@@ -620,14 +607,18 @@
         }
     }
 
-    private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+    @VisibleForTesting
+    class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
 
         @Override
         public void onInterfaceLinkStateChanged(String iface, boolean up) {
             if (DBG) {
                 Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
             }
-            mHandler.post(() -> updateInterfaceState(iface, up));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                updateInterfaceState(iface, up);
+            });
         }
 
         @Override
@@ -635,7 +626,10 @@
             if (DBG) {
                 Log.i(TAG, "onInterfaceAdded, iface: " + iface);
             }
-            mHandler.post(() -> maybeTrackInterface(iface));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                maybeTrackInterface(iface);
+            });
         }
 
         @Override
@@ -643,7 +637,10 @@
             if (DBG) {
                 Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
             }
-            mHandler.post(() -> stopTrackingInterface(iface));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                stopTrackingInterface(iface);
+            });
         }
     }
 
@@ -922,6 +919,8 @@
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
         postAndWaitForRunnable(() -> {
             pw.println(getClass().getSimpleName());
+            pw.println("Ethernet State: "
+                    + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
             pw.println("Ethernet interface name filter: " + mIfaceMatch);
             pw.println("Default interface: " + mDefaultInterface);
             pw.println("Default interface mode: " + mDefaultInterfaceMode);
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
index 25c88eb..3b44d81 100644
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -38,7 +38,7 @@
     private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
     // This is current path but may be changed soon.
     private static final String IFACE_INDEX_NAME_MAP_PATH =
-            "/sys/fs/bpf/map_netd_iface_index_name_map";
+            "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
     private final IBpfMap<U32, InterfaceMapValue> mBpfMap;
     private final INetd mNetd;
     private final Handler mHandler;
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 3b93f1a..b628251 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -164,16 +164,17 @@
     }
 
     public NetworkStatsFactory(@NonNull Context ctx) {
-        this(ctx, new File("/proc/"), true);
+        this(ctx, new File("/proc/"), true, new BpfNetMaps());
     }
 
     @VisibleForTesting
-    public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats) {
+    public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats,
+            BpfNetMaps bpfNetMaps) {
         mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
         mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
         mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
         mUseBpfStats = useBpfStats;
-        mBpfNetMaps = new BpfNetMaps();
+        mBpfNetMaps = bpfNetMaps;
         synchronized (mPersistentDataLock) {
             mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
             mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index fdfc893..1cd670a 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -18,6 +18,7 @@
 
 import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
 
+import android.annotation.NonNull;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -38,10 +39,12 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.PerUidCounter;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -52,16 +55,26 @@
  */
 class NetworkStatsObservers {
     private static final String TAG = "NetworkStatsObservers";
+    private static final boolean LOG = true;
     private static final boolean LOGV = false;
 
     private static final int MSG_REGISTER = 1;
     private static final int MSG_UNREGISTER = 2;
     private static final int MSG_UPDATE_STATS = 3;
 
+    private static final int DUMP_USAGE_REQUESTS_COUNT = 200;
+
+    // The maximum number of request allowed per uid before an exception is thrown.
+    @VisibleForTesting
+    static final int MAX_REQUESTS_PER_UID = 100;
+
     // All access to this map must be done from the handler thread.
     // indexed by DataUsageRequest#requestId
     private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
 
+    // Request counters per uid, this is thread safe.
+    private final PerUidCounter mDataUsageRequestsPerUid = new PerUidCounter(MAX_REQUESTS_PER_UID);
+
     // Sequence number of DataUsageRequests
     private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
 
@@ -77,13 +90,16 @@
      *
      * @return the normalized request wrapped within {@link RequestInfo}.
      */
-    public DataUsageRequest register(Context context, DataUsageRequest inputRequest,
-            IUsageCallback callback, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+    public DataUsageRequest register(@NonNull Context context,
+            @NonNull DataUsageRequest inputRequest, @NonNull IUsageCallback callback,
+            int callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
         DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
-        RequestInfo requestInfo = buildRequestInfo(request, callback, callingUid,
-                accessLevel);
+        RequestInfo requestInfo = buildRequestInfo(request, callback, callingPid, callingUid,
+                callingPackage, accessLevel);
+        if (LOG) Log.d(TAG, "Registering observer for " + requestInfo);
+        mDataUsageRequestsPerUid.incrementCountOrThrow(callingUid);
 
-        if (LOGV) Log.v(TAG, "Registering observer for " + request);
         getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
         return request;
     }
@@ -172,7 +188,7 @@
         RequestInfo requestInfo;
         requestInfo = mDataUsageRequests.get(request.requestId);
         if (requestInfo == null) {
-            if (LOGV) Log.v(TAG, "Trying to unregister unknown request " + request);
+            if (LOG) Log.d(TAG, "Trying to unregister unknown request " + request);
             return;
         }
         if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
@@ -180,8 +196,9 @@
             return;
         }
 
-        if (LOGV) Log.v(TAG, "Unregistering " + request);
+        if (LOG) Log.d(TAG, "Unregistering " + requestInfo);
         mDataUsageRequests.remove(request.requestId);
+        mDataUsageRequestsPerUid.decrementCountOrThrow(requestInfo.mCallingUid);
         requestInfo.unlinkDeathRecipient();
         requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
     }
@@ -214,18 +231,19 @@
     }
 
     private RequestInfo buildRequestInfo(DataUsageRequest request, IUsageCallback callback,
-            int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+            int callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
         if (accessLevel <= NetworkStatsAccess.Level.USER) {
-            return new UserUsageRequestInfo(this, request, callback, callingUid,
-                    accessLevel);
+            return new UserUsageRequestInfo(this, request, callback, callingPid,
+                    callingUid, callingPackage, accessLevel);
         } else {
             // Safety check in case a new access level is added and we forgot to update this
             if (accessLevel < NetworkStatsAccess.Level.DEVICESUMMARY) {
                 throw new IllegalArgumentException(
                         "accessLevel " + accessLevel + " is less than DEVICESUMMARY.");
             }
-            return new NetworkUsageRequestInfo(this, request, callback, callingUid,
-                    accessLevel);
+            return new NetworkUsageRequestInfo(this, request, callback, callingPid,
+                    callingUid, callingPackage, accessLevel);
         }
     }
 
@@ -237,18 +255,22 @@
         private final NetworkStatsObservers mStatsObserver;
         protected final DataUsageRequest mRequest;
         private final IUsageCallback mCallback;
+        protected final int mCallingPid;
         protected final int mCallingUid;
+        protected final String mCallingPackage;
         protected final @NetworkStatsAccess.Level int mAccessLevel;
         protected NetworkStatsRecorder mRecorder;
         protected NetworkStatsCollection mCollection;
 
         RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
             mStatsObserver = statsObserver;
             mRequest = request;
             mCallback = callback;
+            mCallingPid = callingPid;
             mCallingUid = callingUid;
+            mCallingPackage = callingPackage;
             mAccessLevel = accessLevel;
 
             try {
@@ -269,7 +291,8 @@
 
         @Override
         public String toString() {
-            return "RequestInfo from uid:" + mCallingUid
+            return "RequestInfo from pid/uid:" + mCallingPid + "/" + mCallingUid
+                    + "(" + mCallingPackage + ")"
                     + " for " + mRequest + " accessLevel:" + mAccessLevel;
         }
 
@@ -338,9 +361,10 @@
 
     private static class NetworkUsageRequestInfo extends RequestInfo {
         NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
-            super(statsObserver, request, callback, callingUid, accessLevel);
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid, callingPackage,
+                    accessLevel);
         }
 
         @Override
@@ -380,9 +404,10 @@
 
     private static class UserUsageRequestInfo extends RequestInfo {
         UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                    IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
-            super(statsObserver, request, callback, callingUid, accessLevel);
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid,
+                    callingPackage, accessLevel);
         }
 
         @Override
@@ -448,4 +473,10 @@
             mCurrentTime = currentTime;
         }
     }
+
+    public void dump(IndentingPrintWriter pw) {
+        for (int i = 0; i < Math.min(mDataUsageRequests.size(), DUMP_USAGE_REQUESTS_COUNT); i++) {
+            pw.println(mDataUsageRequests.valueAt(i));
+        }
+    }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index f62765d..3da1585 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -21,6 +21,7 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.text.format.DateUtils.YEAR_IN_MILLIS;
 
+import android.annotation.NonNull;
 import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
 import android.net.NetworkStats.NonMonotonicObserver;
@@ -42,7 +43,6 @@
 import libcore.io.IoUtils;
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -68,7 +68,7 @@
 
     private static final String TAG_NETSTATS_DUMP = "netstats_dump";
 
-    /** Dump before deleting in {@link #recoverFromWtf()}. */
+    /** Dump before deleting in {@link #recoverAndDeleteData()}. */
     private static final boolean DUMP_BEFORE_DELETE = true;
 
     private final FileRotator mRotator;
@@ -78,6 +78,7 @@
 
     private final long mBucketDuration;
     private final boolean mOnlyTags;
+    private final boolean mWipeOnError;
 
     private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
     private NetworkStats mLastSnapshot;
@@ -102,6 +103,7 @@
         // slack to avoid overflow
         mBucketDuration = YEAR_IN_MILLIS;
         mOnlyTags = false;
+        mWipeOnError = true;
 
         mPending = null;
         mSinceBoot = new NetworkStatsCollection(mBucketDuration);
@@ -113,7 +115,8 @@
      * Persisted recorder.
      */
     public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
-            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
+            boolean wipeOnError) {
         mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
         mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
         mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -121,6 +124,7 @@
 
         mBucketDuration = bucketDuration;
         mOnlyTags = onlyTags;
+        mWipeOnError = wipeOnError;
 
         mPending = new NetworkStatsCollection(bucketDuration);
         mSinceBoot = new NetworkStatsCollection(bucketDuration);
@@ -156,6 +160,15 @@
         return mSinceBoot;
     }
 
+    public long getBucketDuration() {
+        return mBucketDuration;
+    }
+
+    @NonNull
+    public String getCookie() {
+        return mCookie;
+    }
+
     /**
      * Load complete history represented by {@link FileRotator}. Caches
      * internally as a {@link WeakReference}, and updated with future
@@ -189,10 +202,10 @@
             res.recordCollection(mPending);
         } catch (IOException e) {
             Log.wtf(TAG, "problem completely reading network stats", e);
-            recoverFromWtf();
+            recoverAndDeleteData();
         } catch (OutOfMemoryError e) {
             Log.wtf(TAG, "problem completely reading network stats", e);
-            recoverFromWtf();
+            recoverAndDeleteData();
         }
         return res;
     }
@@ -300,10 +313,10 @@
                 mPending.reset();
             } catch (IOException e) {
                 Log.wtf(TAG, "problem persisting pending stats", e);
-                recoverFromWtf();
+                recoverAndDeleteData();
             } catch (OutOfMemoryError e) {
                 Log.wtf(TAG, "problem persisting pending stats", e);
-                recoverFromWtf();
+                recoverAndDeleteData();
             }
         }
     }
@@ -319,10 +332,10 @@
                 mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
             } catch (IOException e) {
                 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
-                recoverFromWtf();
+                recoverAndDeleteData();
             } catch (OutOfMemoryError e) {
                 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
-                recoverFromWtf();
+                recoverAndDeleteData();
             }
         }
 
@@ -347,8 +360,7 @@
 
     /**
      * Rewriter that will combine current {@link NetworkStatsCollection} values
-     * with anything read from disk, and write combined set to disk. Clears the
-     * original {@link NetworkStatsCollection} when finished writing.
+     * with anything read from disk, and write combined set to disk.
      */
     private static class CombiningRewriter implements FileRotator.Rewriter {
         private final NetworkStatsCollection mCollection;
@@ -375,7 +387,6 @@
         @Override
         public void write(OutputStream out) throws IOException {
             mCollection.write(out);
-            mCollection.reset();
         }
     }
 
@@ -415,43 +426,87 @@
         }
     }
 
-    public void importLegacyNetworkLocked(File file) throws IOException {
-        Objects.requireNonNull(mRotator, "missing FileRotator");
+    /**
+     * Import a specified {@link NetworkStatsCollection} instance into this recorder,
+     * and write it into a standalone file.
+     * @param collection The target {@link NetworkStatsCollection} instance to be imported.
+     */
+    public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
+            throws IOException {
+        if (mRotator != null) {
+            mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
+                    collection.getEndMillis());
+        }
 
-        // legacy file still exists; start empty to avoid double importing
-        mRotator.deleteAll();
-
-        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
-        collection.readLegacyNetwork(file);
-
-        final long startMillis = collection.getStartMillis();
-        final long endMillis = collection.getEndMillis();
-
-        if (!collection.isEmpty()) {
-            // process legacy data, creating active file at starting time, then
-            // using end time to possibly trigger rotation.
-            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
-            mRotator.maybeRotate(endMillis);
+        if (mComplete != null) {
+            throw new IllegalStateException("cannot import data when data already loaded");
         }
     }
 
-    public void importLegacyUidLocked(File file) throws IOException {
-        Objects.requireNonNull(mRotator, "missing FileRotator");
+    /**
+     * Rewriter that will remove any histories or persisted data points before the
+     * specified cutoff time, only writing data back when modified.
+     */
+    public static class RemoveDataBeforeRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mTemp;
+        private final long mCutoffMills;
 
-        // legacy file still exists; start empty to avoid double importing
-        mRotator.deleteAll();
+        public RemoveDataBeforeRewriter(long bucketDuration, long cutoffMills) {
+            mTemp = new NetworkStatsCollection(bucketDuration);
+            mCutoffMills = cutoffMills;
+        }
 
-        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
-        collection.readLegacyUid(file, mOnlyTags);
+        @Override
+        public void reset() {
+            mTemp.reset();
+        }
 
-        final long startMillis = collection.getStartMillis();
-        final long endMillis = collection.getEndMillis();
+        @Override
+        public void read(InputStream in) throws IOException {
+            mTemp.read(in);
+            mTemp.clearDirty();
+            mTemp.removeHistoryBefore(mCutoffMills);
+        }
 
-        if (!collection.isEmpty()) {
-            // process legacy data, creating active file at starting time, then
-            // using end time to possibly trigger rotation.
-            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
-            mRotator.maybeRotate(endMillis);
+        @Override
+        public boolean shouldWrite() {
+            return mTemp.isDirty();
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            mTemp.write(out);
+        }
+    }
+
+    /**
+     * Remove persisted data which contains or is before the cutoff timestamp.
+     */
+    public void removeDataBefore(long cutoffMillis) throws IOException {
+        if (mRotator != null) {
+            try {
+                mRotator.rewriteAll(new RemoveDataBeforeRewriter(
+                        mBucketDuration, cutoffMillis));
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverAndDeleteData();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverAndDeleteData();
+            }
+        }
+
+        // Clean up any pending stats
+        if (mPending != null) {
+            mPending.removeHistoryBefore(cutoffMillis);
+        }
+        if (mSinceBoot != null) {
+            mSinceBoot.removeHistoryBefore(cutoffMillis);
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+        if (complete != null) {
+            complete.removeHistoryBefore(cutoffMillis);
         }
     }
 
@@ -486,9 +541,10 @@
 
     /**
      * Recover from {@link FileRotator} failure by dumping state to
-     * {@link DropBoxManager} and deleting contents.
+     * {@link DropBoxManager} and deleting contents if this recorder
+     * sets {@code mWipeOnError} to true, otherwise keep the contents.
      */
-    private void recoverFromWtf() {
+    void recoverAndDeleteData() {
         if (DUMP_BEFORE_DELETE) {
             final ByteArrayOutputStream os = new ByteArrayOutputStream();
             try {
@@ -501,7 +557,9 @@
             }
             mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
         }
-
-        mRotator.deleteAll();
+        // Delete all files if this recorder is set wipe on error.
+        if (mWipeOnError) {
+            mRotator.deleteAll();
+        }
     }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index e3794e4..424dcd9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -25,6 +25,7 @@
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.EXTRA_UID;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
 import static android.net.NetworkStats.IFACE_ALL;
@@ -67,6 +68,7 @@
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.app.usage.NetworkStatsManager;
+import android.content.ApexEnvironment;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -74,7 +76,10 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsService;
@@ -100,6 +105,7 @@
 import android.net.UnderlyingNetworkInfo;
 import android.net.Uri;
 import android.net.netstats.IUsageCallback;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
 import android.net.netstats.provider.INetworkStatsProvider;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.netstats.provider.NetworkStatsProvider;
@@ -118,6 +124,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.service.NetworkInterfaceProto;
@@ -135,6 +142,7 @@
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FileRotator;
@@ -143,6 +151,7 @@
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.net.module.util.NetworkStatsUtils;
@@ -155,13 +164,19 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.nio.file.Path;
 import java.time.Clock;
+import java.time.Instant;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Semaphore;
@@ -218,17 +233,33 @@
     private static final String NETSTATS_COMBINE_SUBTYPE_ENABLED =
             "netstats_combine_subtype_enabled";
 
-    // This is current path but may be changed soon.
     private static final String UID_COUNTERSET_MAP_PATH =
-            "/sys/fs/bpf/map_netd_uid_counterset_map";
+            "/sys/fs/bpf/netd_shared/map_netd_uid_counterset_map";
     private static final String COOKIE_TAG_MAP_PATH =
-            "/sys/fs/bpf/map_netd_cookie_tag_map";
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
     private static final String APP_UID_STATS_MAP_PATH =
-            "/sys/fs/bpf/map_netd_app_uid_stats_map";
+            "/sys/fs/bpf/netd_shared/map_netd_app_uid_stats_map";
     private static final String STATS_MAP_A_PATH =
-            "/sys/fs/bpf/map_netd_stats_map_A";
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_A";
     private static final String STATS_MAP_B_PATH =
-            "/sys/fs/bpf/map_netd_stats_map_B";
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
+
+    /**
+     * DeviceConfig flag used to indicate whether the files should be stored in the apex data
+     * directory.
+     */
+    static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata";
+    /**
+     * DeviceConfig flag is used to indicate whether the legacy files need to be imported, and
+     * retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA}
+     * set to true. Note that the value gets rollback when the mainline module gets rollback.
+     */
+    static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS =
+            "netstats_import_legacy_target_attempts";
+    static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
+    static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
+    static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
+    static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
 
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
@@ -237,8 +268,7 @@
     private final NetworkStatsSettings mSettings;
     private final NetworkStatsObservers mStatsObservers;
 
-    private final File mSystemDir;
-    private final File mBaseDir;
+    private final File mStatsDir;
 
     private final PowerManager.WakeLock mWakeLock;
 
@@ -248,6 +278,13 @@
     protected INetd mNetd;
     private final AlertObserver mAlertObserver = new AlertObserver();
 
+    // Persistent counters that backed by AtomicFile which stored in the data directory as a file,
+    // to track attempts/successes/fallbacks count across reboot. Note that these counter values
+    // will be rollback as the module rollbacks.
+    private PersistentInt mImportLegacyAttemptsCounter = null;
+    private PersistentInt mImportLegacySuccessesCounter = null;
+    private PersistentInt mImportLegacyFallbacksCounter = null;
+
     @VisibleForTesting
     public static final String ACTION_NETWORK_STATS_POLL =
             "com.android.server.action.NETWORK_STATS_POLL";
@@ -311,11 +348,16 @@
     @GuardedBy("mStatsLock")
     private String mActiveIface;
 
-    /** Set of any ifaces associated with mobile networks since boot. */
+    /** Set of all ifaces currently associated with mobile networks. */
     private volatile String[] mMobileIfaces = new String[0];
 
-    /** Set of any ifaces associated with wifi networks since boot. */
-    private volatile String[] mWifiIfaces = new String[0];
+    /* A set of all interfaces that have ever been associated with mobile networks since boot. */
+    @GuardedBy("mStatsLock")
+    private final Set<String> mAllMobileIfacesSinceBoot = new ArraySet<>();
+
+    /* A set of all interfaces that have ever been associated with wifi networks since boot. */
+    @GuardedBy("mStatsLock")
+    private final Set<String> mAllWifiIfacesSinceBoot = new ArraySet<>();
 
     /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
     @GuardedBy("mStatsLock")
@@ -375,9 +417,19 @@
 
     private long mLastStatsSessionPoll;
 
-    /** Map from UID to number of opened sessions */
-    @GuardedBy("mOpenSessionCallsPerUid")
+    private final Object mOpenSessionCallsLock = new Object();
+    /**
+     * Map from UID to number of opened sessions. This is used for rate-limt an app to open
+     * session frequently
+     */
+    @GuardedBy("mOpenSessionCallsLock")
     private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+    /**
+     * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
+     * the caller of open session and it is only for debugging.
+     */
+    @GuardedBy("mOpenSessionCallsLock")
+    private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
 
     private final static int DUMP_STATS_SESSION_COUNT = 20;
 
@@ -393,21 +445,49 @@
     @NonNull
     private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
 
-    private static @NonNull File getDefaultSystemDir() {
-        return new File(Environment.getDataDirectory(), "system");
-    }
-
-    private static @NonNull File getDefaultBaseDir() {
-        File baseDir = new File(getDefaultSystemDir(), "netstats");
-        baseDir.mkdirs();
-        return baseDir;
-    }
-
     private static @NonNull Clock getDefaultClock() {
         return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
                 Clock.systemUTC());
     }
 
+    /**
+     * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of
+     * the caller.
+     */
+    private static class OpenSessionKey {
+        public final int uid;
+        public final String packageName;
+
+        OpenSessionKey(int uid, @NonNull String packageName) {
+            this.uid = uid;
+            this.packageName = packageName;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append("uid=").append(uid).append(",");
+            sb.append("package=").append(packageName);
+            sb.append("}");
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(@NonNull Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+
+            final OpenSessionKey key = (OpenSessionKey) o;
+            return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, packageName);
+        }
+    }
+
     private final class NetworkStatsHandler extends Handler {
         NetworkStatsHandler(@NonNull Looper looper) {
             super(looper);
@@ -456,8 +536,7 @@
                 INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
                 alarmManager, wakeLock, getDefaultClock(),
                 new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
-                new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
-                new Dependencies());
+                new NetworkStatsObservers(), new Dependencies());
 
         return service;
     }
@@ -467,8 +546,8 @@
     @VisibleForTesting
     NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
             PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
-            NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
-            File baseDir, @NonNull Dependencies deps) {
+            NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
+            @NonNull Dependencies deps) {
         mContext = Objects.requireNonNull(context, "missing Context");
         mNetd = Objects.requireNonNull(netd, "missing Netd");
         mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
@@ -477,9 +556,11 @@
         mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
         mStatsFactory = Objects.requireNonNull(factory, "missing factory");
         mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
-        mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
-        mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
         mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+        mStatsDir = mDeps.getOrCreateStatsDir();
+        if (!mStatsDir.exists()) {
+            throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir);
+        }
 
         final HandlerThread handlerThread = mDeps.makeHandlerThread();
         handlerThread.start();
@@ -506,6 +587,80 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
+         * Get legacy platform stats directory.
+         */
+        @NonNull
+        public File getLegacyStatsDir() {
+            final File systemDataDir = new File(Environment.getDataDirectory(), "system");
+            return new File(systemDataDir, "netstats");
+        }
+
+        /**
+         * Get or create the directory that stores the persisted data usage.
+         */
+        @NonNull
+        public File getOrCreateStatsDir() {
+            final boolean storeInApexDataDir = getStoreFilesInApexData();
+
+            final File statsDataDir;
+            if (storeInApexDataDir) {
+                final File apexDataDir = ApexEnvironment
+                        .getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+                        .getDeviceProtectedDataDir();
+                statsDataDir = new File(apexDataDir, "netstats");
+
+            } else {
+                statsDataDir = getLegacyStatsDir();
+            }
+
+            if (statsDataDir.exists() || statsDataDir.mkdirs()) {
+                return statsDataDir;
+            }
+            throw new IllegalStateException("Cannot write into stats data directory: "
+                    + statsDataDir);
+        }
+
+        /**
+         * Get the count of import legacy target attempts.
+         */
+        public int getImportLegacyTargetAttempts() {
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+                    DeviceConfig.NAMESPACE_TETHERING,
+                    NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
+                    DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
+        }
+
+        /**
+         * Create a persistent counter for given directory and name.
+         */
+        public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
+                throws IOException {
+            // TODO: Modify PersistentInt to call setStartTime every time a write is made.
+            //  Create and pass a real logger here.
+            final String path = dir.resolve(name).toString();
+            return new PersistentInt(path, null /* logger */);
+        }
+
+        /**
+         * Get the flag of storing files in the apex data directory.
+         * @return whether to store files in the apex data directory.
+         */
+        public boolean getStoreFilesInApexData() {
+            return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                    DeviceConfig.NAMESPACE_TETHERING,
+                    NETSTATS_STORE_FILES_IN_APEXDATA, true);
+        }
+
+        /**
+         * Read legacy persisted network stats from disk.
+         */
+        @NonNull
+        public NetworkStatsCollection readPlatformCollection(
+                @NonNull String prefix, long bucketDuration) throws IOException {
+            return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration);
+        }
+
+        /**
          * Create a HandlerThread to use in NetworkStatsService.
          */
         @NonNull
@@ -613,6 +768,11 @@
                 return null;
             }
         }
+
+        /** Gets whether the build is userdebug. */
+        public boolean isDebuggable() {
+            return Build.isDebuggable();
+        }
     }
 
     /**
@@ -640,14 +800,18 @@
             mSystemReady = true;
 
             // create data recorders along with historical rotators
-            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
-            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false);
-            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
-            mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
+            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+                    mStatsDir, true /* wipeOnError */);
 
             updatePersistThresholdsLocked();
 
-            // upgrade any legacy stats, migrating them to rotated files
+            // upgrade any legacy stats
             maybeUpgradeLegacyStatsLocked();
 
             // read historical network stats from disk, since policy service
@@ -707,12 +871,14 @@
     }
 
     private NetworkStatsRecorder buildRecorder(
-            String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+            String prefix, NetworkStatsSettings.Config config, boolean includeTags,
+            File baseDir, boolean wipeOnError) {
         final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
                 Context.DROPBOX_SERVICE);
         return new NetworkStatsRecorder(new FileRotator(
-                mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+                baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
+                wipeOnError);
     }
 
     @GuardedBy("mStatsLock")
@@ -741,32 +907,371 @@
         mSystemReady = false;
     }
 
+    private static class MigrationInfo {
+        public final NetworkStatsRecorder recorder;
+        public NetworkStatsCollection collection;
+        public boolean imported;
+        MigrationInfo(@NonNull final NetworkStatsRecorder recorder) {
+            this.recorder = recorder;
+            collection = null;
+            imported = false;
+        }
+    }
+
     @GuardedBy("mStatsLock")
     private void maybeUpgradeLegacyStatsLocked() {
-        File file;
-        try {
-            file = new File(mSystemDir, "netstats.bin");
-            if (file.exists()) {
-                mDevRecorder.importLegacyNetworkLocked(file);
-                file.delete();
-            }
-
-            file = new File(mSystemDir, "netstats_xt.bin");
-            if (file.exists()) {
-                file.delete();
-            }
-
-            file = new File(mSystemDir, "netstats_uid.bin");
-            if (file.exists()) {
-                mUidRecorder.importLegacyUidLocked(file);
-                mUidTagRecorder.importLegacyUidLocked(file);
-                file.delete();
-            }
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem during legacy upgrade", e);
-        } catch (OutOfMemoryError e) {
-            Log.wtf(TAG, "problem during legacy upgrade", e);
+        final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
+        if (!storeFilesInApexData) {
+            return;
         }
+        try {
+            mImportLegacyAttemptsCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+                    NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME);
+            mImportLegacySuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+                    NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME);
+            mImportLegacyFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+                    NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+            return;
+        }
+
+        final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
+        final int attempts;
+        final int fallbacks;
+        final boolean runComparison;
+        try {
+            attempts = mImportLegacyAttemptsCounter.get();
+            // Fallbacks counter would be set to non-zero value to indicate the migration was
+            // not successful.
+            fallbacks = mImportLegacyFallbacksCounter.get();
+            runComparison = shouldRunComparison();
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to read counters, skip.", e);
+            return;
+        }
+
+        // If the target number of attempts are reached, don't import any data.
+        // However, if comparison is requested, still read the legacy data and compare
+        // it to the importer output. This allows OEMs to debug issues with the
+        // importer code and to collect signals from the field.
+        final boolean dryRunImportOnly =
+                fallbacks != 0 && runComparison && (attempts >= targetAttempts);
+        // Return if target attempts are reached and there is no need to dry run.
+        if (attempts >= targetAttempts && !dryRunImportOnly) return;
+
+        if (dryRunImportOnly) {
+            Log.i(TAG, "Starting import : only perform read");
+        } else {
+            Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+        }
+
+        final MigrationInfo[] migrations = new MigrationInfo[]{
+                new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
+                new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
+        };
+
+        // Legacy directories will be created by recorders if they do not exist
+        final NetworkStatsRecorder[] legacyRecorders;
+        if (runComparison) {
+            final File legacyBaseDir = mDeps.getLegacyStatsDir();
+            // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+            // failed and calling deleteAll.
+            legacyRecorders = new NetworkStatsRecorder[]{
+                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
+                        false /* wipeOnError */)};
+        } else {
+            legacyRecorders = null;
+        }
+
+        long migrationEndTime = Long.MIN_VALUE;
+        try {
+            // First, read all legacy collections. This is OEM code and it can throw. Don't
+            // commit any data to disk until all are read.
+            for (int i = 0; i < migrations.length; i++) {
+                final MigrationInfo migration = migrations[i];
+
+                // Read the collection from platform code, and set fallbacks counter if throws
+                // for better debugging.
+                try {
+                    migration.collection = readPlatformCollectionForRecorder(migration.recorder);
+                } catch (Throwable e) {
+                    if (dryRunImportOnly) {
+                        Log.wtf(TAG, "Platform data read failed. ", e);
+                        return;
+                    } else {
+                        // Data is not imported successfully, set fallbacks counter to non-zero
+                        // value to trigger dry run every later boot when the runComparison is
+                        // true, in order to make it easier to debug issues.
+                        tryIncrementLegacyFallbacksCounter();
+                        // Re-throw for error handling. This will increase attempts counter.
+                        throw e;
+                    }
+                }
+
+                if (runComparison) {
+                    final boolean success =
+                            compareImportedToLegacyStats(migration, legacyRecorders[i]);
+                    if (!success && !dryRunImportOnly) {
+                        tryIncrementLegacyFallbacksCounter();
+                    }
+                }
+            }
+
+            // For cases where the fallbacks are not zero but target attempts counts reached,
+            // only perform reads above and return here.
+            if (dryRunImportOnly) return;
+
+            // Find the latest end time.
+            for (final MigrationInfo migration : migrations) {
+                final long migrationEnd = migration.collection.getEndMillis();
+                if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd;
+            }
+
+            // Reading all collections from legacy data has succeeded. At this point it is
+            // safe to start overwriting the files on disk. The next step is to remove all
+            // data in the new location that overlaps with imported data. This ensures that
+            // any data in the new location that was created by a previous failed import is
+            // ignored. After that, write the imported data into the recorder. The code
+            // below can still possibly throw (disk error or OutOfMemory for example), but
+            // does not depend on code from non-mainline code.
+            Log.i(TAG, "Rewriting data with imported collections with cutoff "
+                    + Instant.ofEpochMilli(migrationEndTime));
+            for (final MigrationInfo migration : migrations) {
+                migration.imported = true;
+                migration.recorder.removeDataBefore(migrationEndTime);
+                if (migration.collection.isEmpty()) continue;
+                migration.recorder.importCollectionLocked(migration.collection);
+            }
+
+            // Success normally or uses fallback method.
+        } catch (Throwable e) {
+            // The code above calls OEM code that may behave differently across devices.
+            // It can throw any exception including RuntimeExceptions and
+            // OutOfMemoryErrors. Try to recover anyway.
+            Log.wtf(TAG, "Platform data import failed. Remaining tries "
+                    + (targetAttempts - attempts), e);
+
+            // Failed this time around : try again next time unless we're out of tries.
+            try {
+                mImportLegacyAttemptsCounter.set(attempts + 1);
+            } catch (IOException ex) {
+                Log.wtf(TAG, "Failed to update attempts counter.", ex);
+            }
+
+            // Try to remove any data from the failed import.
+            if (migrationEndTime > Long.MIN_VALUE) {
+                try {
+                    for (final MigrationInfo migration : migrations) {
+                        if (migration.imported) {
+                            migration.recorder.removeDataBefore(migrationEndTime);
+                        }
+                    }
+                } catch (Throwable f) {
+                    // If rollback still throws, there isn't much left to do. Try nuking
+                    // all data, since that's the last stop. If nuking still throws, the
+                    // framework will reboot, and if there are remaining tries, the migration
+                    // process will retry, which is fine because it's idempotent.
+                    for (final MigrationInfo migration : migrations) {
+                        migration.recorder.recoverAndDeleteData();
+                    }
+                }
+            }
+
+            return;
+        }
+
+        // Success ! No need to import again next time.
+        try {
+            mImportLegacyAttemptsCounter.set(targetAttempts);
+            Log.i(TAG, "Successfully imported platform collections");
+            // The successes counter is only for debugging. Hence, the synchronization
+            // between successes counter and attempts counter are not very critical.
+            final int successCount = mImportLegacySuccessesCounter.get();
+            mImportLegacySuccessesCounter.set(successCount + 1);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Succeed but failed to update counters.", e);
+        }
+    }
+
+    void tryIncrementLegacyFallbacksCounter() {
+        try {
+            final int fallbacks = mImportLegacyFallbacksCounter.get();
+            mImportLegacyFallbacksCounter.set(fallbacks + 1);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to update fallback counter.", e);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldRunComparison() {
+        final ConnectivityResources resources = new ConnectivityResources(mContext);
+        // 0 if id not found.
+        Boolean overlayValue = null;
+        try {
+            switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) {
+                case 1:
+                    overlayValue = Boolean.TRUE;
+                    break;
+                case 0:
+                    overlayValue = Boolean.FALSE;
+                    break;
+            }
+        } catch (Resources.NotFoundException e) {
+            // Overlay value is not defined.
+        }
+        return overlayValue != null ? overlayValue : mDeps.isDebuggable();
+    }
+
+    /**
+     * Compare imported data with the data returned by legacy recorders.
+     *
+     * @return true if the data matches, false if the data does not match or throw with exceptions.
+     */
+    private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
+            @NonNull NetworkStatsRecorder legacyRecorder) {
+        final NetworkStatsCollection legacyStats;
+        try {
+            legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to read stats with legacy method for recorder "
+                    + legacyRecorder.getCookie(), e);
+            // Cannot read data from legacy method, skip comparison.
+            return false;
+        }
+
+        // The result of comparison is only for logging.
+        try {
+            final String error = compareStats(migration.collection, legacyStats);
+            if (error != null) {
+                Log.wtf(TAG, "Unexpected comparison result for recorder "
+                        + legacyRecorder.getCookie() + ": " + error);
+                return false;
+            }
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
+                    + legacyRecorder.getCookie(), e);
+            return false;
+        }
+        return true;
+    }
+
+    private static String str(NetworkStatsCollection.Key key) {
+        StringBuilder sb = new StringBuilder()
+                .append(key.ident.toString())
+                .append(" uid=").append(key.uid);
+        if (key.set != SET_FOREGROUND) {
+            sb.append(" set=").append(key.set);
+        }
+        if (key.tag != 0) {
+            sb.append(" tag=").append(key.tag);
+        }
+        return sb.toString();
+    }
+
+    // The importer will modify some keys when importing them.
+    // In order to keep the comparison code simple, add such special cases here and simply
+    // ignore them. This should not impact fidelity much because the start/end checks and the total
+    // bytes check still need to pass.
+    private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+        if (key.ident.isEmpty()) return false;
+        final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+        // Non-mobile network with non-empty RAT type.
+        // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+        // in, but it looks like it was previously possible to persist it to disk. The importer sets
+        // the RAT type to NETWORK_TYPE_ALL.
+        if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+                && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Nullable
+    private static String compareStats(
+            NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+                migrated.getEntries();
+        final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+        final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+                new ArraySet<>(legEntries.keySet());
+
+        for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+            final NetworkStatsHistory legHistory = legEntries.get(legKey);
+            final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+            if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+                unmatchedLegKeys.remove(legKey);
+                continue;
+            }
+
+            if (migHistory == null) {
+                return "Missing migrated history for legacy key " + str(legKey)
+                        + ", legacy history was " + legHistory;
+            }
+            if (!migHistory.isSameAs(legHistory)) {
+                return "Difference in history for key " + legKey + "; legacy history " + legHistory
+                        + ", migrated history " + migHistory;
+            }
+            unmatchedLegKeys.remove(legKey);
+        }
+
+        if (!unmatchedLegKeys.isEmpty()) {
+            final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+            return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+                    + ", first unmatched collection " + first;
+        }
+
+        if (migrated.getStartMillis() != legacy.getStartMillis()
+                || migrated.getEndMillis() != legacy.getEndMillis()) {
+            return "Start / end of the collections "
+                    + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+                    + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+                    + " don't match";
+        }
+
+        if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+            return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+                    + " don't match for collections with start/end "
+                    + migrated.getStartMillis()
+                    + "/" + legacy.getStartMillis();
+        }
+
+        return null;
+    }
+
+    @GuardedBy("mStatsLock")
+    @NonNull
+    private NetworkStatsCollection readPlatformCollectionForRecorder(
+            @NonNull final NetworkStatsRecorder rec) throws IOException {
+        final String prefix = rec.getCookie();
+        Log.i(TAG, "Importing platform collection for prefix " + prefix);
+        final NetworkStatsCollection collection = Objects.requireNonNull(
+                mDeps.readPlatformCollection(prefix, rec.getBucketDuration()),
+                "Imported platform collection for prefix " + prefix + " must not be null");
+
+        final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+        if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) {
+            throw new IllegalArgumentException("Platform collection for prefix " + prefix
+                    + " contains data that could not possibly come from the previous boot "
+                    + "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis())
+                    + ", last booted at " + Instant.ofEpochMilli(bootTimestamp));
+        }
+
+        Log.i(TAG, "Successfully read platform collection spanning from "
+                // Instant uses ISO-8601 for toString()
+                + Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to "
+                + Instant.ofEpochMilli(collection.getEndMillis()).toString());
+        return collection;
     }
 
     /**
@@ -795,16 +1300,27 @@
         return openSessionInternal(flags, callingPackage);
     }
 
-    private boolean isRateLimitedForPoll(int callingUid) {
-        if (callingUid == android.os.Process.SYSTEM_UID) {
-            return false;
-        }
-
+    private boolean isRateLimitedForPoll(@NonNull OpenSessionKey key) {
         final long lastCallTime;
         final long now = SystemClock.elapsedRealtime();
-        synchronized (mOpenSessionCallsPerUid) {
-            int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
-            mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+
+        synchronized (mOpenSessionCallsLock) {
+            Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key);
+            if (callsPerCaller == null) {
+                mOpenSessionCallsPerCaller.put((key), 1);
+            } else {
+                mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1));
+            }
+
+            int callsPerUid = mOpenSessionCallsPerUid.get(key.uid, 0);
+            mOpenSessionCallsPerUid.put(key.uid, callsPerUid + 1);
+
+            if (key.uid == android.os.Process.SYSTEM_UID) {
+                return false;
+            }
+
+            // To avoid a non-system user to be rate-limited after system users open sessions,
+            // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID.
             lastCallTime = mLastStatsSessionPoll;
             mLastStatsSessionPoll = now;
         }
@@ -812,7 +1328,7 @@
         return now - lastCallTime < POLL_RATE_LIMIT_MS;
     }
 
-    private int restrictFlagsForCaller(int flags) {
+    private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
         // All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
         final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -822,14 +1338,15 @@
         }
         // Non-system uids are rate limited for POLL_ON_OPEN.
         final int callingUid = Binder.getCallingUid();
-        flags = isRateLimitedForPoll(callingUid)
+        final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage);
+        flags = isRateLimitedForPoll(key)
                 ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
                 : flags;
         return flags;
     }
 
     private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
-        final int restrictedFlags = restrictFlagsForCaller(flags);
+        final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
         if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
                 | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
             final long ident = Binder.clearCallingIdentity();
@@ -1038,9 +1555,9 @@
         // We've been using pure XT stats long enough that we no longer need to
         // splice DEV and XT together.
         final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL,
-                accessLevel, callingUid, start, end);
+                accessLevel, callingUid, Long.MIN_VALUE, Long.MAX_VALUE);
 
-        final long now = System.currentTimeMillis();
+        final long now = mClock.millis();
         final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
 
         final NetworkStats stats = new NetworkStats(end - start, 1);
@@ -1121,17 +1638,37 @@
         return dataLayer;
     }
 
+    private String[] getAllIfacesSinceBoot(int transport) {
+        synchronized (mStatsLock) {
+            final Set<String> ifaceSet;
+            if (transport == TRANSPORT_WIFI) {
+                ifaceSet = mAllWifiIfacesSinceBoot;
+            } else if (transport == TRANSPORT_CELLULAR) {
+                ifaceSet = mAllMobileIfacesSinceBoot;
+            } else {
+                throw new IllegalArgumentException("Invalid transport " + transport);
+            }
+
+            return ifaceSet.toArray(new String[0]);
+        }
+    }
+
     @Override
     public NetworkStats getUidStatsForTransport(int transport) {
         PermissionUtils.enforceNetworkStackPermission(mContext);
         try {
-            final String[] relevantIfaces =
-                    transport == TRANSPORT_WIFI ? mWifiIfaces : mMobileIfaces;
+            final String[] ifaceArray = getAllIfacesSinceBoot(transport);
             // TODO(b/215633405) : mMobileIfaces and mWifiIfaces already contain the stacked
             // interfaces, so this is not useful, remove it.
             final String[] ifacesToQuery =
-                    mStatsFactory.augmentWithStackedInterfaces(relevantIfaces);
-            return getNetworkStatsUidDetail(ifacesToQuery);
+                    mStatsFactory.augmentWithStackedInterfaces(ifaceArray);
+            final NetworkStats stats = getNetworkStatsUidDetail(ifacesToQuery);
+            // Clear the interfaces of the stats before returning, so callers won't get this
+            // information. This is because no caller needs this information for now, and it
+            // makes it easier to change the implementation later by using the histories in the
+            // recorder.
+            stats.clearInterfaces();
+            return stats;
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error compiling UID stats", e);
             return new NetworkStats(0L, 0);
@@ -1140,11 +1677,6 @@
 
     @Override
     public String[] getMobileIfaces() {
-        // TODO (b/192758557): Remove debug log.
-        if (CollectionUtils.contains(mMobileIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
-        }
         return mMobileIfaces.clone();
     }
 
@@ -1280,13 +1812,14 @@
         Objects.requireNonNull(request.template, "NetworkTemplate is null");
         Objects.requireNonNull(callback, "callback is null");
 
-        int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
         @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
         DataUsageRequest normalizedRequest;
         final long token = Binder.clearCallingIdentity();
         try {
             normalizedRequest = mStatsObservers.register(mContext,
-                    request, callback, callingUid, accessLevel);
+                    request, callback, callingPid, callingUid, callingPackage, accessLevel);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -1511,7 +2044,6 @@
 
         final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled();
         final ArraySet<String> mobileIfaces = new ArraySet<>();
-        final ArraySet<String> wifiIfaces = new ArraySet<>();
         for (NetworkStateSnapshot snapshot : snapshots) {
             final int displayTransport =
                     getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
@@ -1556,9 +2088,12 @@
 
                 if (isMobile) {
                     mobileIfaces.add(baseIface);
+                    // If the interface name was present in the wifi set, the interface won't
+                    // be removed from it to prevent stats from getting rollback.
+                    mAllMobileIfacesSinceBoot.add(baseIface);
                 }
                 if (isWifi) {
-                    wifiIfaces.add(baseIface);
+                    mAllWifiIfacesSinceBoot.add(baseIface);
                 }
             }
 
@@ -1600,9 +2135,10 @@
                     findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
                     if (isMobile) {
                         mobileIfaces.add(iface);
+                        mAllMobileIfacesSinceBoot.add(iface);
                     }
                     if (isWifi) {
-                        wifiIfaces.add(iface);
+                        mAllWifiIfacesSinceBoot.add(iface);
                     }
 
                     mStatsFactory.noteStackedIface(iface, baseIface);
@@ -1611,16 +2147,6 @@
         }
 
         mMobileIfaces = mobileIfaces.toArray(new String[0]);
-        mWifiIfaces = wifiIfaces.toArray(new String[0]);
-        // TODO (b/192758557): Remove debug log.
-        if (CollectionUtils.contains(mMobileIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
-        }
-        if (CollectionUtils.contains(mWifiIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mWifiIfaces: " + Arrays.toString(mWifiIfaces));
-        }
     }
 
     private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
@@ -1938,6 +2464,9 @@
         for (int uid : uids) {
             deleteKernelTagData(uid);
         }
+
+       // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+       // mOpenSessionCallsPerCaller
     }
 
     /**
@@ -2036,10 +2565,35 @@
                 return;
             }
 
+            pw.println("Directory:");
+            pw.increaseIndent();
+            pw.println(mStatsDir);
+            pw.decreaseIndent();
+
             pw.println("Configs:");
             pw.increaseIndent();
             pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
             pw.println();
+            pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData());
+            pw.println();
+            pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts());
+            pw.println();
+            if (mDeps.getStoreFilesInApexData()) {
+                try {
+                    pw.print("platform legacy stats import attempts count",
+                            mImportLegacyAttemptsCounter.get());
+                    pw.println();
+                    pw.print("platform legacy stats import successes count",
+                            mImportLegacySuccessesCounter.get());
+                    pw.println();
+                    pw.print("platform legacy stats import fallbacks count",
+                            mImportLegacyFallbacksCounter.get());
+                    pw.println();
+                } catch (IOException e) {
+                    pw.println("(failed to dump platform legacy stats import counters)");
+                }
+            }
+
             pw.decreaseIndent();
 
             pw.println("Active interfaces:");
@@ -2060,26 +2614,38 @@
             }
             pw.decreaseIndent();
 
-            // Get the top openSession callers
-            final SparseIntArray calls;
-            synchronized (mOpenSessionCallsPerUid) {
-                calls = mOpenSessionCallsPerUid.clone();
-            }
-
-            final int N = calls.size();
-            final long[] values = new long[N];
-            for (int j = 0; j < N; j++) {
-                values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
-            }
-            Arrays.sort(values);
-
-            pw.println("Top openSession callers (uid=count):");
+            pw.println("All wifi interfaces:");
             pw.increaseIndent();
-            final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
-            for (int j = N - 1; j >= end; j--) {
-                final int uid = (int) (values[j] & 0xffffffff);
-                final int count = (int) (values[j] >> 32);
-                pw.print(uid); pw.print("="); pw.println(count);
+            for (String iface : mAllWifiIfacesSinceBoot) {
+                pw.print(iface + " ");
+            }
+            pw.println();
+            pw.decreaseIndent();
+
+            pw.println("All mobile interfaces:");
+            pw.increaseIndent();
+            for (String iface : mAllMobileIfacesSinceBoot) {
+                pw.print(iface + " ");
+            }
+            pw.println();
+            pw.decreaseIndent();
+
+            // Get the top openSession callers
+            final HashMap calls;
+            synchronized (mOpenSessionCallsLock) {
+                calls = new HashMap<>(mOpenSessionCallsPerCaller);
+            }
+            final List<Map.Entry<OpenSessionKey, Integer>> list = new ArrayList<>(calls.entrySet());
+            Collections.sort(list,
+                    (left, right) -> Integer.compare(left.getValue(), right.getValue()));
+            final int num = list.size();
+            final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT);
+            pw.println("Top openSession callers:");
+            pw.increaseIndent();
+            for (int j = num - 1; j >= end; j--) {
+                final Map.Entry<OpenSessionKey, Integer> entry = list.get(j);
+                pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+
             }
             pw.decreaseIndent();
             pw.println();
@@ -2099,6 +2665,13 @@
                 }
             });
             pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Stats Observers:");
+            pw.increaseIndent();
+            mStatsObservers.dump(pw);
+            pw.decreaseIndent();
+            pw.println();
 
             pw.println("Dev stats:");
             pw.increaseIndent();
diff --git a/service-t/src/com/android/server/net/PersistentInt.java b/service-t/src/com/android/server/net/PersistentInt.java
new file mode 100644
index 0000000..c212b77
--- /dev/null
+++ b/service-t/src/com/android/server/net/PersistentInt.java
@@ -0,0 +1,108 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.AtomicFile;
+import android.util.SystemConfigFileCommitEventLogger;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * A simple integer backed by an on-disk {@link AtomicFile}. Not thread-safe.
+ */
+public class PersistentInt {
+    private final String mPath;
+    private final AtomicFile mFile;
+
+    /**
+     * Constructs a new {@code PersistentInt}. The counter is set to 0 if the file does not exist.
+     * Before returning, the constructor checks that the file is readable and writable. This
+     * indicates that in the future {@link #get} and {@link #set} are likely to succeed,
+     * though other events (data corruption, other code deleting the file, etc.) may cause these
+     * calls to fail in the future.
+     *
+     * @param path the path of the file to use.
+     * @param logger the logger
+     * @throws IOException the counter could not be read or written
+     */
+    public PersistentInt(@NonNull String path, @Nullable SystemConfigFileCommitEventLogger logger)
+            throws IOException {
+        mPath = path;
+        mFile = new AtomicFile(new File(path), logger);
+        checkReadWrite();
+    }
+
+    private void checkReadWrite() throws IOException {
+        int value;
+        try {
+            value = get();
+        } catch (FileNotFoundException e) {
+            // Counter does not exist. Attempt to initialize to 0.
+            // Note that we cannot tell here if the file does not exist or if opening it failed,
+            // because in Java both of those throw FileNotFoundException.
+            value = 0;
+        }
+        set(value);
+        get();
+        // No exceptions? Good.
+    }
+
+    /**
+      * Gets the current value.
+      *
+      * @return the current value of the counter.
+      * @throws IOException if reading the value failed.
+      */
+    public int get() throws IOException {
+        try (FileInputStream fin = mFile.openRead();
+             DataInputStream din = new DataInputStream(fin)) {
+            return din.readInt();
+        }
+    }
+
+    /**
+     * Sets the current value.
+     * @param value the value to set
+     * @throws IOException if writing the value failed.
+     */
+    public void set(int value) throws IOException {
+        FileOutputStream fout = null;
+        try {
+            fout = mFile.startWrite();
+            DataOutputStream dout = new DataOutputStream(fout);
+            dout.writeInt(value);
+            mFile.finishWrite(fout);
+        } catch (IOException e) {
+            if (fout != null) {
+                mFile.failWrite(fout);
+            }
+            throw e;
+        }
+    }
+
+    public String getPath() {
+        return mPath;
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
index 25b970a..7a4fb33 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -158,6 +158,7 @@
     static_libs: [
         // Do not add libs here if they are already included
         // in framework-connectivity
+        "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
         "dnsresolver_aidl_interface-V9-java",
         "modules-utils-shell-command-handler",
@@ -169,7 +170,7 @@
         "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
-        "NetworkStackApiCurrentShims",
+        "NetworkStackApiStableShims",
     ],
     apex_available: [
         "com.android.tethering",
@@ -181,6 +182,25 @@
     ],
 }
 
+// TODO: Remove this temporary library and put code into module when test coverage is enough.
+java_library {
+    name: "service-mdns",
+    sdk_version: "system_server_current",
+    min_sdk_version: "30",
+    srcs: [
+        "mdns/**/*.java",
+    ],
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity-pre-jarjar",
+        "framework-wifi.stubs.module_lib",
+        "service-connectivity-pre-jarjar",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity/tests:__subpackages__",
+    ],
+}
+
 java_library {
     name: "service-connectivity-protos",
     sdk_version: "system_current",
@@ -198,11 +218,10 @@
     lint: { strict_updatability_linting: true },
 }
 
-java_library {
-    name: "service-connectivity",
+java_defaults {
+    name: "service-connectivity-defaults",
     sdk_version: "system_server_current",
     min_sdk_version: "30",
-    installable: true,
     // This library combines system server jars that have access to different bootclasspath jars.
     // Lower SDK service jars must not depend on higher SDK jars as that would let them
     // transitively depend on the wrong bootclasspath jars. Sources also cannot be added here as
@@ -216,9 +235,32 @@
     apex_available: [
         "com.android.tethering",
     ],
+    optimize: {
+        enabled: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
+    },
     lint: { strict_updatability_linting: true },
 }
 
+// A special library created strictly for use by the tests as they need the
+// implementation library but that is not available when building from prebuilts.
+// Using a library with a different name to what is used by the prebuilts ensures
+// that this will never depend on the prebuilt.
+// Switching service-connectivity to a java_sdk_library would also have worked as
+// that has built in support for managing this but that is too big a change at this
+// point.
+java_library {
+    name: "service-connectivity-for-tests",
+    defaults: ["service-connectivity-defaults"],
+}
+
+java_library {
+    name: "service-connectivity",
+    defaults: ["service-connectivity-defaults"],
+    installable: true,
+}
+
 filegroup {
     name: "connectivity-jarjar-rules",
     srcs: ["jarjar-rules.txt"],
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index f491cc7..02b2875 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -23,6 +23,7 @@
     name: "ServiceConnectivityResources",
     sdk_version: "module_30",
     min_sdk_version: "30",
+    target_sdk_version: "33",
     resource_dirs: [
         "res",
     ],
diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml
index 385c75c..85bd84f 100644
--- a/service/ServiceConnectivityResources/res/values-sq/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml
@@ -35,7 +35,7 @@
   <string-array name="network_switch_type_name">
     <item msgid="3004933964374161223">"të dhënat celulare"</item>
     <item msgid="5624324321165953608">"Wi-Fi"</item>
-    <item msgid="5667906231066981731">"Bluetooth"</item>
+    <item msgid="5667906231066981731">"Bluetooth-i"</item>
     <item msgid="346574747471703768">"Eternet"</item>
     <item msgid="5734728378097476003">"VPN"</item>
   </string-array>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 81782f9..bff6953 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -179,4 +179,13 @@
     Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
     to ensure that apps see the network disconnect and reconnect. -->
     <integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
+
+    <!-- Whether the network stats service should run compare on the result of
+    {@link NetworkStatsDataMigrationUtils#readPlatformCollection} and the result
+    of reading from legacy recorders. Possible values are:
+      0 = never compare,
+      1 = always compare,
+      2 = compare on debuggable builds (default value)
+      -->
+    <integer translatable="false" name="config_netstats_validate_import">2</integer>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index b92dd08..3389d63 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -41,6 +41,7 @@
             <item type="array" name="config_ethernet_interfaces"/>
             <item type="string" name="config_ethernet_iface_regex"/>
             <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
+            <item type="integer" name="config_netstats_validate_import" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
new file mode 100644
index 0000000..b0d6763
--- /dev/null
+++ b/service/jarjar-excludes.txt
@@ -0,0 +1,9 @@
+# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
+com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
+
+# Do not jarjar com.android.server, as several unit tests fail because they lose
+# package-private visibility between jarjared and non-jarjared classes.
+# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
+# is specific to the module like com.android.server.connectivity
+com\.android\.server\..+
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index 4b21569..1ad75e3 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -92,7 +92,6 @@
 rule android.net.util.KeepalivePacketDataUtil* com.android.connectivity.@0
 
 # From connectivity-module-utils
-rule android.net.util.SharedLog* com.android.connectivity.@0
 rule android.net.shared.** com.android.connectivity.@0
 
 # From services-connectivity-shared-srcs
@@ -111,5 +110,14 @@
 # From mdns-aidl-interface
 rule android.net.mdns.aidl.** android.net.connectivity.@0
 
+# From nearby-service, including proto
+rule service.proto.** com.android.server.nearby.@0
+rule androidx.annotation.Keep* com.android.server.nearby.@0
+rule androidx.collection.** com.android.server.nearby.@0
+rule androidx.core.** com.android.server.nearby.@0
+rule androidx.versionedparcelable.** com.android.server.nearby.@0
+rule com.google.common.** com.android.server.nearby.@0
+rule android.support.v4.** com.android.server.nearby.@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_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index f13c68d..49392e0 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -39,148 +39,125 @@
 
 namespace android {
 
-static void native_init(JNIEnv* env, jobject clazz) {
+#define CHECK_LOG(status) \
+  do { \
+    if (!isOk(status)) \
+      ALOGE("%s failed, error code = %d", __func__, status.code()); \
+  } while (0)
+
+static void native_init(JNIEnv* env, jclass clazz) {
   Status status = mTc.start();
-   if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
 }
 
-static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGD("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
-  auto chain = static_cast<ChildChain>(childChain);
-  int res = mTc.toggleUidOwnerMap(chain, enable);
-  if (res) {
-    ALOGE("%s failed, error code = %d", __func__, res);
-  }
-  return (jint)res;
-}
-
-static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
-                                jintArray jUids) {
+static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
+                                   jintArray jUids) {
     const ScopedUtfChars chainNameUtf8(env, name);
-    if (chainNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
-    }
+    if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
     const std::string chainName(chainNameUtf8.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
-    if (res) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
-                          jint firewallRule) {
+static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
+                              jint firewallRule) {
     auto chain = static_cast<ChildChain>(childChain);
     auto rule = static_cast<FirewallRule>(firewallRule);
     FirewallType fType = mTc.getFirewallType(chain);
 
     int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
-    if (res) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
-                                    jintArray jUids) {
-    const ScopedUtfChars ifNameUtf8(env, ifName);
-    if (ifNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
+                                        jintArray jUids) {
+    // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
+    // set to 0.
+    int ifIndex = 0;
+    if (ifName != nullptr) {
+        const ScopedUtfChars ifNameUtf8(env, ifName);
+        const std::string interfaceName(ifNameUtf8.c_str());
+        ifIndex = if_nametoindex(interfaceName.c_str());
     }
-    const std::string interfaceName(ifNameUtf8.c_str());
-    const int ifIndex = if_nametoindex(interfaceName.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.addUidInterfaceRules(ifIndex, data);
-    if (!isOk(status)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.removeUidInterfaceRules(data);
-    if (!isOk(status)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
+    Status status = mTc.updateUidLockdownRule(uid, add);
+    CHECK_LOG(status);
+    return (jint)status.code();
+}
+
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
     Status status = mTc.swapActiveStatsMap();
-    if (!isOk(status)) {
-        ALOGD("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
-                                      jintArray jUids) {
+static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
+                                        jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
     if (uids.get() == nullptr) return;
 
@@ -190,7 +167,7 @@
     mTc.setPermissionForUids(permission, data);
 }
 
-static void native_dump(JNIEnv* env, jobject clazz, jobject javaFd, jboolean verbose) {
+static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
     int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (fd < 0) {
         jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
@@ -215,8 +192,6 @@
     (void*)native_addNiceApp},
     {"native_removeNiceApp", "(I)I",
     (void*)native_removeNiceApp},
-    {"native_setChildChain", "(IZ)I",
-    (void*)native_setChildChain},
     {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
     (void*)native_replaceUidChain},
     {"native_setUidRule", "(III)I",
@@ -225,6 +200,8 @@
     (void*)native_addUidInterfaceRules},
     {"native_removeUidInterfaceRules", "([I)I",
     (void*)native_removeUidInterfaceRules},
+    {"native_updateUidLockdownRule", "(IZ)I",
+    (void*)native_updateUidLockdownRule},
     {"native_swapActiveStatsMap", "()I",
     (void*)native_swapActiveStatsMap},
     {"native_setPermissionForUids", "(I[I)V",
@@ -235,9 +212,8 @@
 // clang-format on
 
 int register_com_android_server_BpfNetMaps(JNIEnv* env) {
-    return jniRegisterNativeMethods(env,
-    "com/android/server/BpfNetMaps",
-    gMethods, NELEM(gMethods));
+    return jniRegisterNativeMethods(env, "com/android/server/BpfNetMaps",
+                                    gMethods, NELEM(gMethods));
 }
 
 }; // namespace android
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 4efd0e1..9c7a761 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -51,7 +51,15 @@
     jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
 }
 
-static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+// enable or disable  carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
+    uint32_t carrierOn = enabled;
+    if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+        throwException(env, errno, "set carrier", iface);
+    }
+}
+
+static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, const char* iface) {
     base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
     ifreq ifr{};
 
@@ -63,6 +71,11 @@
         return -1;
     }
 
+    if (!hasCarrier) {
+        // disable carrier before setting IFF_UP
+        setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
+    }
+
     // Activate interface using an unconnected datagram socket.
     base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
     ifr.ifr_flags = IFF_UP;
@@ -79,23 +92,31 @@
 
 //------------------------------------------------------------------------------
 
-static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
+                                    jIface, jint tunFd, jboolean enabled) {
+    ScopedUtfChars iface(env, jIface);
+    if (!iface.c_str()) {
+        jniThrowNullPointerException(env, "iface");
+    }
+    setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
+                             jboolean hasCarrier, jstring jIface) {
     ScopedUtfChars iface(env, jIface);
     if (!iface.c_str()) {
         jniThrowNullPointerException(env, "iface");
         return -1;
     }
 
-    int tun = createTunTapInterface(env, isTun, iface.c_str());
-
-    // Any exceptions will be thrown from the createTunTapInterface call
-    return tun;
+    return createTunTapImpl(env, isTun, hasCarrier, iface.c_str());
 }
 
 //------------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
-    {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+    {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
+    {"nativeCreateTunTap", "(ZZLjava/lang/String;)I", (void*)createTunTap},
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 500c696..e2c5a63 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -314,7 +314,8 @@
     }
 
     // TODO: use android::base::ScopeGuard.
-    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK
+                                           | POSIX_SPAWN_CLOEXEC_DEFAULT)) {
         posix_spawnattr_destroy(&attr);
         throwIOException(env, "posix_spawnattr_setflags failed", ret);
         return -1;
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
new file mode 100644
index 0000000..2b99d0a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/** Interface for monitoring connectivity changes. */
+public interface ConnectivityMonitor {
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    void startWatchingConnectivityChanges();
+
+    /** Stops monitoring changes of connectivity. */
+    void stopWatchingConnectivityChanges();
+
+    void notifyConnectivityChange();
+
+    /** Listener interface for receiving connectivity changes. */
+    interface Listener {
+        void onConnectivityChanged();
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
new file mode 100644
index 0000000..3563d61
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
+public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
+    private static final String TAG = "ConnMntrWConnMgr";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+
+    private final Listener listener;
+    private final ConnectivityManager.NetworkCallback networkCallback;
+    private final ConnectivityManager connectivityManager;
+    // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
+    // the receiver is unregistered twice yet.
+    private boolean isCallbackRegistered = false;
+
+    @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+        this.listener = listener;
+
+        connectivityManager =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        networkCallback =
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        LOGGER.log("network available.");
+                        notifyConnectivityChange();
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        LOGGER.log("network lost.");
+                        notifyConnectivityChange();
+                    }
+
+                    @Override
+                    public void onUnavailable() {
+                        LOGGER.log("network unavailable.");
+                        notifyConnectivityChange();
+                    }
+                };
+    }
+
+    @Override
+    public void notifyConnectivityChange() {
+        listener.onConnectivityChanged();
+    }
+
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public void startWatchingConnectivityChanges() {
+        LOGGER.log("Start watching connectivity changes");
+        if (isCallbackRegistered) {
+            return;
+        }
+
+        connectivityManager.registerNetworkCallback(
+                new NetworkRequest.Builder().addTransportType(
+                        NetworkCapabilities.TRANSPORT_WIFI).build(),
+                networkCallback);
+        isCallbackRegistered = true;
+    }
+
+    /** Stops monitoring changes of connectivity. */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public void stopWatchingConnectivityChanges() {
+        LOGGER.log("Stop watching connectivity changes");
+        if (!isCallbackRegistered) {
+            return;
+        }
+
+        connectivityManager.unregisterNetworkCallback(networkCallback);
+        isCallbackRegistered = false;
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
new file mode 100644
index 0000000..3db1b22
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
+ * query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
+ * and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
+ * it can not be enqueued, then call to {@link #call()} returns {@code null}.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
+
+    private static final String TAG = "MdnsQueryCallable";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final List<Integer> castShellEmulatorMdnsPorts;
+
+    static {
+        castShellEmulatorMdnsPorts = new ArrayList<>();
+        String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
+
+        for (String port : stringPorts) {
+            try {
+                castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
+            } catch (NumberFormatException e) {
+                // Ignore.
+            }
+        }
+    }
+
+    private final WeakReference<MdnsSocketClient> weakRequestSender;
+    private final MdnsPacketWriter packetWriter;
+    private final String[] serviceTypeLabels;
+    private final List<String> subtypes;
+    private final boolean expectUnicastResponse;
+    private final int transactionId;
+
+    EnqueueMdnsQueryCallable(
+            @NonNull MdnsSocketClient requestSender,
+            @NonNull MdnsPacketWriter packetWriter,
+            @NonNull String serviceType,
+            @NonNull Collection<String> subtypes,
+            boolean expectUnicastResponse,
+            int transactionId) {
+        weakRequestSender = new WeakReference<>(requestSender);
+        this.packetWriter = packetWriter;
+        serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+        this.subtypes = new ArrayList<>(subtypes);
+        this.expectUnicastResponse = expectUnicastResponse;
+        this.transactionId = transactionId;
+    }
+
+    @Override
+    public Pair<Integer, List<String>> call() {
+        try {
+            MdnsSocketClient requestSender = weakRequestSender.get();
+            if (requestSender == null) {
+                return null;
+            }
+
+            int numQuestions = 1;
+            if (!subtypes.isEmpty()) {
+                numQuestions += subtypes.size();
+            }
+
+            // Header.
+            packetWriter.writeUInt16(transactionId); // transaction ID
+            packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
+            packetWriter.writeUInt16(numQuestions); // number of questions
+            packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
+            packetWriter.writeUInt16(0); // number of authority entries
+            packetWriter.writeUInt16(0); // number of additional records
+
+            // Question(s). There will be one question for each (fqdn+subtype, recordType)
+          // combination,
+            // as well as one for each (fqdn, recordType) combination.
+
+            for (String subtype : subtypes) {
+                String[] labels = new String[serviceTypeLabels.length + 2];
+                labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+                labels[1] = MdnsConstants.SUBTYPE_LABEL;
+                System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+                packetWriter.writeLabels(labels);
+                packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+                packetWriter.writeUInt16(
+                        MdnsConstants.QCLASS_INTERNET
+                                | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+            }
+
+            packetWriter.writeLabels(serviceTypeLabels);
+            packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+            packetWriter.writeUInt16(
+                    MdnsConstants.QCLASS_INTERNET
+                            | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+
+            InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
+            if (requestSender.isOnIPv6OnlyNetwork()) {
+                mdnsAddress = MdnsConstants.getMdnsIPv6Address();
+            }
+
+            sendPacketTo(requestSender,
+                    new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
+            for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+                sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
+            }
+            return Pair.create(transactionId, subtypes);
+        } catch (IOException e) {
+            LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+                    TextUtils.join(",", subtypes)), e);
+            return null;
+        }
+    }
+
+    private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
+            throws IOException {
+        DatagramPacket packet = packetWriter.getPacket(address);
+        if (expectUnicastResponse) {
+            requestSender.sendUnicastPacket(packet);
+        } else {
+            requestSender.sendMulticastPacket(packet);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
new file mode 100644
index 0000000..72b65e0
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
+ * instances, and provides method to shutdown all the created executors.
+ */
+public class ExecutorProvider {
+
+    private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
+            new ArraySet<>();
+
+    /** Returns a new {@link ScheduledExecutorService} instance. */
+    public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
+        // TODO: actually use a pool ?
+        ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
+        serviceTypeClientSchedulerExecutors.add(executor);
+        return executor;
+    }
+
+    /** Shuts down all the created {@link ScheduledExecutorService} instances. */
+    public void shutdownAll() {
+        for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+            executor.shutdownNow();
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
new file mode 100644
index 0000000..922037b
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * mDNS configuration values.
+ *
+ * TODO: consider making some of these adjustable via flags.
+ */
+public class MdnsConfigs {
+    public static String[] castShellEmulatorMdnsPorts() {
+        return new String[0];
+    }
+
+    public static long initialTimeBetweenBurstsMs() {
+        return 5000L;
+    }
+
+    public static long timeBetweenBurstsMs() {
+        return 20_000L;
+    }
+
+    public static int queriesPerBurst() {
+        return 3;
+    }
+
+    public static long timeBetweenQueriesInBurstMs() {
+        return 1000L;
+    }
+
+    public static int queriesPerBurstPassive() {
+        return 1;
+    }
+
+    public static boolean alwaysAskForUnicastResponseInEachBurst() {
+        return false;
+    }
+
+    public static boolean useSessionIdToScheduleMdnsTask() {
+        return false;
+    }
+
+    public static boolean shouldCancelScanTaskWhenFutureIsNull() {
+        return false;
+    }
+
+    public static long sleepTimeForSocketThreadMs() {
+        return 20_000L;
+    }
+
+    public static boolean checkMulticastResponse() {
+        return false;
+    }
+
+    public static boolean useSeparateSocketToSendUnicastQuery() {
+        return false;
+    }
+
+    public static long checkMulticastResponseIntervalMs() {
+        return 10_000L;
+    }
+
+    public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
+        return true;
+    }
+
+    public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
+        return false;
+    }
+
+    public static int mdnsPacketQueueMaxSize() {
+        return Integer.MAX_VALUE;
+    }
+
+    public static boolean preferIpv6() {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
new file mode 100644
index 0000000..ed28700
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/** mDNS-related constants. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public final class MdnsConstants {
+    public static final int MDNS_PORT = 5353;
+    // Flags word format is:
+    // 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+    // QR [ Opcode  ] AA TC RD RA  Z AD CD [  Rcode  ]
+    // See http://www.networksorcery.com/enp/protocol/dns.htm
+    // For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
+    // should be 0.
+    public static final int FLAGS_QUERY = 0x0000;
+    public static final int FLAGS_RESPONSE_MASK = 0xF80F;
+    public static final int FLAGS_RESPONSE = 0x8000;
+    public static final int QCLASS_INTERNET = 0x0001;
+    public static final int QCLASS_UNICAST = 0x8000;
+    public static final String SUBTYPE_LABEL = "_sub";
+    public static final String SUBTYPE_PREFIX = "_";
+    private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
+    private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+    private static InetAddress mdnsAddress;
+    private static Charset utf8Charset;
+    private MdnsConstants() {
+    }
+
+    public static InetAddress getMdnsIPv4Address() {
+        synchronized (MdnsConstants.class) {
+            InetAddress addr = null;
+            try {
+                addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
+            } catch (UnknownHostException e) {
+                /* won't happen */
+            }
+            mdnsAddress = addr;
+            return mdnsAddress;
+        }
+    }
+
+    public static InetAddress getMdnsIPv6Address() {
+        synchronized (MdnsConstants.class) {
+            InetAddress addr = null;
+            try {
+                addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
+            } catch (UnknownHostException e) {
+                /* won't happen */
+            }
+            mdnsAddress = addr;
+            return mdnsAddress;
+        }
+    }
+
+    public static Charset getUtf8Charset() {
+        synchronized (MdnsConstants.class) {
+            if (utf8Charset == null) {
+                utf8Charset = getUtf8CharsetOnKitKat();
+            }
+            return utf8Charset;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static Charset getUtf8CharsetOnKitKat() {
+        return StandardCharsets.UTF_8;
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
new file mode 100644
index 0000000..1faa6ce
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
+ * notify them when a mDNS service instance is found, updated, or removed?
+ */
+public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
+
+    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
+
+    private final ExecutorProvider executorProvider;
+    private final MdnsSocketClient socketClient;
+
+    private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
+
+    public MdnsDiscoveryManager(
+            @NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
+        this.executorProvider = executorProvider;
+        this.socketClient = socketClient;
+    }
+
+    /**
+     * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
+     * {@code listener} for receiving mDNS service discovery responses.
+     *
+     * @param serviceType   The type of the service to discover.
+     * @param listener      The {@link MdnsServiceBrowserListener} listener.
+     * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
+     *                      serviceType}.
+     */
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public synchronized void registerListener(
+            @NonNull String serviceType,
+            @NonNull MdnsServiceBrowserListener listener,
+            @NonNull MdnsSearchOptions searchOptions) {
+        LOGGER.log(
+                "Registering listener for subtypes: %s",
+                TextUtils.join(",", searchOptions.getSubtypes()));
+        if (serviceTypeClients.isEmpty()) {
+            // First listener. Starts the socket client.
+            try {
+                socketClient.startDiscovery();
+            } catch (IOException e) {
+                LOGGER.e("Failed to start discover.", e);
+                return;
+            }
+        }
+        // All listeners of the same service types shares the same MdnsServiceTypeClient.
+        MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+        if (serviceTypeClient == null) {
+            serviceTypeClient = createServiceTypeClient(serviceType);
+            serviceTypeClients.put(serviceType, serviceTypeClient);
+        }
+        serviceTypeClient.startSendAndReceive(listener, searchOptions);
+    }
+
+    /**
+     * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
+     * registered for the given service type, stops discovery for the service type.
+     *
+     * @param serviceType The type of the service to discover.
+     * @param listener    The {@link MdnsServiceBrowserListener} listener.
+     */
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public synchronized void unregisterListener(
+            @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+        LOGGER.log("Unregistering listener for service type: %s", serviceType);
+        MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+        if (serviceTypeClient == null) {
+            return;
+        }
+        if (serviceTypeClient.stopSendAndReceive(listener)) {
+            // No listener is registered for the service type anymore, remove it from the list of
+          // the
+            // service type clients.
+            serviceTypeClients.remove(serviceType);
+            if (serviceTypeClients.isEmpty()) {
+                // No discovery request. Stops the socket client.
+                socketClient.stopDiscovery();
+            }
+        }
+    }
+
+    @Override
+    public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
+        String[] name =
+                response.getPointerRecords().isEmpty()
+                        ? null
+                        : response.getPointerRecords().get(0).getName();
+        if (name != null) {
+            for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+                String[] serviceType = serviceTypeClient.getServiceTypeLabels();
+                if ((Arrays.equals(name, serviceType)
+                        || ((name.length == (serviceType.length + 2))
+                        && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+                        && MdnsRecord.labelsAreSuffix(serviceType, name)))) {
+                    serviceTypeClient.processResponse(response);
+                    return;
+                }
+            }
+        }
+    }
+
+    @Override
+    public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+        for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+            serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+        }
+    }
+
+    @VisibleForTesting
+    MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+        return new MdnsServiceTypeClient(
+                serviceType, socketClient,
+                executorProvider.newServiceTypeClientSchedulerExecutor());
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
new file mode 100644
index 0000000..e35743c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsInetAddressRecord extends MdnsRecord {
+    private Inet6Address inet6Address;
+    private Inet4Address inet4Address;
+
+    /**
+     * Constructs the {@link MdnsRecord}
+     *
+     * @param name   the service host name
+     * @param type   the type of record (either Type 'AAAA' or Type 'A')
+     * @param reader the reader to read the record from.
+     */
+    public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
+            throws IOException {
+        super(name, type, reader);
+    }
+
+    /** Returns the IPv6 address. */
+    public Inet6Address getInet6Address() {
+        return inet6Address;
+    }
+
+    /** Returns the IPv4 address. */
+    public Inet4Address getInet4Address() {
+        return inet4Address;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        int size = 4;
+        if (super.getType() == MdnsRecord.TYPE_AAAA) {
+            size = 16;
+        }
+        byte[] buf = new byte[size];
+        reader.readBytes(buf);
+        try {
+            InetAddress address = InetAddress.getByAddress(buf);
+            if (address instanceof Inet4Address) {
+                inet4Address = (Inet4Address) address;
+                inet6Address = null;
+            } else if (address instanceof Inet6Address) {
+                inet4Address = null;
+                inet6Address = (Inet6Address) address;
+            } else {
+                inet4Address = null;
+                inet6Address = null;
+            }
+        } catch (UnknownHostException e) {
+            // Ignore exception
+        }
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        byte[] buf = null;
+        if (inet4Address != null) {
+            buf = inet4Address.getAddress();
+        } else if (inet6Address != null) {
+            buf = inet6Address.getAddress();
+        }
+        if (buf != null) {
+            writer.writeBytes(buf);
+        }
+    }
+
+    @Override
+    public String toString() {
+        String type = "AAAA";
+        if (super.getType() == MdnsRecord.TYPE_A) {
+            type = "A";
+        }
+        return String.format(
+                Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
+                inet6Address);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31)
+                + Objects.hashCode(inet4Address)
+                + Objects.hashCode(inet6Address);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsInetAddressRecord)) {
+            return false;
+        }
+
+        return super.equals(other)
+                && Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
+                && Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
new file mode 100644
index 0000000..61c5f5a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.SparseArray;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Simple decoder for mDNS packets. */
+public class MdnsPacketReader {
+    private final byte[] buf;
+    private final int count;
+    private final SparseArray<LabelEntry> labelDictionary;
+    private int pos;
+    private int limit;
+
+    /** Constructs a reader for the given packet. */
+    public MdnsPacketReader(DatagramPacket packet) {
+        buf = packet.getData();
+        count = packet.getLength();
+        pos = 0;
+        limit = -1;
+        labelDictionary = new SparseArray<>(16);
+    }
+
+    /**
+     * Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
+     * read past this limit will result in an EOFException.
+     *
+     * @param limit The new limit.
+     * @throws IOException If there is insufficient data for the new limit.
+     */
+    public void setLimit(int limit) throws IOException {
+        if (limit >= 0) {
+            if (pos + limit <= count) {
+                this.limit = pos + limit;
+            } else {
+                throw new IOException(
+                        String.format(
+                                Locale.ROOT,
+                                "attempt to set limit beyond available data: %d exceeds %d",
+                                pos + limit,
+                                count));
+            }
+        }
+    }
+
+    /** Clears the limit set by {@link #setLimit}. */
+    public void clearLimit() {
+        limit = -1;
+    }
+
+    /**
+     * Returns the number of bytes left to read, between the current read position and either the
+     * limit (if set) or the end of the packet.
+     */
+    public int getRemaining() {
+        return (limit >= 0 ? limit : count) - pos;
+    }
+
+    /**
+     * Reads an unsigned 8-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public int readUInt8() throws EOFException {
+        checkRemaining(1);
+        byte val = buf[pos++];
+        return val & 0xFF;
+    }
+
+    /**
+     * Reads an unsigned 16-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public int readUInt16() throws EOFException {
+        checkRemaining(2);
+        int val = (buf[pos++] & 0xFF) << 8;
+        val |= (buf[pos++]) & 0xFF;
+        return val;
+    }
+
+    /**
+     * Reads an unsigned 32-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public long readUInt32() throws EOFException {
+        checkRemaining(4);
+        long val = (long) (buf[pos++] & 0xFF) << 24;
+        val |= (long) (buf[pos++] & 0xFF) << 16;
+        val |= (long) (buf[pos++] & 0xFF) << 8;
+        val |= buf[pos++] & 0xFF;
+        return val;
+    }
+
+    /**
+     * Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
+     * either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
+     * pointer, or a pointer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     * @throws IOException  If invalid data is read.
+     */
+    public String[] readLabels() throws IOException {
+        List<String> result = new ArrayList<>(5);
+        LabelEntry previousEntry = null;
+
+        while (getRemaining() > 0) {
+            byte nextByte = peekByte();
+
+            if (nextByte == 0) {
+                // A NUL byte terminates a sequence of labels.
+                skip(1);
+                break;
+            }
+
+            int currentOffset = pos;
+
+            boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
+            if (isLabelPointer) {
+                // A pointer terminates a sequence of labels. Store the pointer value in the
+                // previous label entry.
+                int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
+                if (previousEntry != null) {
+                    previousEntry.nextOffset = labelOffset;
+                }
+
+                // Follow the chain of labels starting at this pointer, adding all of them onto the
+                // result.
+                while (labelOffset != 0) {
+                    LabelEntry entry = labelDictionary.get(labelOffset);
+                    if (entry == null) {
+                        throw new IOException(
+                                String.format(Locale.ROOT, "Invalid label pointer: %04X",
+                                        labelOffset));
+                    }
+                    result.add(entry.label);
+                    labelOffset = entry.nextOffset;
+                }
+                break;
+            } else {
+                // It's an ordinary label. Chain it onto the previous label entry (if any), and add
+                // it onto the result.
+                String val = readString();
+                LabelEntry newEntry = new LabelEntry(val);
+                labelDictionary.put(currentOffset, newEntry);
+
+                if (previousEntry != null) {
+                    previousEntry.nextOffset = currentOffset;
+                }
+                previousEntry = newEntry;
+                result.add(val);
+            }
+        }
+
+        return result.toArray(new String[result.size()]);
+    }
+
+    /**
+     * Reads a length-prefixed string.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public String readString() throws EOFException {
+        int len = readUInt8();
+        checkRemaining(len);
+        String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
+        pos += len;
+        return val;
+    }
+
+    /**
+     * Reads a specific number of bytes.
+     *
+     * @param bytes The array to fill.
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public void readBytes(byte[] bytes) throws EOFException {
+        checkRemaining(bytes.length);
+        System.arraycopy(buf, pos, bytes, 0, bytes.length);
+        pos += bytes.length;
+    }
+
+    /**
+     * Skips over the given number of bytes.
+     *
+     * @param count The number of bytes to read and discard.
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public void skip(int count) throws EOFException {
+        checkRemaining(count);
+        pos += count;
+    }
+
+    /**
+     * Peeks at and returns the next byte in the packet, without advancing the read position.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public byte peekByte() throws EOFException {
+        checkRemaining(1);
+        return buf[pos];
+    }
+
+    /** Returns the current byte position of the reader for the data packet. */
+    public int getPosition() {
+        return pos;
+    }
+
+    // Checks if the number of remaining bytes to be read in the packet is at least |count|.
+    private void checkRemaining(int count) throws EOFException {
+        if (getRemaining() < count) {
+            throw new EOFException();
+        }
+    }
+
+    private static class LabelEntry {
+        public final String label;
+        public int nextOffset = 0;
+
+        public LabelEntry(String label) {
+            this.label = label;
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
new file mode 100644
index 0000000..2fed36d
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Simple encoder for mDNS packets. */
+public class MdnsPacketWriter {
+    private static final int MDNS_POINTER_MASK = 0xC000;
+    private final byte[] data;
+    private final Map<Integer, String[]> labelDictionary;
+    private int pos = 0;
+    private int savedWritePos = -1;
+
+    /**
+     * Constructs a writer for a new packet.
+     *
+     * @param maxSize The maximum size of a packet.
+     */
+    public MdnsPacketWriter(int maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("invalid size");
+        }
+
+        data = new byte[maxSize];
+        labelDictionary = new HashMap<>();
+    }
+
+    /** Returns the current write position. */
+    public int getWritePosition() {
+        return pos;
+    }
+
+    /**
+     * Saves the current write position and then rewinds the write position by the given number of
+     * bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
+     * nested.
+     *
+     * @param position The position to rewind to.
+     * @throws IOException If the count would go beyond the beginning of the packet, or if there is
+     *                     already a rewind in effect.
+     */
+    public void rewind(int position) throws IOException {
+        if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
+            throw new IOException("invalid rewind");
+        }
+
+        savedWritePos = pos;
+        pos = position;
+    }
+
+    /**
+     * Sets the current write position to what it was prior to the last rewind.
+     *
+     * @throws IOException If there was no rewind in effect.
+     */
+    public void unrewind() throws IOException {
+        if (savedWritePos == -1) {
+            throw new IOException("no rewind is in effect");
+        }
+        pos = savedWritePos;
+        savedWritePos = -1;
+    }
+
+    /** Clears any rewind state. */
+    public void clearRewind() {
+        savedWritePos = -1;
+    }
+
+    /**
+     * Writes an unsigned 8-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt8(int value) throws IOException {
+        checkRemaining(1);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes an unsigned 16-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt16(int value) throws IOException {
+        checkRemaining(2);
+        data[pos++] = (byte) ((value >>> 8) & 0xFF);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes an unsigned 32-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt32(long value) throws IOException {
+        checkRemaining(4);
+        data[pos++] = (byte) ((value >>> 24) & 0xFF);
+        data[pos++] = (byte) ((value >>> 16) & 0xFF);
+        data[pos++] = (byte) ((value >>> 8) & 0xFF);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes a specific number of bytes.
+     *
+     * @param data The array to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeBytes(byte[] data) throws IOException {
+        checkRemaining(data.length);
+        System.arraycopy(data, 0, this.data, pos, data.length);
+        pos += data.length;
+    }
+
+    /**
+     * Writes a string.
+     *
+     * @param value The string to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeString(String value) throws IOException {
+        byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
+        writeUInt8(utf8.length);
+        writeBytes(utf8);
+    }
+
+    /**
+     * Writes a series of labels. Uses name compression.
+     *
+     * @param labels The labels to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeLabels(String[] labels) throws IOException {
+        // See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
+        // of the name compression method used here.
+
+        int suffixLength = 0;
+        int suffixPointer = 0;
+
+        for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
+            int existingOffset = entry.getKey();
+            String[] existingLabels = entry.getValue();
+
+            if (Arrays.equals(existingLabels, labels)) {
+                writePointer(existingOffset);
+                return;
+            } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
+                // Keep track of the longest matching suffix so far.
+                if (existingLabels.length > suffixLength) {
+                    suffixLength = existingLabels.length;
+                    suffixPointer = existingOffset;
+                }
+            }
+        }
+
+        if (suffixLength > 0) {
+            for (int i = 0; i < (labels.length - suffixLength); ++i) {
+                writeString(labels[i]);
+            }
+            writePointer(suffixPointer);
+        } else {
+            int[] offsets = new int[labels.length];
+            for (int i = 0; i < labels.length; ++i) {
+                offsets[i] = getWritePosition();
+                writeString(labels[i]);
+            }
+            writeUInt8(0); // NUL terminator
+
+            // Add entries to the label dictionary for each suffix of the label list, including
+            // the whole list itself.
+            for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
+                String[] value = new String[len];
+                System.arraycopy(labels, i, value, 0, len);
+                labelDictionary.put(offsets[i], value);
+            }
+        }
+    }
+
+    /** Returns the number of bytes that can still be written. */
+    public int getRemaining() {
+        return data.length - pos;
+    }
+
+    // Writes a pointer to a label.
+    private void writePointer(int offset) throws IOException {
+        writeUInt16(MDNS_POINTER_MASK | offset);
+    }
+
+    // Checks if the remaining space in the packet is at least |count|.
+    private void checkRemaining(int count) throws IOException {
+        if (getRemaining() < count) {
+            throw new IOException();
+        }
+    }
+
+    /** Builds and returns the packet. */
+    public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
+        return new DatagramPacket(data, pos, destAddress);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
new file mode 100644
index 0000000..2b36a3c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "PTR" record, which holds a name (the "pointer"). */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsPointerRecord extends MdnsRecord {
+    private String[] pointer;
+
+    public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        super(name, TYPE_PTR, reader);
+    }
+
+    /** Returns the pointer as an array of labels. */
+    public String[] getPointer() {
+        return pointer;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        pointer = reader.readLabels();
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        writer.writeLabels(pointer);
+    }
+
+    public boolean hasSubtype() {
+        return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+    }
+
+    public String getSubtype() {
+        return hasSubtype() ? name[0] : null;
+    }
+
+    @Override
+    public String toString() {
+        return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsPointerRecord)) {
+            return false;
+        }
+
+        return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
new file mode 100644
index 0000000..4bfdb2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Abstract base class for mDNS records. Stores the header fields and provides methods for reading
+ * the record from and writing it to a packet.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public abstract class MdnsRecord {
+    public static final int TYPE_A = 0x0001;
+    public static final int TYPE_AAAA = 0x001C;
+    public static final int TYPE_PTR = 0x000C;
+    public static final int TYPE_SRV = 0x0021;
+    public static final int TYPE_TXT = 0x0010;
+
+    /** Status indicating that the record is current. */
+    public static final int STATUS_OK = 0;
+    /** Status indicating that the record has expired (TTL reached 0). */
+    public static final int STATUS_EXPIRED = 1;
+    /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
+    public static final int STATUS_NEEDS_REFRESH = 2;
+
+    protected final String[] name;
+    private final int type;
+    private final int cls;
+    private final long receiptTimeMillis;
+    private final long ttlMillis;
+    private Object key;
+
+    /**
+     * Constructs a new record with the given name and type.
+     *
+     * @param reader The reader to read the record from.
+     * @throws IOException If an error occurs while reading the packet.
+     */
+    protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
+        this.name = name;
+        this.type = type;
+        cls = reader.readUInt16();
+        ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
+        int dataLength = reader.readUInt16();
+
+        receiptTimeMillis = SystemClock.elapsedRealtime();
+
+        reader.setLimit(dataLength);
+        readData(reader);
+        reader.clearLimit();
+    }
+
+    /**
+     * Converts an array of labels into their dot-separated string representation. This method
+     * should
+     * be used for logging purposes only.
+     */
+    public static String labelsToString(String[] labels) {
+        if (labels == null) {
+            return null;
+        }
+        return TextUtils.join(".", labels);
+    }
+
+    /** Tests if |list1| is a suffix of |list2|. */
+    public static boolean labelsAreSuffix(String[] list1, String[] list2) {
+        int offset = list2.length - list1.length;
+
+        if (offset < 1) {
+            return false;
+        }
+
+        for (int i = 0; i < list1.length; ++i) {
+            if (!list1[i].equals(list2[i + offset])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /** Returns the record's receipt (creation) time. */
+    public final long getReceiptTime() {
+        return receiptTimeMillis;
+    }
+
+    /** Returns the record's name. */
+    public String[] getName() {
+        return name;
+    }
+
+    /** Returns the record's original TTL, in milliseconds. */
+    public final long getTtl() {
+        return ttlMillis;
+    }
+
+    /** Returns the record's type. */
+    public final int getType() {
+        return type;
+    }
+
+    /**
+     * Returns the record's remaining TTL.
+     *
+     * @param now The current system time.
+     * @return The remaning TTL, in milliseconds.
+     */
+    public long getRemainingTTL(final long now) {
+        long age = now - receiptTimeMillis;
+        if (age > ttlMillis) {
+            return 0;
+        }
+
+        return ttlMillis - age;
+    }
+
+    /**
+     * Reads the record's payload from a packet.
+     *
+     * @param reader The reader to use.
+     * @throws IOException If an I/O error occurs.
+     */
+    protected abstract void readData(MdnsPacketReader reader) throws IOException;
+
+    /**
+     * Writes the record to a packet.
+     *
+     * @param writer The writer to use.
+     * @param now    The current system time. This is used when writing the updated TTL.
+     */
+    @VisibleForTesting
+    public final void write(MdnsPacketWriter writer, long now) throws IOException {
+        writer.writeLabels(name);
+        writer.writeUInt16(type);
+        writer.writeUInt16(cls);
+
+        writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
+
+        int dataLengthPos = writer.getWritePosition();
+        writer.writeUInt16(0); // data length
+        int dataPos = writer.getWritePosition();
+
+        writeData(writer);
+
+        // Calculate amount of data written, and overwrite the data field earlier in the packet.
+        int endPos = writer.getWritePosition();
+        int dataLength = endPos - dataPos;
+        writer.rewind(dataLengthPos);
+        writer.writeUInt16(dataLength);
+        writer.unrewind();
+    }
+
+    /**
+     * Writes the record's payload to a packet.
+     *
+     * @param writer The writer to use.
+     * @throws IOException If an I/O error occurs.
+     */
+    protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
+
+    /** Gets the status of the record. */
+    public int getStatus(final long now) {
+        final long age = now - receiptTimeMillis;
+        if (age > ttlMillis) {
+            return STATUS_EXPIRED;
+        }
+        if (age > (ttlMillis / 2)) {
+            return STATUS_NEEDS_REFRESH;
+        }
+        return STATUS_OK;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof MdnsRecord)) {
+            return false;
+        }
+
+        MdnsRecord otherRecord = (MdnsRecord) other;
+
+        return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(name), type);
+    }
+
+    /**
+     * Returns an opaque object that uniquely identifies this record through a combination of its
+     * type
+     * and name. Suitable for use as a key in caches.
+     */
+    public final Object getKey() {
+        if (key == null) {
+            key = new Key(type, name);
+        }
+        return key;
+    }
+
+    private static final class Key {
+        private final int recordType;
+        private final String[] recordName;
+
+        public Key(int recordType, String[] recordName) {
+            this.recordType = recordType;
+            this.recordName = recordName;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof Key)) {
+                return false;
+            }
+
+            Key otherKey = (Key) other;
+
+            return (recordType == otherKey.recordType) && Arrays.equals(recordName,
+                    otherKey.recordName);
+        }
+
+        @Override
+        public int hashCode() {
+            return (recordType * 31) + Arrays.hashCode(recordName);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
new file mode 100644
index 0000000..1305e07
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/** An mDNS response. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponse {
+    private final List<MdnsRecord> records;
+    private final List<MdnsPointerRecord> pointerRecords;
+    private MdnsServiceRecord serviceRecord;
+    private MdnsTextRecord textRecord;
+    private MdnsInetAddressRecord inet4AddressRecord;
+    private MdnsInetAddressRecord inet6AddressRecord;
+    private long lastUpdateTime;
+
+    /** Constructs a new, empty response. */
+    public MdnsResponse(long now) {
+        lastUpdateTime = now;
+        records = new LinkedList<>();
+        pointerRecords = new LinkedList<>();
+    }
+
+    // This generic typed helper compares records for equality.
+    // Returns True if records are the same.
+    private <T> boolean recordsAreSame(T a, T b) {
+        return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
+    }
+
+    /**
+     * Adds a pointer record.
+     *
+     * @return <code>true</code> if the record was added, or <code>false</code> if a matching
+     * pointer
+     * record is already present in the response.
+     */
+    public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
+        if (!pointerRecords.contains(pointerRecord)) {
+            pointerRecords.add(pointerRecord);
+            records.add(pointerRecord);
+            return true;
+        }
+
+        return false;
+    }
+
+    /** Gets the pointer records. */
+    public synchronized List<MdnsPointerRecord> getPointerRecords() {
+        // Returns a shallow copy.
+        return new LinkedList<>(pointerRecords);
+    }
+
+    public synchronized boolean hasPointerRecords() {
+        return !pointerRecords.isEmpty();
+    }
+
+    @VisibleForTesting
+    /* package */ synchronized void clearPointerRecords() {
+        pointerRecords.clear();
+    }
+
+    public synchronized boolean hasSubtypes() {
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            if (pointerRecord.hasSubtype()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public synchronized List<String> getSubtypes() {
+        List<String> subtypes = null;
+
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            if (pointerRecord.hasSubtype()) {
+                if (subtypes == null) {
+                    subtypes = new LinkedList<>();
+                }
+                subtypes.add(pointerRecord.getSubtype());
+            }
+        }
+
+        return subtypes;
+    }
+
+    @VisibleForTesting
+    public synchronized void removeSubtypes() {
+        Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
+        while (iter.hasNext()) {
+            MdnsPointerRecord pointerRecord = iter.next();
+            if (pointerRecord.hasSubtype()) {
+                iter.remove();
+            }
+        }
+    }
+
+    /** Sets the service record. */
+    public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
+        if (recordsAreSame(this.serviceRecord, serviceRecord)) {
+            return false;
+        }
+        if (this.serviceRecord != null) {
+            records.remove(this.serviceRecord);
+        }
+        this.serviceRecord = serviceRecord;
+        if (this.serviceRecord != null) {
+            records.add(this.serviceRecord);
+        }
+        return true;
+    }
+
+    /** Gets the service record. */
+    public synchronized MdnsServiceRecord getServiceRecord() {
+        return serviceRecord;
+    }
+
+    public synchronized boolean hasServiceRecord() {
+        return serviceRecord != null;
+    }
+
+    /** Sets the text record. */
+    public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
+        if (recordsAreSame(this.textRecord, textRecord)) {
+            return false;
+        }
+        if (this.textRecord != null) {
+            records.remove(this.textRecord);
+        }
+        this.textRecord = textRecord;
+        if (this.textRecord != null) {
+            records.add(this.textRecord);
+        }
+        return true;
+    }
+
+    /** Gets the text record. */
+    public synchronized MdnsTextRecord getTextRecord() {
+        return textRecord;
+    }
+
+    public synchronized boolean hasTextRecord() {
+        return textRecord != null;
+    }
+
+    /** Sets the IPv4 address record. */
+    public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
+        if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
+            return false;
+        }
+        if (this.inet4AddressRecord != null) {
+            records.remove(this.inet4AddressRecord);
+        }
+        if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
+            this.inet4AddressRecord = newInet4AddressRecord;
+            records.add(this.inet4AddressRecord);
+        }
+        return true;
+    }
+
+    /** Gets the IPv4 address record. */
+    public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
+        return inet4AddressRecord;
+    }
+
+    public synchronized boolean hasInet4AddressRecord() {
+        return inet4AddressRecord != null;
+    }
+
+    /** Sets the IPv6 address record. */
+    public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
+        if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
+            return false;
+        }
+        if (this.inet6AddressRecord != null) {
+            records.remove(this.inet6AddressRecord);
+        }
+        if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
+            this.inet6AddressRecord = newInet6AddressRecord;
+            records.add(this.inet6AddressRecord);
+        }
+        return true;
+    }
+
+
+    /** Gets the IPv6 address record. */
+    public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
+        return inet6AddressRecord;
+    }
+
+    public synchronized boolean hasInet6AddressRecord() {
+        return inet6AddressRecord != null;
+    }
+
+    /** Gets all of the records. */
+    public synchronized List<MdnsRecord> getRecords() {
+        return new LinkedList<>(records);
+    }
+
+    /**
+     * Merges any records that are present in another response into this one.
+     *
+     * @return <code>true</code> if any records were added or updated.
+     */
+    public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
+        lastUpdateTime = other.lastUpdateTime;
+
+        boolean updated = false;
+
+        List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
+        if (pointerRecords != null) {
+            for (MdnsPointerRecord pointerRecord : pointerRecords) {
+                if (addPointerRecord(pointerRecord)) {
+                    updated = true;
+                }
+            }
+        }
+
+        MdnsServiceRecord serviceRecord = other.getServiceRecord();
+        if (serviceRecord != null) {
+            if (setServiceRecord(serviceRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsTextRecord textRecord = other.getTextRecord();
+        if (textRecord != null) {
+            if (setTextRecord(textRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
+        if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
+            if (setInet4AddressRecord(otherInet4AddressRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
+        if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
+            if (setInet6AddressRecord(otherInet6AddressRecord)) {
+                updated = true;
+            }
+        }
+
+        // If the hostname in the service record no longer matches the hostname in either of the
+        // address records, then drop the address records.
+        if (this.serviceRecord != null) {
+            boolean dropAddressRecords = false;
+
+            if (this.inet4AddressRecord != null) {
+                if (!Arrays.equals(
+                        this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
+                    dropAddressRecords = true;
+                }
+            }
+            if (this.inet6AddressRecord != null) {
+                if (!Arrays.equals(
+                        this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
+                    dropAddressRecords = true;
+                }
+            }
+
+            if (dropAddressRecords) {
+                setInet4AddressRecord(null);
+                setInet6AddressRecord(null);
+                updated = true;
+            }
+        }
+
+        return updated;
+    }
+
+    /**
+     * Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
+     * TXT, and A (for IPv4) or AAAA (for IPv6) records.
+     */
+    public synchronized boolean isComplete() {
+        return !pointerRecords.isEmpty()
+                && (serviceRecord != null)
+                && (textRecord != null)
+                && (inet4AddressRecord != null || inet6AddressRecord != null);
+    }
+
+    /**
+     * Returns the key for this response. The key uniquely identifies the response by its service
+     * name.
+     */
+    public synchronized String getServiceInstanceName() {
+        if (pointerRecords.isEmpty()) {
+            return null;
+        }
+        String[] pointers = pointerRecords.get(0).getPointer();
+        return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
+    }
+
+    /**
+     * Tests if this response is a goodbye message. This will be true if a service record is present
+     * and any of the records have a TTL of 0.
+     */
+    public synchronized boolean isGoodbye() {
+        if (getServiceInstanceName() != null) {
+            for (MdnsRecord record : records) {
+                // Expiring PTR records with subtypes just signal a change in known supported
+                // criteria, not the device itself going offline, so ignore those.
+                if ((record instanceof MdnsPointerRecord)
+                        && ((MdnsPointerRecord) record).hasSubtype()) {
+                    continue;
+                }
+
+                if (record.getTtl() == 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Writes the response to a packet.
+     *
+     * @param writer The writer to use.
+     * @param now    The current time. This is used to write updated TTLs that reflect the remaining
+     *               TTL
+     *               since the response was received.
+     * @return The number of records that were written.
+     * @throws IOException If an error occurred while writing (typically indicating overflow).
+     */
+    public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
+        int count = 0;
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            pointerRecord.write(writer, now);
+            ++count;
+        }
+
+        if (serviceRecord != null) {
+            serviceRecord.write(writer, now);
+            ++count;
+        }
+
+        if (textRecord != null) {
+            textRecord.write(writer, now);
+            ++count;
+        }
+
+        if (inet4AddressRecord != null) {
+            inet4AddressRecord.write(writer, now);
+            ++count;
+        }
+
+        if (inet6AddressRecord != null) {
+            inet6AddressRecord.write(writer, now);
+            ++count;
+        }
+
+        return count;
+    }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
new file mode 100644
index 0000000..72c3156
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/** A class that decodes mDNS responses from UDP packets. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponseDecoder {
+
+    public static final int SUCCESS = 0;
+    private static final String TAG = "MdnsResponseDecoder";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private final String[] serviceType;
+    private final Clock clock;
+
+    /** Constructs a new decoder that will extract responses for the given service type. */
+    public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+        this.clock = clock;
+        this.serviceType = serviceType;
+    }
+
+    private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
+        reader.skip(2 + 4); // skip the class and TTL
+        int dataLength = reader.readUInt16();
+        reader.skip(dataLength);
+    }
+
+    private static MdnsResponse findResponseWithPointer(
+            List<MdnsResponse> responses, String[] pointer) {
+        if (responses != null) {
+            for (MdnsResponse response : responses) {
+                List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
+                if (pointerRecords == null) {
+                    continue;
+                }
+                for (MdnsPointerRecord pointerRecord : pointerRecords) {
+                    if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
+                        return response;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private static MdnsResponse findResponseWithHostName(
+            List<MdnsResponse> responses, String[] hostName) {
+        if (responses != null) {
+            for (MdnsResponse response : responses) {
+                MdnsServiceRecord serviceRecord = response.getServiceRecord();
+                if (serviceRecord == null) {
+                    continue;
+                }
+                if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+                    return response;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Decodes all mDNS responses for the desired service type from a packet. The class does not
+     * check
+     * the responses for completeness; the caller should do that.
+     *
+     * @param packet The packet to read from.
+     * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+     */
+    public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        List<MdnsRecord> records;
+        try {
+            reader.readUInt16(); // transaction ID (not used)
+            int flags = reader.readUInt16();
+            if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
+                return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
+            }
+
+            int numQuestions = reader.readUInt16();
+            int numAnswers = reader.readUInt16();
+            int numAuthority = reader.readUInt16();
+            int numRecords = reader.readUInt16();
+
+            LOGGER.log(String.format(
+                    "num questions: %d, num answers: %d, num authority: %d, num records: %d",
+                    numQuestions, numAnswers, numAuthority, numRecords));
+
+            if (numAnswers < 1) {
+                return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
+            }
+
+            records = new LinkedList<>();
+
+            for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
+                String[] name;
+                try {
+                    name = reader.readLabels();
+                } catch (IOException e) {
+                    LOGGER.e("Failed to read labels from mDNS response.", e);
+                    return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
+                }
+                int type = reader.readUInt16();
+
+                switch (type) {
+                    case MdnsRecord.TYPE_A: {
+                        try {
+                            records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read A record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_AAAA: {
+                        try {
+                            // AAAA should only contain the IPv6 address.
+                            MdnsInetAddressRecord record =
+                                    new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+                            if (record.getInet6Address() != null) {
+                                records.add(record);
+                            }
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read AAAA record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_PTR: {
+                        try {
+                            records.add(new MdnsPointerRecord(name, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read PTR record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_SRV: {
+                        if (name.length == 4) {
+                            try {
+                                records.add(new MdnsServiceRecord(name, reader));
+                            } catch (IOException e) {
+                                LOGGER.e("Failed to read SRV record from mDNS response.", e);
+                                return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
+                            }
+                        } else {
+                            try {
+                                skipMdnsRecord(reader);
+                            } catch (IOException e) {
+                                LOGGER.e("Failed to skip SVR record from mDNS response.", e);
+                                return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
+                            }
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_TXT: {
+                        try {
+                            records.add(new MdnsTextRecord(name, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read TXT record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
+                        }
+                        break;
+                    }
+
+                    default: {
+                        try {
+                            skipMdnsRecord(reader);
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to skip mDNS record.", e);
+                            return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
+                        }
+                    }
+                }
+            }
+        } catch (EOFException e) {
+            LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
+            return MdnsResponseErrorCode.ERROR_END_OF_FILE;
+        }
+
+        // The response records are structured in a hierarchy, where some records reference
+        // others, as follows:
+        //
+        //        PTR
+        //        / \
+        //       /   \
+        //      TXT  SRV
+        //           / \
+        //          /   \
+        //         A   AAAA
+        //
+        // But the order in which these records appear in the response packet is completely
+        // arbitrary. This means that we need to rescan the record list to construct each level of
+        // this hierarchy.
+        //
+        // PTR: service type -> service instance name
+        //
+        // SRV: service instance name -> host name (priority, weight)
+        //
+        // TXT: service instance name -> machine readable txt entries.
+        //
+        // A: host name -> IP address
+
+        // Loop 1: find PTR records, which identify distinct service instances.
+        long now = SystemClock.elapsedRealtime();
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsPointerRecord) {
+                String[] name = record.getName();
+                if ((serviceType == null)
+                        || Arrays.equals(name, serviceType)
+                        || ((name.length == (serviceType.length + 2))
+                        && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+                        && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+                    MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
+                    // Group PTR records that refer to the same service instance name into a single
+                    // response.
+                    MdnsResponse response = findResponseWithPointer(responses,
+                            pointerRecord.getPointer());
+                    if (response == null) {
+                        response = new MdnsResponse(now);
+                        responses.add(response);
+                    }
+                    response.addPointerRecord((MdnsPointerRecord) record);
+                }
+            }
+        }
+
+        // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsServiceRecord) {
+                MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
+                MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
+                if (response != null) {
+                    response.setServiceRecord(serviceRecord);
+                }
+            } else if (record instanceof MdnsTextRecord) {
+                MdnsTextRecord textRecord = (MdnsTextRecord) record;
+                MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
+                if (response != null) {
+                    response.setTextRecord(textRecord);
+                }
+            }
+        }
+
+        // Loop 3: find A and AAAA records, which reference the host name in the SRV record.
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsInetAddressRecord) {
+                MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
+                MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
+                if (inetRecord.getInet4Address() != null && response != null) {
+                    response.setInet4AddressRecord(inetRecord);
+                } else if (inetRecord.getInet6Address() != null && response != null) {
+                    response.setInet6AddressRecord(inetRecord);
+                }
+            }
+        }
+
+        return SUCCESS;
+    }
+
+    public static class Clock {
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
new file mode 100644
index 0000000..fcf9058
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * The list of error code for parsing mDNS response.
+ *
+ * @hide
+ */
+public class MdnsResponseErrorCode {
+    public static final int SUCCESS = 0;
+    public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
+    public static final int ERROR_NO_ANSWERS = 2;
+    public static final int ERROR_READING_RECORD_NAME = 3;
+    public static final int ERROR_READING_A_RDATA = 4;
+    public static final int ERROR_READING_AAAA_RDATA = 5;
+    public static final int ERROR_READING_PTR_RDATA = 6;
+    public static final int ERROR_SKIPPING_PTR_RDATA = 7;
+    public static final int ERROR_READING_SRV_RDATA = 8;
+    public static final int ERROR_SKIPPING_SRV_RDATA = 9;
+    public static final int ERROR_READING_TXT_RDATA = 10;
+    public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
+    public static final int ERROR_END_OF_FILE = 12;
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
new file mode 100644
index 0000000..6e90d2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * API configuration parameters for searching the mDNS service.
+ *
+ * <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
+ *
+ * @hide
+ */
+public class MdnsSearchOptions implements Parcelable {
+
+    /** @hide */
+    public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
+            new Parcelable.Creator<MdnsSearchOptions>() {
+                @Override
+                public MdnsSearchOptions createFromParcel(Parcel source) {
+                    return new MdnsSearchOptions(source.createStringArrayList(),
+                            source.readBoolean());
+                }
+
+                @Override
+                public MdnsSearchOptions[] newArray(int size) {
+                    return new MdnsSearchOptions[size];
+                }
+            };
+    private static MdnsSearchOptions defaultOptions;
+    private final List<String> subtypes;
+
+    private final boolean isPassiveMode;
+
+    /** Parcelable constructs for a {@link MdnsServiceInfo}. */
+    MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
+        this.subtypes = new ArrayList<>();
+        if (subtypes != null) {
+            this.subtypes.addAll(subtypes);
+        }
+        this.isPassiveMode = isPassiveMode;
+    }
+
+    /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** Returns a default search options. */
+    public static synchronized MdnsSearchOptions getDefaultOptions() {
+        if (defaultOptions == null) {
+            defaultOptions = newBuilder().build();
+        }
+        return defaultOptions;
+    }
+
+    /** @return the list of subtypes to search. */
+    public List<String> getSubtypes() {
+        return subtypes;
+    }
+
+    /**
+     * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
+     * order to conserve battery and produce less network traffic.
+     */
+    public boolean isPassiveMode() {
+        return isPassiveMode;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStringList(subtypes);
+        out.writeBoolean(isPassiveMode);
+    }
+
+    /** A builder to create {@link MdnsSearchOptions}. */
+    public static final class Builder {
+        private final Set<String> subtypes;
+        private boolean isPassiveMode = true;
+
+        private Builder() {
+            subtypes = new ArraySet<>();
+        }
+
+        /**
+         * Adds a subtype to search.
+         *
+         * @param subtype the subtype to add.
+         */
+        public Builder addSubtype(@NonNull String subtype) {
+            if (TextUtils.isEmpty(subtype)) {
+                throw new IllegalArgumentException("Empty subtype");
+            }
+            subtypes.add(subtype);
+            return this;
+        }
+
+        /**
+         * Adds a set of subtypes to search.
+         *
+         * @param subtypes The list of subtypes to add.
+         */
+        public Builder addSubtypes(@NonNull Collection<String> subtypes) {
+            this.subtypes.addAll(Objects.requireNonNull(subtypes));
+            return this;
+        }
+
+        /**
+         * Sets if the passive mode scan should be used. The passive mode scans less frequently in
+         * order
+         * to conserve battery and produce less network traffic.
+         *
+         * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
+         *                      false}, active mode will be used.
+         */
+        public Builder setIsPassiveMode(boolean isPassiveMode) {
+            this.isPassiveMode = isPassiveMode;
+            return this;
+        }
+
+        /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
+        public MdnsSearchOptions build() {
+            return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
new file mode 100644
index 0000000..53e58d1
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Listener interface for mDNS service instance discovery events.
+ *
+ * @hide
+ */
+public interface MdnsServiceBrowserListener {
+
+    /**
+     * Called when an mDNS service instance is found.
+     *
+     * @param serviceInfo The found mDNS service instance.
+     */
+    void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when an mDNS service instance is updated.
+     *
+     * @param serviceInfo The updated mDNS service instance.
+     */
+    void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when an mDNS service instance is no longer valid and removed.
+     *
+     * @param serviceInstanceName The service instance name of the removed mDNS service.
+     */
+    void onServiceRemoved(@NonNull String serviceInstanceName);
+
+    /**
+     * Called when searching for mDNS service has stopped because of an error.
+     *
+     * TODO (changed when importing code): define error constants
+     *
+     * @param error The error code of the stop reason.
+     */
+    void onSearchStoppedWithError(int error);
+
+    /** Called when it failed to start an mDNS service discovery process. */
+    void onSearchFailedToStart();
+
+    /**
+     * Called when a mDNS service discovery query has been sent.
+     *
+     * @param subtypes      The list of subtypes in the discovery query.
+     * @param transactionId The transaction ID of the query.
+     */
+    void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
+
+    /**
+     * Called when an error has happened when parsing a received mDNS response packet.
+     *
+     * @param receivedPacketNumber The packet sequence number of the received packet.
+     * @param errorCode            The error code, defined in {@link MdnsResponseErrorCode}.
+     */
+    void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
new file mode 100644
index 0000000..2e4a4e5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class representing a discovered mDNS service instance.
+ *
+ * @hide
+ */
+public class MdnsServiceInfo implements Parcelable {
+
+    /** @hide */
+    public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
+            new Parcelable.Creator<MdnsServiceInfo>() {
+
+                @Override
+                public MdnsServiceInfo createFromParcel(Parcel source) {
+                    return new MdnsServiceInfo(
+                            source.readString(),
+                            source.createStringArray(),
+                            source.createStringArrayList(),
+                            source.createStringArray(),
+                            source.readInt(),
+                            source.readString(),
+                            source.readString(),
+                            source.createStringArrayList());
+                }
+
+                @Override
+                public MdnsServiceInfo[] newArray(int size) {
+                    return new MdnsServiceInfo[size];
+                }
+            };
+
+    private final String serviceInstanceName;
+    private final String[] serviceType;
+    private final List<String> subtypes;
+    private final String[] hostName;
+    private final int port;
+    private final String ipv4Address;
+    private final String ipv6Address;
+    private final Map<String, String> attributes = new HashMap<>();
+    List<String> textStrings;
+
+    /**
+     * Constructs a {@link MdnsServiceInfo} object with default values.
+     *
+     * @hide
+     */
+    public MdnsServiceInfo(
+            String serviceInstanceName,
+            String[] serviceType,
+            List<String> subtypes,
+            String[] hostName,
+            int port,
+            String ipv4Address,
+            String ipv6Address,
+            List<String> textStrings) {
+        this.serviceInstanceName = serviceInstanceName;
+        this.serviceType = serviceType;
+        this.subtypes = new ArrayList<>();
+        if (subtypes != null) {
+            this.subtypes.addAll(subtypes);
+        }
+        this.hostName = hostName;
+        this.port = port;
+        this.ipv4Address = ipv4Address;
+        this.ipv6Address = ipv6Address;
+        if (textStrings != null) {
+            for (String text : textStrings) {
+                int pos = text.indexOf('=');
+                if (pos < 1) {
+                    continue;
+                }
+                attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
+                        text.substring(++pos));
+            }
+        }
+    }
+
+    /** @return the name of this service instance. */
+    public String getServiceInstanceName() {
+        return serviceInstanceName;
+    }
+
+    /** @return the type of this service instance. */
+    public String[] getServiceType() {
+        return serviceType;
+    }
+
+    /** @return the list of subtypes supported by this service instance. */
+    public List<String> getSubtypes() {
+        return new ArrayList<>(subtypes);
+    }
+
+    /**
+     * @return {@code true} if this service instance supports any subtypes.
+     * @return {@code false} if this service instance does not support any subtypes.
+     */
+    public boolean hasSubtypes() {
+        return !subtypes.isEmpty();
+    }
+
+    /** @return the host name of this service instance. */
+    public String[] getHostName() {
+        return hostName;
+    }
+
+    /** @return the port number of this service instance. */
+    public int getPort() {
+        return port;
+    }
+
+    /** @return the IPV4 address of this service instance. */
+    public String getIpv4Address() {
+        return ipv4Address;
+    }
+
+    /** @return the IPV6 address of this service instance. */
+    public String getIpv6Address() {
+        return ipv6Address;
+    }
+
+    /**
+     * @return the attribute value for {@code key}.
+     * @return {@code null} if no attribute value exists for {@code key}.
+     */
+    public String getAttributeByKey(@NonNull String key) {
+        return attributes.get(key.toLowerCase(Locale.ENGLISH));
+    }
+
+    /** @return an immutable map of all attributes. */
+    public Map<String, String> getAttributes() {
+        return Collections.unmodifiableMap(attributes);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        if (textStrings == null) {
+            // Lazily initialize the parcelable field mTextStrings.
+            textStrings = new ArrayList<>(attributes.size());
+            for (Map.Entry<String, String> kv : attributes.entrySet()) {
+                textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
+            }
+        }
+
+        out.writeString(serviceInstanceName);
+        out.writeStringArray(serviceType);
+        out.writeStringList(subtypes);
+        out.writeStringArray(hostName);
+        out.writeInt(port);
+        out.writeString(ipv4Address);
+        out.writeString(ipv6Address);
+        out.writeStringList(textStrings);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.ROOT,
+                "Name: %s, subtypes: %s, ip: %s, port: %d",
+                serviceInstanceName,
+                TextUtils.join(",", subtypes),
+                ipv4Address,
+                port);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
new file mode 100644
index 0000000..51de3b2
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "SRV" record, which contains service information. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsServiceRecord extends MdnsRecord {
+    public static final int PROTO_NONE = 0;
+    public static final int PROTO_TCP = 1;
+    public static final int PROTO_UDP = 2;
+    private static final String PROTO_TOKEN_TCP = "_tcp";
+    private static final String PROTO_TOKEN_UDP = "_udp";
+    private int servicePriority;
+    private int serviceWeight;
+    private int servicePort;
+    private String[] serviceHost;
+
+    public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        super(name, TYPE_SRV, reader);
+    }
+
+    /** Returns the service's port number. */
+    public int getServicePort() {
+        return servicePort;
+    }
+
+    /** Returns the service's host name. */
+    public String[] getServiceHost() {
+        return serviceHost;
+    }
+
+    /** Returns the service's priority. */
+    public int getServicePriority() {
+        return servicePriority;
+    }
+
+    /** Returns the service's weight. */
+    public int getServiceWeight() {
+        return serviceWeight;
+    }
+
+    // Format of name is <instance-name>.<service-name>.<protocol>.<domain>
+
+    /** Returns the service's instance name, which uniquely identifies the service instance. */
+    public String getServiceInstanceName() {
+        if (name.length < 1) {
+            return null;
+        }
+        return name[0];
+    }
+
+    /** Returns the service's name. */
+    public String getServiceName() {
+        if (name.length < 2) {
+            return null;
+        }
+        return name[1];
+    }
+
+    /** Returns the service's protocol. */
+    public int getServiceProtocol() {
+        if (name.length < 3) {
+            return PROTO_NONE;
+        }
+
+        String protocol = name[2];
+        if (protocol.equals(PROTO_TOKEN_TCP)) {
+            return PROTO_TCP;
+        }
+        if (protocol.equals(PROTO_TOKEN_UDP)) {
+            return PROTO_UDP;
+        }
+        return PROTO_NONE;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        servicePriority = reader.readUInt16();
+        serviceWeight = reader.readUInt16();
+        servicePort = reader.readUInt16();
+        serviceHost = reader.readLabels();
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        writer.writeUInt16(servicePriority);
+        writer.writeUInt16(serviceWeight);
+        writer.writeUInt16(servicePort);
+        writer.writeLabels(serviceHost);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.ROOT,
+                "SRV: %s:%d (prio=%d, weight=%d)",
+                labelsToString(serviceHost),
+                servicePort,
+                servicePriority,
+                serviceWeight);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31)
+                + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+                servicePort);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsServiceRecord)) {
+            return false;
+        }
+        MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
+
+        return super.equals(other)
+                && (servicePriority == otherRecord.servicePriority)
+                && (serviceWeight == otherRecord.serviceWeight)
+                && Objects.equals(serviceHost, otherRecord.serviceHost)
+                && (servicePort == otherRecord.servicePort);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
new file mode 100644
index 0000000..c3a86e3
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Instance of this class sends and receives mDNS packets of a given service type and invoke
+ * registered {@link MdnsServiceBrowserListener} instances.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsServiceTypeClient {
+
+    private static final int DEFAULT_MTU = 1500;
+    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsServiceTypeClient");
+
+    private final String serviceType;
+    private final String[] serviceTypeLabels;
+    private final MdnsSocketClient socketClient;
+    private final ScheduledExecutorService executor;
+    private final Object lock = new Object();
+    private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
+    private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+
+    // The session ID increases when startSendAndReceive() is called where we schedule a
+    // QueryTask for
+    // new subtypes. It stays the same between packets for same subtypes.
+    private long currentSessionId = 0;
+
+    @GuardedBy("lock")
+    private Future<?> requestTaskFuture;
+
+    /**
+     * Constructor of {@link MdnsServiceTypeClient}.
+     *
+     * @param socketClient Sends and receives mDNS packet.
+     * @param executor     A {@link ScheduledExecutorService} used to schedule query tasks.
+     */
+    public MdnsServiceTypeClient(
+            @NonNull String serviceType,
+            @NonNull MdnsSocketClient socketClient,
+            @NonNull ScheduledExecutorService executor) {
+        this.serviceType = serviceType;
+        this.socketClient = socketClient;
+        this.executor = executor;
+        serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+    }
+
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
+            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+        String[] hostName = response.getServiceRecord().getServiceHost();
+        int port = response.getServiceRecord().getServicePort();
+
+        String ipv4Address = null;
+        String ipv6Address = null;
+        if (response.hasInet4AddressRecord()) {
+            ipv4Address = response.getInet4AddressRecord().getInet4Address().getHostAddress();
+        }
+        if (response.hasInet6AddressRecord()) {
+            ipv6Address = response.getInet6AddressRecord().getInet6Address().getHostAddress();
+        }
+        // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+        return new MdnsServiceInfo(
+                response.getServiceInstanceName(),
+                serviceTypeLabels,
+                response.getSubtypes(),
+                hostName,
+                port,
+                ipv4Address,
+                ipv6Address,
+                response.getTextRecord().getStrings());
+    }
+
+    /**
+     * Registers {@code listener} for receiving discovery event of mDNS service instances, and
+     * starts
+     * (or continue) to send mDNS queries periodically.
+     *
+     * @param listener      The {@link MdnsServiceBrowserListener} to register.
+     * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
+     */
+    public void startSendAndReceive(
+            @NonNull MdnsServiceBrowserListener listener,
+            @NonNull MdnsSearchOptions searchOptions) {
+        synchronized (lock) {
+            if (!listeners.contains(listener)) {
+                listeners.add(listener);
+                for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+                    if (existingResponse.isComplete()) {
+                        listener.onServiceFound(
+                                buildMdnsServiceInfoFromResponse(existingResponse,
+                                        serviceTypeLabels));
+                    }
+                }
+            }
+            // Cancel the next scheduled periodical task.
+            if (requestTaskFuture != null) {
+                requestTaskFuture.cancel(true);
+            }
+            // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
+            // interested anymore.
+            requestTaskFuture =
+                    executor.submit(
+                            new QueryTask(
+                                    new QueryTaskConfig(
+                                            searchOptions.getSubtypes(),
+                                            searchOptions.isPassiveMode(),
+                                            ++currentSessionId)));
+        }
+    }
+
+    /**
+     * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
+     *
+     * @param listener The {@link MdnsServiceBrowserListener} to unregister.
+     * @return {@code true} if no listener is registered with this client after unregistering {@code
+     * listener}. Otherwise returns {@code false}.
+     */
+    public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
+        synchronized (lock) {
+            listeners.remove(listener);
+            if (listeners.isEmpty() && requestTaskFuture != null) {
+                requestTaskFuture.cancel(true);
+                requestTaskFuture = null;
+            }
+            return listeners.isEmpty();
+        }
+    }
+
+    public String[] getServiceTypeLabels() {
+        return serviceTypeLabels;
+    }
+
+    public synchronized void processResponse(@NonNull MdnsResponse response) {
+        if (response.isGoodbye()) {
+            onGoodbyeReceived(response.getServiceInstanceName());
+        } else {
+            onResponseReceived(response);
+        }
+    }
+
+    public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+        for (MdnsServiceBrowserListener listener : listeners) {
+            listener.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+        }
+    }
+
+    private void onResponseReceived(@NonNull MdnsResponse response) {
+        MdnsResponse currentResponse;
+        currentResponse = instanceNameToResponse.get(response.getServiceInstanceName());
+
+        boolean newServiceFound = false;
+        boolean existingServiceChanged = false;
+        if (currentResponse == null) {
+            newServiceFound = true;
+            currentResponse = response;
+            instanceNameToResponse.put(response.getServiceInstanceName(), currentResponse);
+        } else if (currentResponse.mergeRecordsFrom(response)) {
+            existingServiceChanged = true;
+        }
+        if (!currentResponse.isComplete() || (!newServiceFound && !existingServiceChanged)) {
+            return;
+        }
+        MdnsServiceInfo serviceInfo =
+                buildMdnsServiceInfoFromResponse(currentResponse, serviceTypeLabels);
+
+        for (MdnsServiceBrowserListener listener : listeners) {
+            if (newServiceFound) {
+                listener.onServiceFound(serviceInfo);
+            } else {
+                listener.onServiceUpdated(serviceInfo);
+            }
+        }
+    }
+
+    private void onGoodbyeReceived(@NonNull String serviceInstanceName) {
+        instanceNameToResponse.remove(serviceInstanceName);
+        for (MdnsServiceBrowserListener listener : listeners) {
+            listener.onServiceRemoved(serviceInstanceName);
+        }
+    }
+
+    @VisibleForTesting
+    MdnsPacketWriter createMdnsPacketWriter() {
+        return new MdnsPacketWriter(DEFAULT_MTU);
+    }
+
+    // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+    // Call to getConfigForNextRun returns a config that can be used to build the next query task.
+    @VisibleForTesting
+    static class QueryTaskConfig {
+
+        private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+                (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+        private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+        private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+        private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+                (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+        private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+                (int) MdnsConfigs.queriesPerBurstPassive();
+        private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+        // The following fields are used by QueryTask so we need to test them.
+        @VisibleForTesting
+        final List<String> subtypes;
+        private final boolean alwaysAskForUnicastResponse =
+                MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
+        private final boolean usePassiveMode;
+        private final long sessionId;
+        @VisibleForTesting
+        int transactionId;
+        @VisibleForTesting
+        boolean expectUnicastResponse;
+        private int queriesPerBurst;
+        private int timeBetweenBurstsInMs;
+        private int burstCounter;
+        private int timeToRunNextTaskInMs;
+        private boolean isFirstBurst;
+
+        QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
+                long sessionId) {
+            this.usePassiveMode = usePassiveMode;
+            this.subtypes = new ArrayList<>(subtypes);
+            this.queriesPerBurst = QUERIES_PER_BURST;
+            this.burstCounter = 0;
+            this.transactionId = 1;
+            this.expectUnicastResponse = true;
+            this.isFirstBurst = true;
+            this.sessionId = sessionId;
+            // Config the scan frequency based on the scan mode.
+            if (this.usePassiveMode) {
+                // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
+                // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+                // queries.
+                this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+            } else {
+                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+                this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+            }
+        }
+
+        QueryTaskConfig getConfigForNextRun() {
+            if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
+                transactionId = 1;
+            }
+            // Only the first query expects uni-cast response.
+            expectUnicastResponse = false;
+            if (++burstCounter == queriesPerBurst) {
+                burstCounter = 0;
+
+                if (alwaysAskForUnicastResponse) {
+                    expectUnicastResponse = true;
+                }
+                // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
+                // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+                // queries.
+                if (isFirstBurst) {
+                    isFirstBurst = false;
+                    if (usePassiveMode) {
+                        queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+                    }
+                }
+                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+                timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+                if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
+                    timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+                            TIME_BETWEEN_BURSTS_MS);
+                }
+            } else {
+                timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            }
+            return this;
+        }
+    }
+
+    // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
+    private class QueryTask implements Runnable {
+
+        private final QueryTaskConfig config;
+
+        QueryTask(@NonNull QueryTaskConfig config) {
+            this.config = config;
+        }
+
+        @Override
+        public void run() {
+            Pair<Integer, List<String>> result;
+            try {
+                result =
+                        new EnqueueMdnsQueryCallable(
+                                socketClient,
+                                createMdnsPacketWriter(),
+                                serviceType,
+                                config.subtypes,
+                                config.expectUnicastResponse,
+                                config.transactionId)
+                                .call();
+            } catch (Exception e) {
+                LOGGER.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
+                        TextUtils.join(",", config.subtypes)), e);
+                result = null;
+            }
+            synchronized (lock) {
+                if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
+                    // In case that the task is not canceled successfully, use session ID to check
+                    // if this task should continue to schedule more.
+                    if (config.sessionId != currentSessionId) {
+                        return;
+                    }
+                }
+
+                if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
+                    if (requestTaskFuture == null) {
+                        // If requestTaskFuture is set to null, the task is cancelled. We can't use
+                        // isCancelled() here because this QueryTask is different from the future
+                        // that is returned from executor.schedule(). See b/71646910.
+                        return;
+                    }
+                }
+                if ((result != null)) {
+                    for (MdnsServiceBrowserListener listener : listeners) {
+                        listener.onDiscoveryQuerySent(result.second, result.first);
+                    }
+                }
+                QueryTaskConfig config = this.config.getConfigForNextRun();
+                requestTaskFuture =
+                        executor.schedule(
+                                new QueryTask(config), config.timeToRunNextTaskInMs,
+                                TimeUnit.MILLISECONDS);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
new file mode 100644
index 0000000..241a52a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.util.List;
+
+/**
+ * {@link MdnsSocket} provides a similar interface to {@link MulticastSocket} and binds to all
+ * available multi-cast network interfaces.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocket {
+    private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+    private static boolean isOnIPv6OnlyNetwork = false;
+    private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
+    private final MulticastSocket multicastSocket;
+
+    public MdnsSocket(
+            @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+            throws IOException {
+        this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
+        this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
+        multicastSocket = createMulticastSocket(port);
+        // RFC Spec: https://tools.ietf.org/html/rfc6762
+        // Time to live is set 255, which is similar to the jMDNS implementation.
+        multicastSocket.setTimeToLive(255);
+
+        // TODO (changed when importing code): consider tagging the socket for data usage
+        isOnIPv6OnlyNetwork = false;
+    }
+
+    public void send(DatagramPacket packet) throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.setNetworkInterface(networkInterface.getNetworkInterface());
+            multicastSocket.send(packet);
+        }
+    }
+
+    public void receive(DatagramPacket packet) throws IOException {
+        multicastSocket.receive(packet);
+    }
+
+    public void joinGroup() throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+        if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+            isOnIPv6OnlyNetwork = true;
+            multicastAddress = MULTICAST_IPV6_ADDRESS;
+        } else {
+            isOnIPv6OnlyNetwork = false;
+        }
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+        }
+    }
+
+    public void leaveGroup() throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+        if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+            multicastAddress = MULTICAST_IPV6_ADDRESS;
+        }
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+        }
+    }
+
+    public void close() {
+        // This is a race with the use of the file descriptor (b/27403984).
+        multicastSocket.close();
+        multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
+    }
+
+    @VisibleForTesting
+    MulticastSocket createMulticastSocket(int port) throws IOException {
+        return new MulticastSocket(port);
+    }
+
+    public boolean isOnIPv6OnlyNetwork() {
+        return isOnIPv6OnlyNetwork;
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
new file mode 100644
index 0000000..e689d6c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link MdnsSocketClient} maintains separate threads to send and receive mDNS packets for all
+ * the requested service types.
+ *
+ * <p>See https://tools.ietf.org/html/rfc6763 (namely sections 4 and 5).
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocketClient {
+
+    private static final String TAG = "MdnsClient";
+    // TODO: The following values are copied from cast module. We need to think about the
+    // better way to share those.
+    private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
+    private static final String CAST_PREFS_NAME = "google_cast";
+    private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final String MULTICAST_TYPE = "multicast";
+    private static final String UNICAST_TYPE = "unicast";
+
+    private static final long SLEEP_TIME_FOR_SOCKET_THREAD_MS =
+            MdnsConfigs.sleepTimeForSocketThreadMs();
+    // A value of 0 leads to an infinite wait.
+    private static final long THREAD_JOIN_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
+    private static final int RECEIVER_BUFFER_SIZE = 2048;
+    @VisibleForTesting
+    final Queue<DatagramPacket> multicastPacketQueue = new ArrayDeque<>();
+    @VisibleForTesting
+    final Queue<DatagramPacket> unicastPacketQueue = new ArrayDeque<>();
+    private final Context context;
+    private final byte[] multicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+    private final byte[] unicastReceiverBuffer;
+    private final MdnsResponseDecoder responseDecoder;
+    private final MulticastLock multicastLock;
+    private final boolean useSeparateSocketForUnicast =
+            MdnsConfigs.useSeparateSocketToSendUnicastQuery();
+    private final boolean checkMulticastResponse = MdnsConfigs.checkMulticastResponse();
+    private final long checkMulticastResponseIntervalMs =
+            MdnsConfigs.checkMulticastResponseIntervalMs();
+    private final Object socketLock = new Object();
+    private final Object timerObject = new Object();
+    // If multicast response was received in the current session. The value is reset in the
+    // beginning of each session.
+    @VisibleForTesting
+    boolean receivedMulticastResponse;
+    // If unicast response was received in the current session. The value is reset in the beginning
+    // of each session.
+    @VisibleForTesting
+    boolean receivedUnicastResponse;
+    // If the phone is the bad state where it can't receive any multicast response.
+    @VisibleForTesting
+    AtomicBoolean cannotReceiveMulticastResponse = new AtomicBoolean(false);
+    @VisibleForTesting
+    volatile Thread sendThread;
+    @VisibleForTesting
+    Thread multicastReceiveThread;
+    @VisibleForTesting
+    Thread unicastReceiveThread;
+    private volatile boolean shouldStopSocketLoop;
+    private Callback callback;
+    private MdnsSocket multicastSocket;
+    private MdnsSocket unicastSocket;
+    private int receivedPacketNumber = 0;
+    private Timer logMdnsPacketTimer;
+    private AtomicInteger packetsCount;
+    private Timer checkMulticastResponseTimer;
+
+    public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock) {
+        this.context = context;
+        this.multicastLock = multicastLock;
+        responseDecoder = new MdnsResponseDecoder(new MdnsResponseDecoder.Clock(), null);
+        if (useSeparateSocketForUnicast) {
+            unicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+        } else {
+            unicastReceiverBuffer = null;
+        }
+    }
+
+    public synchronized void setCallback(@Nullable Callback callback) {
+        this.callback = callback;
+    }
+
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public synchronized void startDiscovery() throws IOException {
+        if (multicastSocket != null) {
+            LOGGER.w("Discovery is already in progress.");
+            return;
+        }
+
+        receivedMulticastResponse = false;
+        receivedUnicastResponse = false;
+        cannotReceiveMulticastResponse.set(false);
+
+        shouldStopSocketLoop = false;
+        try {
+            // TODO (changed when importing code): consider setting thread stats tag
+            multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+            multicastSocket.joinGroup();
+            if (useSeparateSocketForUnicast) {
+                // For unicast, use port 0 and the system will assign it with any available port.
+                unicastSocket = createMdnsSocket(0);
+            }
+            multicastLock.acquire();
+        } catch (IOException e) {
+            multicastLock.release();
+            if (multicastSocket != null) {
+                multicastSocket.close();
+                multicastSocket = null;
+            }
+            if (unicastSocket != null) {
+                unicastSocket.close();
+                unicastSocket = null;
+            }
+            throw e;
+        } finally {
+            // TODO (changed when importing code): consider resetting thread stats tag
+        }
+        createAndStartSendThread();
+        createAndStartReceiverThreads();
+    }
+
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public void stopDiscovery() {
+        LOGGER.log("Stop discovery.");
+        if (multicastSocket == null && unicastSocket == null) {
+            return;
+        }
+
+        if (MdnsConfigs.clearMdnsPacketQueueAfterDiscoveryStops()) {
+            synchronized (multicastPacketQueue) {
+                multicastPacketQueue.clear();
+            }
+            synchronized (unicastPacketQueue) {
+                unicastPacketQueue.clear();
+            }
+        }
+
+        multicastLock.release();
+
+        shouldStopSocketLoop = true;
+        waitForSendThreadToStop();
+        waitForReceiverThreadsToStop();
+
+        synchronized (socketLock) {
+            multicastSocket = null;
+            unicastSocket = null;
+        }
+
+        synchronized (timerObject) {
+            if (checkMulticastResponseTimer != null) {
+                checkMulticastResponseTimer.cancel();
+                checkMulticastResponseTimer = null;
+            }
+        }
+    }
+
+    /** Sends a mDNS request packet that asks for multicast response. */
+    public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+        sendMdnsPacket(packet, multicastPacketQueue);
+    }
+
+    /** Sends a mDNS request packet that asks for unicast response. */
+    public void sendUnicastPacket(DatagramPacket packet) {
+        if (useSeparateSocketForUnicast) {
+            sendMdnsPacket(packet, unicastPacketQueue);
+        } else {
+            sendMdnsPacket(packet, multicastPacketQueue);
+        }
+    }
+
+    private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+        if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
+            LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+            return;
+        }
+        synchronized (packetQueueToUse) {
+            while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+                packetQueueToUse.remove();
+            }
+            packetQueueToUse.add(packet);
+        }
+        triggerSendThread();
+    }
+
+    private void createAndStartSendThread() {
+        if (sendThread != null) {
+            LOGGER.w("A socket thread already exists.");
+            return;
+        }
+        sendThread = new Thread(this::sendThreadMain);
+        sendThread.setName("mdns-send");
+        sendThread.start();
+    }
+
+    private void createAndStartReceiverThreads() {
+        if (multicastReceiveThread != null) {
+            LOGGER.w("A multicast receiver thread already exists.");
+            return;
+        }
+        multicastReceiveThread =
+                new Thread(() -> receiveThreadMain(multicastReceiverBuffer, multicastSocket));
+        multicastReceiveThread.setName("mdns-multicast-receive");
+        multicastReceiveThread.start();
+
+        if (useSeparateSocketForUnicast) {
+            unicastReceiveThread =
+                    new Thread(() -> receiveThreadMain(unicastReceiverBuffer, unicastSocket));
+            unicastReceiveThread.setName("mdns-unicast-receive");
+            unicastReceiveThread.start();
+        }
+    }
+
+    private void triggerSendThread() {
+        LOGGER.log("Trigger send thread.");
+        Thread sendThread = this.sendThread;
+        if (sendThread != null) {
+            sendThread.interrupt();
+        } else {
+            LOGGER.w("Socket thread is null");
+        }
+    }
+
+    private void waitForReceiverThreadsToStop() {
+        if (multicastReceiveThread != null) {
+            waitForThread(multicastReceiveThread);
+            multicastReceiveThread = null;
+        }
+
+        if (unicastReceiveThread != null) {
+            waitForThread(unicastReceiveThread);
+            unicastReceiveThread = null;
+        }
+    }
+
+    private void waitForSendThreadToStop() {
+        LOGGER.log("wait For Send Thread To Stop");
+        if (sendThread == null) {
+            LOGGER.w("socket thread is already dead.");
+            return;
+        }
+        waitForThread(sendThread);
+        sendThread = null;
+    }
+
+    private void waitForThread(Thread thread) {
+        long startMs = SystemClock.elapsedRealtime();
+        long waitMs = THREAD_JOIN_TIMEOUT_MS;
+        while (thread.isAlive() && (waitMs > 0)) {
+            try {
+                thread.interrupt();
+                thread.join(waitMs);
+                if (thread.isAlive()) {
+                    LOGGER.w("Failed to join thread: " + thread);
+                }
+                break;
+            } catch (InterruptedException e) {
+                // Compute remaining time after at least a single join call, in case the clock
+                // resolution is poor.
+                waitMs = THREAD_JOIN_TIMEOUT_MS - (SystemClock.elapsedRealtime() - startMs);
+            }
+        }
+    }
+
+    private void sendThreadMain() {
+        List<DatagramPacket> multicastPacketsToSend = new ArrayList<>();
+        List<DatagramPacket> unicastPacketsToSend = new ArrayList<>();
+        boolean shouldThreadSleep;
+        try {
+            while (!shouldStopSocketLoop) {
+                try {
+                    // Make a local copy of all packets, and clear the queue.
+                    // Send packets that ask for multicast response.
+                    multicastPacketsToSend.clear();
+                    synchronized (multicastPacketQueue) {
+                        multicastPacketsToSend.addAll(multicastPacketQueue);
+                        multicastPacketQueue.clear();
+                    }
+
+                    // Send packets that ask for unicast response.
+                    if (useSeparateSocketForUnicast) {
+                        unicastPacketsToSend.clear();
+                        synchronized (unicastPacketQueue) {
+                            unicastPacketsToSend.addAll(unicastPacketQueue);
+                            unicastPacketQueue.clear();
+                        }
+                    }
+
+                    // Send all the packets.
+                    sendPackets(multicastPacketsToSend, multicastSocket);
+                    sendPackets(unicastPacketsToSend, unicastSocket);
+
+                    // Sleep ONLY if no more packets have been added to the queue, while packets
+                    // were being sent.
+                    synchronized (multicastPacketQueue) {
+                        synchronized (unicastPacketQueue) {
+                            shouldThreadSleep =
+                                    multicastPacketQueue.isEmpty() && unicastPacketQueue.isEmpty();
+                        }
+                    }
+                    if (shouldThreadSleep) {
+                        Thread.sleep(SLEEP_TIME_FOR_SOCKET_THREAD_MS);
+                    }
+                } catch (InterruptedException e) {
+                    // Don't log the interruption as it's expected.
+                }
+            }
+        } finally {
+            LOGGER.log("Send thread stopped.");
+            try {
+                multicastSocket.leaveGroup();
+            } catch (Exception t) {
+                LOGGER.e("Failed to leave the group.", t);
+            }
+
+            // Close the socket first. This is the only way to interrupt a blocking receive.
+            try {
+                // This is a race with the use of the file descriptor (b/27403984).
+                multicastSocket.close();
+                if (unicastSocket != null) {
+                    unicastSocket.close();
+                }
+            } catch (Exception t) {
+                LOGGER.e("Failed to close the mdns socket.", t);
+            }
+        }
+    }
+
+    private void receiveThreadMain(byte[] receiverBuffer, MdnsSocket socket) {
+        DatagramPacket packet = new DatagramPacket(receiverBuffer, receiverBuffer.length);
+
+        while (!shouldStopSocketLoop) {
+            try {
+                // This is a race with the use of the file descriptor (b/27403984).
+                synchronized (socketLock) {
+                    // This checks is to make sure the socket was not set to null.
+                    if (socket != null && (socket == multicastSocket || socket == unicastSocket)) {
+                        socket.receive(packet);
+                    }
+                }
+
+                if (!shouldStopSocketLoop) {
+                    String responseType = socket == multicastSocket ? MULTICAST_TYPE : UNICAST_TYPE;
+                    processResponsePacket(packet, responseType);
+                }
+            } catch (IOException e) {
+                if (!shouldStopSocketLoop) {
+                    LOGGER.e("Failed to receive mDNS packets.", e);
+                }
+            }
+        }
+        LOGGER.log("Receive thread stopped.");
+    }
+
+    private int processResponsePacket(@NonNull DatagramPacket packet, String responseType)
+            throws IOException {
+        int packetNumber = ++receivedPacketNumber;
+
+        List<MdnsResponse> responses = new LinkedList<>();
+        int errorCode = responseDecoder.decode(packet, responses);
+        if (errorCode == MdnsResponseDecoder.SUCCESS) {
+            if (responseType.equals(MULTICAST_TYPE)) {
+                receivedMulticastResponse = true;
+                if (cannotReceiveMulticastResponse.getAndSet(false)) {
+                    // If we are already in the bad state, receiving a multicast response means
+                    // we are recovered.
+                    LOGGER.e(
+                            "Recovered from the state where the phone can't receive any multicast"
+                                    + " response");
+                }
+            } else {
+                receivedUnicastResponse = true;
+            }
+            for (MdnsResponse response : responses) {
+                String serviceInstanceName = response.getServiceInstanceName();
+                LOGGER.log("mDNS %s response received: %s", responseType, serviceInstanceName);
+                if (callback != null) {
+                    callback.onResponseReceived(response);
+                }
+            }
+        } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+            LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+                    responseType, packetNumber, errorCode));
+            if (callback != null) {
+                callback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+            }
+        }
+        return errorCode;
+    }
+
+    @VisibleForTesting
+    MdnsSocket createMdnsSocket(int port) throws IOException {
+        return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+    }
+
+    private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
+        String requestType = socket == multicastSocket ? "multicast" : "unicast";
+        for (DatagramPacket packet : packets) {
+            if (shouldStopSocketLoop) {
+                break;
+            }
+            try {
+                LOGGER.log("Sending a %s mDNS packet...", requestType);
+                socket.send(packet);
+
+                // Start the timer task to monitor the response.
+                synchronized (timerObject) {
+                    if (socket == multicastSocket) {
+                        if (cannotReceiveMulticastResponse.get()) {
+                            // Don't schedule the timer task if we are already in the bad state.
+                            return;
+                        }
+                        if (checkMulticastResponseTimer != null) {
+                            // Don't schedule the timer task if it's already scheduled.
+                            return;
+                        }
+                        if (checkMulticastResponse && useSeparateSocketForUnicast) {
+                            // Only when useSeparateSocketForUnicast is true, we can tell if we
+                            // received a multicast or unicast response.
+                            checkMulticastResponseTimer = new Timer();
+                            checkMulticastResponseTimer.schedule(
+                                    new TimerTask() {
+                                        @Override
+                                        public void run() {
+                                            synchronized (timerObject) {
+                                                if (checkMulticastResponseTimer == null) {
+                                                    // Discovery already stopped.
+                                                    return;
+                                                }
+                                                if ((!receivedMulticastResponse)
+                                                        && receivedUnicastResponse) {
+                                                    LOGGER.e(String.format(
+                                                            "Haven't received multicast response"
+                                                                    + " in the last %d ms.",
+                                                            checkMulticastResponseIntervalMs));
+                                                    cannotReceiveMulticastResponse.set(true);
+                                                }
+                                                checkMulticastResponseTimer = null;
+                                            }
+                                        }
+                                    },
+                                    checkMulticastResponseIntervalMs);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+            }
+        }
+        packets.clear();
+    }
+
+    public boolean isOnIPv6OnlyNetwork() {
+        return multicastSocket.isOnIPv6OnlyNetwork();
+    }
+
+    /** Callback for {@link MdnsSocketClient}. */
+    public interface Callback {
+        void onResponseReceived(@NonNull MdnsResponse response);
+
+        void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
new file mode 100644
index 0000000..a5b5595
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** An mDNS "TXT" record, which contains a list of text strings. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsTextRecord extends MdnsRecord {
+    private List<String> strings;
+
+    public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        super(name, TYPE_TXT, reader);
+    }
+
+    /** Returns the list of strings. */
+    public List<String> getStrings() {
+        return Collections.unmodifiableList(strings);
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        strings = new ArrayList<>();
+        while (reader.getRemaining() > 0) {
+            strings.add(reader.readString());
+        }
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        if (strings != null) {
+            for (String string : strings) {
+                writer.writeString(string);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("TXT: {");
+        if (strings != null) {
+            for (String string : strings) {
+                sb.append(' ').append(string);
+            }
+        }
+        sb.append("}");
+
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31) + Objects.hash(strings);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsTextRecord)) {
+            return false;
+        }
+
+        return super.equals(other) && Objects.equals(strings, ((MdnsTextRecord) other).strings);
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
new file mode 100644
index 0000000..e0d8fa6
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class is used by the {@link MdnsSocket} to monitor the list of {@link NetworkInterface}
+ * instances that are currently available for multi-cast messaging.
+ */
+public class MulticastNetworkInterfaceProvider {
+
+    private static final String TAG = "MdnsNIProvider";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
+
+    private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
+    // Only modifiable from tests.
+    @VisibleForTesting
+    ConnectivityMonitor connectivityMonitor;
+    private volatile boolean connectivityChanged = true;
+
+    @SuppressWarnings("nullness:methodref.receiver.bound")
+    public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+        // IMPORT CHANGED
+        this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
+                context, this::onConnectivityChanged);
+    }
+
+    private void onConnectivityChanged() {
+        connectivityChanged = true;
+    }
+
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    public void startWatchingConnectivityChanges() {
+        connectivityMonitor.startWatchingConnectivityChanges();
+    }
+
+    /** Stops monitoring changes of connectivity. */
+    public void stopWatchingConnectivityChanges() {
+        connectivityMonitor.stopWatchingConnectivityChanges();
+    }
+
+    /**
+     * Returns the list of {@link NetworkInterfaceWrapper} instances available for multi-cast
+     * messaging.
+     */
+    public synchronized List<NetworkInterfaceWrapper> getMulticastNetworkInterfaces() {
+        if (connectivityChanged) {
+            connectivityChanged = false;
+            updateMulticastNetworkInterfaces();
+            if (multicastNetworkInterfaces.isEmpty()) {
+                LOGGER.log("No network interface available for mDNS scanning.");
+            }
+        }
+        return new ArrayList<>(multicastNetworkInterfaces);
+    }
+
+    private void updateMulticastNetworkInterfaces() {
+        multicastNetworkInterfaces.clear();
+        List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
+        for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
+            if (canScanOnInterface(interfaceWrapper)) {
+                multicastNetworkInterfaces.add(interfaceWrapper);
+            }
+        }
+    }
+
+    public boolean isOnIpV6OnlyNetwork(List<NetworkInterfaceWrapper> networkInterfaces) {
+        if (networkInterfaces.isEmpty()) {
+            return false;
+        }
+
+        // TODO(b/79866499): Remove this when the bug is resolved.
+        if (PREFER_IPV6) {
+            return true;
+        }
+        boolean hasAtleastOneIPv6Address = false;
+        for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaces) {
+            for (InterfaceAddress ifAddr : interfaceWrapper.getInterfaceAddresses()) {
+                if (!(ifAddr.getAddress() instanceof Inet6Address)) {
+                    return false;
+                } else {
+                    hasAtleastOneIPv6Address = true;
+                }
+            }
+        }
+        return hasAtleastOneIPv6Address;
+    }
+
+    @VisibleForTesting
+    List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+        List<NetworkInterfaceWrapper> networkInterfaceWrappers = new ArrayList<>();
+        try {
+            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+            if (interfaces != null) {
+                while (interfaces.hasMoreElements()) {
+                    networkInterfaceWrappers.add(
+                            new NetworkInterfaceWrapper(interfaces.nextElement()));
+                }
+            }
+        } catch (SocketException e) {
+            LOGGER.e("Failed to get network interfaces.", e);
+        } catch (NullPointerException e) {
+            // Android R has a bug that could lead to a NPE. See b/159277702.
+            LOGGER.e("Failed to call getNetworkInterfaces API", e);
+        }
+
+        return networkInterfaceWrappers;
+    }
+
+    private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+        try {
+            if ((networkInterface == null)
+                    || networkInterface.isLoopback()
+                    || networkInterface.isPointToPoint()
+                    || networkInterface.isVirtual()
+                    || !networkInterface.isUp()
+                    || !networkInterface.supportsMulticast()) {
+                return false;
+            }
+            return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
+        } catch (IOException e) {
+            LOGGER.e(String.format("Failed to check interface %s.",
+                    networkInterface.getNetworkInterface().getDisplayName()), e);
+        }
+
+        return false;
+    }
+
+    private boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+        for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+            if (ifAddr.getAddress() instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+        for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+            if (ifAddr.getAddress() instanceof Inet6Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
new file mode 100644
index 0000000..0ecae48
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.List;
+
+/** A wrapper class of {@link NetworkInterface} to be mocked in unit tests. */
+public class NetworkInterfaceWrapper {
+    private final NetworkInterface networkInterface;
+
+    public NetworkInterfaceWrapper(NetworkInterface networkInterface) {
+        this.networkInterface = networkInterface;
+    }
+
+    public NetworkInterface getNetworkInterface() {
+        return networkInterface;
+    }
+
+    public boolean isUp() throws SocketException {
+        return networkInterface.isUp();
+    }
+
+    public boolean isLoopback() throws SocketException {
+        return networkInterface.isLoopback();
+    }
+
+    public boolean isPointToPoint() throws SocketException {
+        return networkInterface.isPointToPoint();
+    }
+
+    public boolean isVirtual() {
+        return networkInterface.isVirtual();
+    }
+
+    public boolean supportsMulticast() throws SocketException {
+        return networkInterface.supportsMulticast();
+    }
+
+    public List<InterfaceAddress> getInterfaceAddresses() {
+        return networkInterface.getInterfaceAddresses();
+    }
+
+    @Override
+    public String toString() {
+        return networkInterface.toString();
+    }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
new file mode 100644
index 0000000..431f1fd
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns.util;
+
+import android.text.TextUtils;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The logger used in mDNS.
+ */
+public class MdnsLogger {
+    // Make this logger public for other level logging than dogfood.
+    public final SharedLog mLog;
+
+    /**
+     * Constructs a new {@link MdnsLogger} with the given logging tag.
+     *
+     * @param tag The log tag that will be used by this logger
+     */
+    public MdnsLogger(String tag) {
+        mLog = new SharedLog(tag);
+    }
+
+    public void log(String message) {
+        mLog.log(message);
+    }
+
+    public void log(String message, Object... args) {
+        mLog.log(message + " ; " + TextUtils.join(" ; ", args));
+    }
+
+    public void d(String message) {
+        mLog.log(message);
+    }
+
+    public void e(String message) {
+        mLog.e(message);
+    }
+
+    public void e(String message, Throwable e) {
+        mLog.e(message, e);
+    }
+
+    public void w(String message) {
+        mLog.w(message);
+    }
+}
diff --git a/service/native/Android.bp b/service/native/Android.bp
index cb26bc3..697fcbd 100644
--- a/service/native/Android.bp
+++ b/service/native/Android.bp
@@ -52,7 +52,8 @@
 
 cc_test {
     name: "traffic_controller_unit_test",
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     require_root: true,
     local_include_dirs: ["include"],
     header_libs: [
@@ -71,4 +72,13 @@
         "libnetd_updatable",
         "netd_aidl_interface-lateststable-ndk",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
 }
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 3e98edb..9331548 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -56,7 +56,6 @@
 using bpf::BpfMap;
 using bpf::synchronizeKernelRCU;
 using netdutils::DumpWriter;
-using netdutils::getIfaceList;
 using netdutils::NetlinkListener;
 using netdutils::NetlinkListenerInterface;
 using netdutils::ScopedIndent;
@@ -74,6 +73,9 @@
 const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
 const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
 const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby";
+const char* TrafficController::LOCAL_OEM_DENY_1 = "fw_oem_deny_1";
+const char* TrafficController::LOCAL_OEM_DENY_2 = "fw_oem_deny_2";
+const char* TrafficController::LOCAL_OEM_DENY_3 = "fw_oem_deny_3";
 
 static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
               "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
@@ -88,7 +90,7 @@
         }                                   \
     } while (0)
 
-const std::string uidMatchTypeToString(uint8_t match) {
+const std::string uidMatchTypeToString(uint32_t match) {
     std::string matchType;
     FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
     FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
@@ -98,20 +100,16 @@
     FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
     FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
     FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+    FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
+    FLAG_MSG_TRANS(matchType, OEM_DENY_1_MATCH, match);
+    FLAG_MSG_TRANS(matchType, OEM_DENY_2_MATCH, match);
+    FLAG_MSG_TRANS(matchType, OEM_DENY_3_MATCH, match);
     if (match) {
         return StringPrintf("Unknown match: %u", match);
     }
     return matchType;
 }
 
-bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
-    // This implementation is the same logic as method ActivityManager#checkComponentPermission.
-    // It implies that the calling uid can never be the same as PER_USER_RANGE.
-    uint32_t appId = uid % PER_USER_RANGE;
-    return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
-            mPrivilegedUser.find(appId) != mPrivilegedUser.end());
-}
-
 const std::string UidPermissionTypeToString(int permission) {
     if (permission == INetd::PERMISSION_NONE) {
         return "PERMISSION_NONE";
@@ -183,6 +181,7 @@
     RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
     RETURN_IF_NOT_OK(mUidOwnerMap.clear());
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -190,16 +189,6 @@
 Status TrafficController::start() {
     RETURN_IF_NOT_OK(initMaps());
 
-    // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
-    // already running, so it will call addInterface() when any new interface appears.
-    // TODO: Clean-up addInterface() after interface monitoring is in
-    // NetworkStatsService.
-    std::map<std::string, uint32_t> ifacePairs;
-    ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
-    for (const auto& ifacePair:ifacePairs) {
-        addInterface(ifacePair.first.c_str(), ifacePair.second);
-    }
-
     auto result = makeSkDestroyListener();
     if (!isOk(result)) {
         ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
@@ -237,22 +226,6 @@
     return netdutils::status::ok;
 }
 
-int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
-    IfaceValue iface;
-    if (ifaceIndex == 0) {
-        ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
-        return -1;
-    }
-
-    strlcpy(iface.name, name, sizeof(IfaceValue));
-    Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
-    if (!isOk(res)) {
-        ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
-        return -res.code();
-    }
-    return 0;
-}
-
 Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
                                               FirewallType type) {
     std::lock_guard guard(mMutex);
@@ -272,7 +245,7 @@
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
                 .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+                .rule = oldMatch.value().rule & ~match,
         };
         if (newMatch.rule == 0) {
             RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
@@ -286,23 +259,20 @@
 }
 
 Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
-    // iif should be non-zero if and only if match == MATCH_IIF
-    if (match == IIF_MATCH && iif == 0) {
-        return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
-    } else if (match != IIF_MATCH && iif != 0) {
+    if (match != IIF_MATCH && iif != 0) {
         return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
     }
     auto oldMatch = mUidOwnerMap.readValue(uid);
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
-                .iif = iif ? iif : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+                .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
+                .rule = oldMatch.value().rule | match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     } else {
         UidOwnerValue newMatch = {
                 .iif = iif,
-                .rule = static_cast<uint8_t>(match),
+                .rule = match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     }
@@ -335,6 +305,12 @@
             return ALLOWLIST;
         case LOW_POWER_STANDBY:
             return ALLOWLIST;
+        case OEM_DENY_1:
+            return DENYLIST;
+        case OEM_DENY_2:
+            return DENYLIST;
+        case OEM_DENY_3:
+            return DENYLIST;
         case NONE:
         default:
             return DENYLIST;
@@ -360,6 +336,15 @@
         case LOW_POWER_STANDBY:
             res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
             break;
+        case OEM_DENY_1:
+            res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
+            break;
+        case OEM_DENY_2:
+            res = updateOwnerMapEntry(OEM_DENY_2_MATCH, uid, rule, type);
+            break;
+        case OEM_DENY_3:
+            res = updateOwnerMapEntry(OEM_DENY_3_MATCH, uid, rule, type);
+            break;
         case NONE:
         default:
             ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +384,6 @@
 
 Status TrafficController::addUidInterfaceRules(const int iif,
                                                const std::vector<int32_t>& uidsToAdd) {
-    if (!iif) {
-        return statusFromErrno(EINVAL, "Interface rule must specify interface");
-    }
     std::lock_guard guard(mMutex);
 
     for (auto uid : uidsToAdd) {
@@ -425,6 +407,18 @@
     return netdutils::status::ok;
 }
 
+Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
+    std::lock_guard guard(mMutex);
+
+    netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
+                               : removeRule(uid, LOCKDOWN_VPN_MATCH);
+    if (!isOk(result)) {
+        ALOGW("%s Lockdown rule failed(%d): uid=%d",
+              (add ? "add": "remove"), result.code(), uid);
+    }
+    return result;
+}
+
 int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
                                           const std::vector<int32_t>& uids) {
     // FirewallRule rule = isAllowlist ? ALLOW : DENY;
@@ -440,6 +434,12 @@
         res = replaceRulesInMap(RESTRICTED_MATCH, uids);
     } else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) {
         res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids);
+    } else if (!name.compare(LOCAL_OEM_DENY_1)) {
+        res = replaceRulesInMap(OEM_DENY_1_MATCH, uids);
+    } else if (!name.compare(LOCAL_OEM_DENY_2)) {
+        res = replaceRulesInMap(OEM_DENY_2_MATCH, uids);
+    } else if (!name.compare(LOCAL_OEM_DENY_3)) {
+        res = replaceRulesInMap(OEM_DENY_3_MATCH, uids);
     } else {
         ALOGE("unknown chain name: %s", name.c_str());
         return -EINVAL;
@@ -451,61 +451,21 @@
     return 0;
 }
 
-int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
-    std::lock_guard guard(mMutex);
-    uint32_t key = UID_RULES_CONFIGURATION_KEY;
-    auto oldConfiguration = mConfigurationMap.readValue(key);
-    if (!oldConfiguration.ok()) {
-        ALOGE("Cannot read the old configuration from map: %s",
-              oldConfiguration.error().message().c_str());
-        return -oldConfiguration.error().code();
-    }
-    Status res;
-    BpfConfig newConfiguration;
-    uint8_t match;
-    switch (chain) {
-        case DOZABLE:
-            match = DOZABLE_MATCH;
-            break;
-        case STANDBY:
-            match = STANDBY_MATCH;
-            break;
-        case POWERSAVE:
-            match = POWERSAVE_MATCH;
-            break;
-        case RESTRICTED:
-            match = RESTRICTED_MATCH;
-            break;
-        case LOW_POWER_STANDBY:
-            match = LOW_POWER_STANDBY_MATCH;
-            break;
-        default:
-            return -EINVAL;
-    }
-    newConfiguration =
-            enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
-    res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
-    if (!isOk(res)) {
-        ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
-    }
-    return -res.code();
-}
-
 Status TrafficController::swapActiveStatsMap() {
     std::lock_guard guard(mMutex);
 
     uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
-    auto oldConfiguration = mConfigurationMap.readValue(key);
-    if (!oldConfiguration.ok()) {
+    auto oldConfigure = mConfigurationMap.readValue(key);
+    if (!oldConfigure.ok()) {
         ALOGE("Cannot read the old configuration from map: %s",
-              oldConfiguration.error().message().c_str());
-        return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+              oldConfigure.error().message().c_str());
+        return Status(oldConfigure.error().code(), oldConfigure.error().message());
     }
 
     // Write to the configuration map to inform the kernel eBPF program to switch
     // from using one map to the other. Use flag BPF_EXIST here since the map should
     // be already populated in initMaps.
-    uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+    uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
     auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
                                             BPF_EXIST);
     if (!res.ok()) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..7730c13 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -30,12 +30,15 @@
 
 #include <gtest/gtest.h>
 
+#include <android-base/file.h>
+#include <android-base/logging.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <binder/Status.h>
 
 #include <netdutils/MockSyscalls.h>
 
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "TrafficController.h"
 #include "bpf/BpfUtils.h"
 #include "NetdUpdatablePublic.h"
@@ -48,6 +51,7 @@
 using android::netdutils::Status;
 using base::Result;
 using netdutils::isOk;
+using netdutils::statusFromErrno;
 
 constexpr int TEST_MAP_SIZE = 10;
 constexpr uid_t TEST_UID = 10086;
@@ -55,8 +59,16 @@
 constexpr uid_t TEST_UID3 = 98765;
 constexpr uint32_t TEST_TAG = 42;
 constexpr uint32_t TEST_COUNTERSET = 1;
+constexpr int TEST_COOKIE = 1;
+constexpr char TEST_IFNAME[] = "test0";
+constexpr int TEST_IFINDEX = 999;
+constexpr int RXPACKETS = 1;
+constexpr int RXBYTES = 100;
+constexpr int TXPACKETS = 0;
+constexpr int TXBYTES = 0;
 
 #define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+#define ASSERT_INVALID(x) ASSERT_FALSE((x).isValid())
 
 class TrafficControllerTest : public ::testing::Test {
   protected:
@@ -65,73 +77,90 @@
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMapB;  // makeTrafficControllerMapsInvalid only
+    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap; ;  // makeTrafficControllerMapsInvalid only
+    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
+    BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
+    BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
 
     void SetUp() {
         std::lock_guard guard(mTc.mMutex);
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
-                                          TEST_MAP_SIZE, 0));
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeCookieTagMap);
 
-        mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue),
-                                            TEST_MAP_SIZE, 0));
+        mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeAppUidStatsMap);
 
-        mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
-                                       TEST_MAP_SIZE, 0));
+        mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue),
-                                         TEST_MAP_SIZE, 0));
+        mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidOwnerMap);
-        mFakeUidPermissionMap.reset(
-                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidPermissionMap);
 
-        mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+        mFakeUidCounterSetMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+        ASSERT_VALID(mFakeUidCounterSetMap);
+
+        mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+        ASSERT_VALID(mFakeIfaceIndexNameMap);
+
+        mTc.mCookieTagMap = mFakeCookieTagMap;
         ASSERT_VALID(mTc.mCookieTagMap);
-        mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap()));
+        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
         ASSERT_VALID(mTc.mAppUidStatsMap);
-        mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+        mTc.mStatsMapA = mFakeStatsMapA;
         ASSERT_VALID(mTc.mStatsMapA);
-        mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+        mTc.mConfigurationMap = mFakeConfigurationMap;
         ASSERT_VALID(mTc.mConfigurationMap);
 
         // Always write to stats map A by default.
-        ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
-                                                          SELECT_MAP_A, BPF_ANY));
-        mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap()));
+        static_assert(SELECT_MAP_A == 0);
+
+        mTc.mUidOwnerMap = mFakeUidOwnerMap;
         ASSERT_VALID(mTc.mUidOwnerMap);
-        mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+        mTc.mUidPermissionMap = mFakeUidPermissionMap;
         ASSERT_VALID(mTc.mUidPermissionMap);
         mTc.mPrivilegedUser.clear();
-    }
 
-    int dupFd(const android::base::unique_fd& mapFd) {
-        return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
+        ASSERT_VALID(mTc.mUidCounterSetMap);
+
+        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
+        ASSERT_VALID(mTc.mIfaceIndexNameMap);
     }
 
     void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
         UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
         EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
-        *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
-        StatsValue statsMapValue = {.rxPackets = 1, .rxBytes = 100};
-        EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
-        key->tag = 0;
+        *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = TEST_IFINDEX};
+        StatsValue statsMapValue = {.rxPackets = RXPACKETS, .rxBytes = RXBYTES,
+                                    .txPackets = TXPACKETS, .txBytes = TXBYTES};
         EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
         EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
         // put tag information back to statsKey
         key->tag = tag;
     }
 
+    void populateFakeCounterSet(uint32_t uid, uint32_t counterSet) {
+        EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
+    }
+
+    void populateFakeIfaceIndexName(const char* name, uint32_t ifaceIndex) {
+        if (name == nullptr || ifaceIndex <= 0) return;
+
+        IfaceValue iface;
+        strlcpy(iface.name, name, sizeof(IfaceValue));
+        EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+    }
+
     void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
         uint32_t uid = TEST_UID;
         EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
@@ -189,7 +218,7 @@
         checkEachUidValue(uids, match);
     }
 
-    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
                                  uint32_t expectedIif) {
         for (uint32_t uid : appUids) {
             Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
@@ -233,37 +262,6 @@
         EXPECT_TRUE(mTc.mPrivilegedUser.empty());
     }
 
-    void addPrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
-    }
-
-    void removePrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
-    }
-
-    void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
-                                  StatsKey tagStatsMapKey) {
-        Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
-        EXPECT_RESULT_OK(cookieMapResult);
-        EXPECT_EQ(uid, cookieMapResult.value().uid);
-        EXPECT_EQ(tag, cookieMapResult.value().tag);
-        Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
-        tagStatsMapKey.tag = 0;
-        statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
-        auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
-        EXPECT_RESULT_OK(appStatsResult);
-        EXPECT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
-    }
-
     Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
                               UidOwnerMatchType matchType, TrafficController::IptOp op) {
         Status ret(0);
@@ -274,6 +272,108 @@
         return ret;
     }
 
+    Status dump(bool verbose, std::vector<std::string>& outputLines) {
+      if (!outputLines.empty()) return statusFromErrno(EUCLEAN, "Output buffer is not empty");
+
+      android::base::unique_fd localFd, remoteFd;
+      if (!Pipe(&localFd, &remoteFd)) return statusFromErrno(errno, "Failed on pipe");
+
+      // dump() blocks until another thread has consumed all its output.
+      std::thread dumpThread =
+          std::thread([this, remoteFd{std::move(remoteFd)}, verbose]() {
+            mTc.dump(remoteFd, verbose);
+          });
+
+      std::string dumpContent;
+      if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
+        return statusFromErrno(errno, "Failed to read dump results from fd");
+      }
+      dumpThread.join();
+
+      std::stringstream dumpStream(std::move(dumpContent));
+      std::string line;
+      while (std::getline(dumpStream, line)) {
+        outputLines.push_back(line);
+      }
+
+      return netdutils::status::ok;
+    }
+
+    // Strings in the |expect| must exist in dump results in order. But no need to be consecutive.
+    bool expectDumpsysContains(std::vector<std::string>& expect) {
+        if (expect.empty()) return false;
+
+        std::vector<std::string> output;
+        Status result = dump(true, output);
+        if (!isOk(result)) {
+            GTEST_LOG_(ERROR) << "TrafficController dump failed: " << netdutils::toString(result);
+            return false;
+        }
+
+        int matched = 0;
+        auto it = expect.begin();
+        for (const auto& line : output) {
+            if (it == expect.end()) break;
+            if (std::string::npos != line.find(*it)) {
+                matched++;
+                ++it;
+            }
+        }
+
+        if (matched != expect.size()) {
+            // dump results for debugging
+            for (const auto& o : output) LOG(INFO) << "output: " << o;
+            for (const auto& e : expect) LOG(INFO) << "expect: " << e;
+            return false;
+        }
+        return true;
+    }
+
+    // Once called, the maps of TrafficController can't recover to valid maps which initialized
+    // in SetUp().
+    void makeTrafficControllerMapsInvalid() {
+        constexpr char INVALID_PATH[] = "invalid";
+
+        mFakeCookieTagMap.init(INVALID_PATH);
+        mTc.mCookieTagMap = mFakeCookieTagMap;
+        ASSERT_INVALID(mTc.mCookieTagMap);
+
+        mFakeAppUidStatsMap.init(INVALID_PATH);
+        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
+        ASSERT_INVALID(mTc.mAppUidStatsMap);
+
+        mFakeStatsMapA.init(INVALID_PATH);
+        mTc.mStatsMapA = mFakeStatsMapA;
+        ASSERT_INVALID(mTc.mStatsMapA);
+
+        mFakeStatsMapB.init(INVALID_PATH);
+        mTc.mStatsMapB = mFakeStatsMapB;
+        ASSERT_INVALID(mTc.mStatsMapB);
+
+        mFakeIfaceStatsMap.init(INVALID_PATH);
+        mTc.mIfaceStatsMap = mFakeIfaceStatsMap;
+        ASSERT_INVALID(mTc.mIfaceStatsMap);
+
+        mFakeConfigurationMap.init(INVALID_PATH);
+        mTc.mConfigurationMap = mFakeConfigurationMap;
+        ASSERT_INVALID(mTc.mConfigurationMap);
+
+        mFakeUidOwnerMap.init(INVALID_PATH);
+        mTc.mUidOwnerMap = mFakeUidOwnerMap;
+        ASSERT_INVALID(mTc.mUidOwnerMap);
+
+        mFakeUidPermissionMap.init(INVALID_PATH);
+        mTc.mUidPermissionMap = mFakeUidPermissionMap;
+        ASSERT_INVALID(mTc.mUidPermissionMap);
+
+        mFakeUidCounterSetMap.init(INVALID_PATH);
+        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
+        ASSERT_INVALID(mTc.mUidCounterSetMap);
+
+        mFakeIfaceIndexNameMap.init(INVALID_PATH);
+        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
+        ASSERT_INVALID(mTc.mIfaceIndexNameMap);
+    }
 };
 
 TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
@@ -307,6 +407,9 @@
     checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
     checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
     checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+    checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
+    checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
+    checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
 }
@@ -318,6 +421,9 @@
     checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
     checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
     checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH);
+    checkUidMapReplace("fw_oem_deny_1", uids, OEM_DENY_1_MATCH);
+    checkUidMapReplace("fw_oem_deny_2", uids, OEM_DENY_2_MATCH);
+    checkUidMapReplace("fw_oem_deny_3", uids, OEM_DENY_3_MATCH);
     ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
 }
 
@@ -432,6 +538,21 @@
     expectMapEmpty(mFakeUidOwnerMap);
 }
 
+TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
+    // Add Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
+    expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove one of Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
+    expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove remaining Lockdown rule
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
 TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
     // Set up existing PENALTY_BOX_MATCH rules
     ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
@@ -491,6 +612,70 @@
     checkEachUidValue({10001, 10002}, IIF_MATCH);
 }
 
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to two uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+    // Remove interface rule from one of the uids
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+    checkEachUidValue({1001}, IIF_MATCH);
+
+    // Remove interface rule from the remaining uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+    // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to the existing uid
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove interface rule with wildcard from the existing uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Set up existing interface rule with wildcard
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+    // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
 TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
     std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
 
@@ -585,6 +770,148 @@
     expectPrivilegedUserSetEmpty();
 }
 
+TEST_F(TrafficControllerTest, TestDumpsys) {
+    StatsKey tagStatsMapKey;
+    populateFakeStats(TEST_COOKIE, TEST_UID, TEST_TAG, &tagStatsMapKey);
+    populateFakeCounterSet(TEST_UID3, TEST_COUNTERSET);
+
+    // Expect: (part of this depends on hard-code values in populateFakeStats())
+    //
+    // mCookieTagMap:
+    // cookie=1 tag=0x2a uid=10086
+    //
+    // mUidCounterSetMap:
+    // 98765 1
+    //
+    // mAppUidStatsMap::
+    // uid rxBytes rxPackets txBytes txPackets
+    // 10086 100 1 0 0
+    //
+    // mStatsMapA:
+    // ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets
+    // 999 test0 0x2a 10086 1 100 1 0 0
+    std::vector<std::string> expectedLines = {
+        "mCookieTagMap:",
+        fmt::format("cookie={} tag={:#x} uid={}", TEST_COOKIE, TEST_TAG, TEST_UID),
+        "mUidCounterSetMap:",
+        fmt::format("{} {}", TEST_UID3, TEST_COUNTERSET),
+        "mAppUidStatsMap::",  // TODO@: fix double colon
+        "uid rxBytes rxPackets txBytes txPackets",
+        fmt::format("{} {} {} {} {}", TEST_UID, RXBYTES, RXPACKETS, TXBYTES, TXPACKETS),
+        "mStatsMapA",
+        "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
+        fmt::format("{} {} {:#x} {} {} {} {} {} {}",
+                    TEST_IFINDEX, TEST_IFNAME, TEST_TAG, TEST_UID, TEST_COUNTERSET, RXBYTES,
+                    RXPACKETS, TXBYTES, TXPACKETS)};
+
+    populateFakeIfaceIndexName(TEST_IFNAME, TEST_IFINDEX);
+    expectedLines.emplace_back("mIfaceIndexNameMap:");
+    expectedLines.emplace_back(fmt::format("ifaceIndex={} ifaceName={}",
+                                           TEST_IFINDEX, TEST_IFNAME));
+
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, HAPPY_BOX_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectedLines.emplace_back("mUidOwnerMap:");
+    expectedLines.emplace_back(fmt::format("{}  HAPPY_BOX_MATCH", TEST_UID));
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, {TEST_UID2});
+    expectedLines.emplace_back("mUidPermissionMap:");
+    expectedLines.emplace_back(fmt::format("{}  BPF_PERMISSION_UPDATE_DEVICE_STATS", TEST_UID2));
+    expectedLines.emplace_back("mPrivilegedUser:");
+    expectedLines.emplace_back(fmt::format("{} ALLOW_UPDATE_DEVICE_STATS", TEST_UID2));
+    EXPECT_TRUE(expectDumpsysContains(expectedLines));
+}
+
+TEST_F(TrafficControllerTest, dumpsysInvalidMaps) {
+    makeTrafficControllerMapsInvalid();
+
+    const std::string kErrIterate = "print end with error: Get firstKey map -1 failed: "
+            "Bad file descriptor";
+    const std::string kErrReadRulesConfig = "read ownerMatch configure failed with error: "
+            "Read value of map -1 failed: Bad file descriptor";
+    const std::string kErrReadStatsMapConfig = "read stats map configure failed with error: "
+            "Read value of map -1 failed: Bad file descriptor";
+
+    std::vector<std::string> expectedLines = {
+        fmt::format("mCookieTagMap {}", kErrIterate),
+        fmt::format("mUidCounterSetMap {}", kErrIterate),
+        fmt::format("mAppUidStatsMap {}", kErrIterate),
+        fmt::format("mStatsMapA {}", kErrIterate),
+        fmt::format("mStatsMapB {}", kErrIterate),
+        fmt::format("mIfaceIndexNameMap {}", kErrIterate),
+        fmt::format("mIfaceStatsMap {}", kErrIterate),
+        fmt::format("mConfigurationMap {}", kErrReadRulesConfig),
+        fmt::format("mConfigurationMap {}", kErrReadStatsMapConfig),
+        fmt::format("mUidOwnerMap {}", kErrIterate),
+        fmt::format("mUidPermissionMap {}", kErrIterate)};
+    EXPECT_TRUE(expectDumpsysContains(expectedLines));
+}
+
+TEST_F(TrafficControllerTest, uidMatchTypeToString) {
+    // NO_MATCH(0) can't be verified because match type flag is added by OR operator.
+    // See TrafficController::addRule()
+    static const struct TestConfig {
+        UidOwnerMatchType uidOwnerMatchType;
+        std::string expected;
+    } testConfigs[] = {
+            // clang-format off
+            {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+            {DOZABLE_MATCH, "DOZABLE_MATCH"},
+            {STANDBY_MATCH, "STANDBY_MATCH"},
+            {POWERSAVE_MATCH, "POWERSAVE_MATCH"},
+            {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+            {RESTRICTED_MATCH, "RESTRICTED_MATCH"},
+            {LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"},
+            {IIF_MATCH, "IIF_MATCH"},
+            {LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"},
+            {OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"},
+            {OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"},
+            {OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"},
+            // clang-format on
+    };
+
+    for (const auto& config : testConfigs) {
+        SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.uidOwnerMatchType,
+                     config.expected));
+
+        // Test private function uidMatchTypeToString() via dumpsys.
+        ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+                                            TrafficController::IptOpInsert)));
+        std::vector<std::string> expectedLines;
+        expectedLines.emplace_back(fmt::format("{}  {}", TEST_UID, config.expected));
+        EXPECT_TRUE(expectDumpsysContains(expectedLines));
+
+        // Clean up the stubs.
+        ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+                                            TrafficController::IptOpDelete)));
+    }
+}
+
+TEST_F(TrafficControllerTest, getFirewallType) {
+    static const struct TestConfig {
+        ChildChain childChain;
+        FirewallType firewallType;
+    } testConfigs[] = {
+            // clang-format off
+            {NONE, DENYLIST},
+            {DOZABLE, ALLOWLIST},
+            {STANDBY, DENYLIST},
+            {POWERSAVE, ALLOWLIST},
+            {RESTRICTED, ALLOWLIST},
+            {LOW_POWER_STANDBY, ALLOWLIST},
+            {OEM_DENY_1, DENYLIST},
+            {OEM_DENY_2, DENYLIST},
+            {OEM_DENY_3, DENYLIST},
+            {INVALID_CHAIN, DENYLIST},
+            // clang-format on
+    };
+
+    for (const auto& config : testConfigs) {
+        SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.childChain, config.firewallType));
+        EXPECT_EQ(config.firewallType, mTc.getFirewallType(config.childChain));
+    }
+}
+
 constexpr uint32_t SOCK_CLOSE_WAIT_US = 30 * 1000;
 constexpr uint32_t ENOBUFS_POLL_WAIT_US = 10 * 1000;
 
@@ -608,7 +935,7 @@
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
 
     void SetUp() {
-        mCookieTagMap.reset(android::bpf::mapRetrieveRW(COOKIE_TAG_MAP_PATH));
+        mCookieTagMap.init(COOKIE_TAG_MAP_PATH);
         ASSERT_TRUE(mCookieTagMap.isValid());
     }
 
@@ -620,7 +947,7 @@
                 if (res.ok() || (res.error().code() == ENOENT)) {
                     return Result<void>();
                 }
-                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
                       strerror(res.error().code()));
             }
             // Move forward to next cookie in the map.
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..03f449a 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -17,9 +17,12 @@
 #pragma once
 // TODO: deduplicate with the constants in NetdConstants.h.
 #include <aidl/android/net/INetd.h>
+#include "clat_mark.h"
 
 using aidl::android::net::INetd;
 
+static_assert(INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
+
 enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
 
 // ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
@@ -35,6 +38,9 @@
     POWERSAVE = 3,
     RESTRICTED = 4,
     LOW_POWER_STANDBY = 5,
+    OEM_DENY_1 = 7,
+    OEM_DENY_2 = 8,
+    OEM_DENY_3 = 9,
     INVALID_CHAIN
 };
 // LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 79e75ac..14c5eaf 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -45,11 +45,6 @@
      */
     netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
 
-    /*
-     * Add the interface name and index pair into the eBPF map.
-     */
-    int addInterface(const char* name, uint32_t ifaceIndex);
-
     int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
 
     int removeUidOwnerRule(const uid_t uid);
@@ -71,11 +66,11 @@
             EXCLUDES(mMutex);
     netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
 
+    netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
+
     netdutils::Status updateUidOwnerMap(const uint32_t uid,
                                         UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
 
-    int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
-
     static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
     makeSkDestroyListener();
 
@@ -88,6 +83,9 @@
     static const char* LOCAL_POWERSAVE;
     static const char* LOCAL_RESTRICTED;
     static const char* LOCAL_LOW_POWER_STANDBY;
+    static const char* LOCAL_OEM_DENY_1;
+    static const char* LOCAL_OEM_DENY_2;
+    static const char* LOCAL_OEM_DENY_3;
 
   private:
     /*
@@ -149,13 +147,13 @@
      * the map right now:
      * - Entry with UID_RULES_CONFIGURATION_KEY:
      *    Store the configuration for the current uid rules. It indicates the device
-     *    is in doze/powersave/standby/restricted/low power standby mode.
+     *    is in doze/powersave/standby/restricted/low power standby/oem deny mode.
      * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
      *    Stores the current live stats map that kernel program is writing to.
      *    Userspace can do scraping and cleaning job on the other one depending on the
      *    current configs.
      */
-    bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+    bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
 
     /*
      * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
@@ -182,8 +180,6 @@
     // need to call back to system server for permission check.
     std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
 
-    bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
-
     // For testing
     friend class TrafficControllerTest;
 };
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 68e4dc4..54d40ac 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -35,7 +35,8 @@
 cc_test {
     name: "libclat_test",
     defaults: ["netd_defaults"],
-    test_suites: ["device-tests"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
     srcs: [
         "clatutils_test.cpp",
     ],
@@ -49,5 +50,14 @@
         "liblog",
         "libnetutils",
     ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
     require_root: true,
 }
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
index 4153e19..8cca1f4 100644
--- a/service/native/libs/libclat/clatutils_test.cpp
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -19,6 +19,7 @@
 #include <gtest/gtest.h>
 #include <linux/if_packet.h>
 #include <linux/if_tun.h>
+#include <netinet/in.h>
 #include "tun_interface.h"
 
 extern "C" {
@@ -182,6 +183,31 @@
     v6Iface.destroy();
 }
 
+// This is not a realistic test because we can't test generateIPv6Address here since it requires
+// manipulating routing, which we can't do without talking to the real netd on the system.
+// See test MakeChecksumNeutral.
+// TODO: remove this test once EthernetTetheringTest can test on mainline test coverage branch.
+TEST_F(ClatUtils, GenerateIpv6AddressFailWithUlaSocketAddress) {
+    // Create an interface for generateIpv6Address to connect to.
+    TunInterface tun;
+    ASSERT_EQ(0, tun.init());
+
+    // Unused because v6 address is ULA and makeChecksumNeutral has never called.
+    in_addr v4 = {inet_addr(kIPv4LocalAddr)};
+    // Not a NAT64 prefix because test can't connect to in generateIpv6Address.
+    // Used to be a fake address from the tun interface for generating an IPv6 socket address.
+    // nat64Prefix won't be used in MakeChecksumNeutral because MakeChecksumNeutral has never
+    // be called.
+    in6_addr nat64Prefix = tun.dstAddr();  // not realistic
+    in6_addr v6;
+    EXPECT_EQ(1, inet_pton(AF_INET6, "::", &v6));  // initialize as zero
+
+    EXPECT_EQ(-ENETUNREACH, generateIpv6Address(tun.name().c_str(), v4, nat64Prefix, &v6));
+    EXPECT_TRUE(IN6_IS_ADDR_ULA(&v6));
+
+    tun.destroy();
+}
+
 }  // namespace clat
 }  // namespace net
 }  // namespace android
diff --git a/service/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..cffa490
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,17 @@
+# Make sure proguard keeps all connectivity classes
+# TODO: instead of keeping everything, consider listing only "entry points"
+# (service loader, JNI registered methods, etc) and letting the optimizer do its job
+-keep class android.net.** { *; }
+-keep class com.android.connectivity.** { *; }
+-keep class com.android.net.** { *; }
+-keep class !com.android.server.nearby.**,com.android.server.** { *; }
+
+# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
+-keep class com.android.server.nearby.NearbyService { *; }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+# This replicates the base proguard rule used by the build by default
+# (proguard_basic_keeps.flags), but needs to be specified here because the
+# com.google.protobuf package is jarjared to the below package.
+-keepclassmembers class * extends android.net.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index c006bc6..6599c7f 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,15 +16,31 @@
 
 package com.android.server;
 
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.system.OsConstants.EINVAL;
+import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.EOPNOTSUPP;
 
 import android.net.INetd;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
+import android.util.SparseLongArray;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -35,22 +51,115 @@
  * {@hide}
  */
 public class BpfNetMaps {
+    private static final boolean PRE_T = !SdkLevel.isAtLeastT();
+    static {
+        if (!PRE_T) {
+            System.loadLibrary("service-connectivity");
+        }
+    }
+
     private static final String TAG = "BpfNetMaps";
     private final INetd mNetd;
     // Use legacy netd for releases before T.
-    private static final boolean USE_NETD = !SdkLevel.isAtLeastT();
     private static boolean sInitialized = false;
 
+    // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
+    // This entry is not accessed by others.
+    // BpfNetMaps acquires this lock while sequence of read, modify, and write.
+    private static final Object sUidRulesConfigBpfMapLock = new Object();
+
+    private static final String CONFIGURATION_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+    private static final String UID_OWNER_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
+    private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+    private static BpfMap<U32, U32> sConfigurationMap = null;
+    // BpfMap for UID_OWNER_MAP_PATH. This map is not accessed by others.
+    private static BpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
+
+    // LINT.IfChange(match_type)
+    @VisibleForTesting public static final long NO_MATCH = 0;
+    @VisibleForTesting public static final long HAPPY_BOX_MATCH = (1 << 0);
+    @VisibleForTesting public static final long PENALTY_BOX_MATCH = (1 << 1);
+    @VisibleForTesting public static final long DOZABLE_MATCH = (1 << 2);
+    @VisibleForTesting public static final long STANDBY_MATCH = (1 << 3);
+    @VisibleForTesting public static final long POWERSAVE_MATCH = (1 << 4);
+    @VisibleForTesting public static final long RESTRICTED_MATCH = (1 << 5);
+    @VisibleForTesting public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+    @VisibleForTesting public static final long IIF_MATCH = (1 << 7);
+    @VisibleForTesting public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+    @VisibleForTesting public static final long OEM_DENY_1_MATCH = (1 << 9);
+    @VisibleForTesting public static final long OEM_DENY_2_MATCH = (1 << 10);
+    @VisibleForTesting public static final long OEM_DENY_3_MATCH = (1 << 11);
+    // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/bpf_shared.h)
+
+    // TODO: Use Java BpfMap instead of JNI code (TrafficController) for map update.
+    // Currently, BpfNetMaps uses TrafficController for map update and TrafficController
+    // (changeUidOwnerRule and toggleUidOwnerMap) also does conversion from "firewall chain" to
+    // "match". Migrating map update from JNI to Java BpfMap will solve this duplication.
+    private static final SparseLongArray FIREWALL_CHAIN_TO_MATCH = new SparseLongArray();
+    static {
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_DOZABLE, DOZABLE_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_STANDBY, STANDBY_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_POWERSAVE, POWERSAVE_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_RESTRICTED, RESTRICTED_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_1, OEM_DENY_1_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_2, OEM_DENY_2_MATCH);
+        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_3, OEM_DENY_3_MATCH);
+    }
+
+    /**
+     * Set configurationMap for test.
+     */
+    @VisibleForTesting
+    public static void setConfigurationMapForTest(BpfMap<U32, U32> configurationMap) {
+        sConfigurationMap = configurationMap;
+    }
+
+    /**
+     * Set uidOwnerMap for test.
+     */
+    @VisibleForTesting
+    public static void setUidOwnerMapForTest(BpfMap<U32, UidOwnerValue> uidOwnerMap) {
+        sUidOwnerMap = uidOwnerMap;
+    }
+
+    private static BpfMap<U32, U32> getConfigurationMap() {
+        try {
+            return new BpfMap<>(
+                    CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open netd configuration map", e);
+        }
+    }
+
+    private static BpfMap<U32, UidOwnerValue> getUidOwnerMap() {
+        try {
+            return new BpfMap<>(
+                    UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, UidOwnerValue.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open uid owner map", e);
+        }
+    }
+
+    private static void setBpfMaps() {
+        if (sConfigurationMap == null) {
+            sConfigurationMap = getConfigurationMap();
+        }
+        if (sUidOwnerMap == null) {
+            sUidOwnerMap = getUidOwnerMap();
+        }
+    }
+
     /**
      * Initializes the class if it is not already initialized. This method will open maps but not
      * cause any other effects. This method may be called multiple times on any thread.
      */
     private static synchronized void ensureInitialized() {
         if (sInitialized) return;
-        if (!USE_NETD) {
-            System.loadLibrary("service-connectivity");
-            native_init();
-        }
+        setBpfMaps();
+        native_init();
         sInitialized = true;
     }
 
@@ -58,20 +167,101 @@
     public BpfNetMaps() {
         this(null);
 
-        if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+        if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
     }
 
-    public BpfNetMaps(INetd netd) {
-        ensureInitialized();
+    public BpfNetMaps(final INetd netd) {
+        if (!PRE_T) {
+            ensureInitialized();
+        }
         mNetd = netd;
     }
 
+    /**
+     * Get corresponding match from firewall chain.
+     */
+    @VisibleForTesting
+    public long getMatchByFirewallChain(final int chain) {
+        final long match = FIREWALL_CHAIN_TO_MATCH.get(chain, NO_MATCH);
+        if (match == NO_MATCH) {
+            throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+        }
+        return match;
+    }
+
     private void maybeThrow(final int err, final String msg) {
         if (err != 0) {
             throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err));
         }
     }
 
+    private void throwIfPreT(final String msg) {
+        if (PRE_T) {
+            throw new UnsupportedOperationException(msg);
+        }
+    }
+
+    private void removeRule(final int uid, final long match, final String caller) {
+        try {
+            synchronized (sUidOwnerMap) {
+                final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+                if (oldMatch == null) {
+                    throw new ServiceSpecificException(ENOENT,
+                            "sUidOwnerMap does not have entry for uid: " + uid);
+                }
+
+                final UidOwnerValue newMatch = new UidOwnerValue(
+                        (match == IIF_MATCH) ? 0 : oldMatch.iif,
+                        oldMatch.rule & ~match
+                );
+
+                if (newMatch.rule == 0) {
+                    sUidOwnerMap.deleteEntry(new U32(uid));
+                } else {
+                    sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+                }
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    caller + " failed to remove rule: " + Os.strerror(e.errno));
+        }
+    }
+
+    private void addRule(final int uid, final long match, final long iif, final String caller) {
+        if (match != IIF_MATCH && iif != 0) {
+            throw new ServiceSpecificException(EINVAL,
+                    "Non-interface match must have zero interface index");
+        }
+
+        try {
+            synchronized (sUidOwnerMap) {
+                final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+                final UidOwnerValue newMatch;
+                if (oldMatch != null) {
+                    newMatch = new UidOwnerValue(
+                            (match == IIF_MATCH) ? iif : oldMatch.iif,
+                            oldMatch.rule | match
+                    );
+                } else {
+                    newMatch = new UidOwnerValue(
+                            iif,
+                            match
+                    );
+                }
+                sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    caller + " failed to add rule: " + Os.strerror(e.errno));
+        }
+    }
+
+    private void addRule(final int uid, final long match, final String caller) {
+        addRule(uid, match, 0 /* iif */, caller);
+    }
+
     /**
      * Add naughty app bandwidth rule for specific app
      *
@@ -80,8 +270,8 @@
      *                                  cause of the failure.
      */
     public void addNaughtyApp(final int uid) {
-        final int err = native_addNaughtyApp(uid);
-        maybeThrow(err, "Unable to add naughty app");
+        throwIfPreT("addNaughtyApp is not available on pre-T devices");
+        addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
     }
 
     /**
@@ -92,8 +282,8 @@
      *                                  cause of the failure.
      */
     public void removeNaughtyApp(final int uid) {
-        final int err = native_removeNaughtyApp(uid);
-        maybeThrow(err, "Unable to remove naughty app");
+        throwIfPreT("removeNaughtyApp is not available on pre-T devices");
+        removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
     }
 
     /**
@@ -104,8 +294,8 @@
      *                                  cause of the failure.
      */
     public void addNiceApp(final int uid) {
-        final int err = native_addNiceApp(uid);
-        maybeThrow(err, "Unable to add nice app");
+        throwIfPreT("addNiceApp is not available on pre-T devices");
+        addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
     }
 
     /**
@@ -116,8 +306,8 @@
      *                                  cause of the failure.
      */
     public void removeNiceApp(final int uid) {
-        final int err = native_removeNiceApp(uid);
-        maybeThrow(err, "Unable to remove nice app");
+        throwIfPreT("removeNiceApp is not available on pre-T devices");
+        removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
     }
 
     /**
@@ -125,12 +315,46 @@
      *
      * @param childChain target chain to enable
      * @param enable     whether to enable or disable child chain.
+     * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
     public void setChildChain(final int childChain, final boolean enable) {
-        final int err = native_setChildChain(childChain, enable);
-        maybeThrow(err, "Unable to set child chain");
+        throwIfPreT("setChildChain is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(childChain);
+        try {
+            synchronized (sUidRulesConfigBpfMapLock) {
+                final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+                final long newConfig = enable ? (config.val | match) : (config.val & ~match);
+                sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to set child chain: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
+     * Get the specified firewall chain's status.
+     *
+     * @param childChain target chain
+     * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public boolean isChainEnabled(final int childChain) {
+        throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(childChain);
+        try {
+            final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+            return (config.val & match) != 0;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to get firewall chain status: " + Os.strerror(e.errno));
+        }
     }
 
     /**
@@ -148,11 +372,13 @@
      */
     public int replaceUidChain(final String chainName, final boolean isAllowlist,
             final int[] uids) {
-        final int err = native_replaceUidChain(chainName, isAllowlist, uids);
-        if (err != 0) {
-            Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+        synchronized (sUidOwnerMap) {
+            final int err = native_replaceUidChain(chainName, isAllowlist, uids);
+            if (err != 0) {
+                Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+            }
+            return -err;
         }
-        return -err;
     }
 
     /**
@@ -165,8 +391,10 @@
      *                                  cause of the failure.
      */
     public void setUidRule(final int childChain, final int uid, final int firewallRule) {
-        final int err = native_setUidRule(childChain, uid, firewallRule);
-        maybeThrow(err, "Unable to set uid rule");
+        synchronized (sUidOwnerMap) {
+            final int err = native_setUidRule(childChain, uid, firewallRule);
+            maybeThrow(err, "Unable to set uid rule");
+        }
     }
 
     /**
@@ -187,12 +415,14 @@
      *                                  cause of the failure.
      */
     public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.firewallAddUidInterfaceRules(ifName, uids);
             return;
         }
-        final int err = native_addUidInterfaceRules(ifName, uids);
-        maybeThrow(err, "Unable to add uid interface rules");
+        synchronized (sUidOwnerMap) {
+            final int err = native_addUidInterfaceRules(ifName, uids);
+            maybeThrow(err, "Unable to add uid interface rules");
+        }
     }
 
     /**
@@ -207,12 +437,31 @@
      *                                  cause of the failure.
      */
     public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.firewallRemoveUidInterfaceRules(uids);
             return;
         }
-        final int err = native_removeUidInterfaceRules(uids);
-        maybeThrow(err, "Unable to remove uid interface rules");
+        synchronized (sUidOwnerMap) {
+            final int err = native_removeUidInterfaceRules(uids);
+            maybeThrow(err, "Unable to remove uid interface rules");
+        }
+    }
+
+    /**
+     * Update lockdown rule for uid
+     *
+     * @param  uid          target uid to add/remove the rule
+     * @param  add          {@code true} to add the rule, {@code false} to remove the rule.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public void updateUidLockdownRule(final int uid, final boolean add) {
+        throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
+        if (add) {
+            addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+        } else {
+            removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+        }
     }
 
     /**
@@ -237,7 +486,7 @@
      * @throws RemoteException when netd has crashed.
      */
     public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.trafficSetNetPermForUids(permissions, uids);
             return;
         }
@@ -253,7 +502,7 @@
      */
     public void dump(final FileDescriptor fd, boolean verbose)
             throws IOException, ServiceSpecificException {
-        if (USE_NETD) {
+        if (PRE_T) {
             throw new ServiceSpecificException(
                     EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
                     + " devices, use dumpsys netd trafficcontroller instead.");
@@ -262,15 +511,24 @@
     }
 
     private static native void native_init();
+    @GuardedBy("sUidOwnerMap")
     private native int native_addNaughtyApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeNaughtyApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_addNiceApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeNiceApp(int uid);
-    private native int native_setChildChain(int childChain, boolean enable);
+    @GuardedBy("sUidOwnerMap")
     private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
+    @GuardedBy("sUidOwnerMap")
     private native int native_setUidRule(int childChain, int uid, int firewallRule);
+    @GuardedBy("sUidOwnerMap")
     private native int native_addUidInterfaceRules(String ifName, int[] uids);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeUidInterfaceRules(int[] uids);
+    @GuardedBy("sUidOwnerMap")
+    private native int native_updateUidLockdownRule(int uid, boolean add);
     private native int native_swapActiveStatsMap();
     private native void native_setPermissionForUids(int permissions, int[] uids);
     private native void native_dump(FileDescriptor fd, boolean verbose);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index d79bdb8..37fc391 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -108,6 +108,7 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
 import android.app.usage.NetworkStatsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -258,6 +259,7 @@
 import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.server.connectivity.AutodestructReference;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
@@ -608,13 +610,6 @@
     // Handle private DNS validation status updates.
     private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
 
-    /**
-     * used to remove a network request, either a listener or a real request and call unavailable
-     * arg1 = UID of caller
-     * obj  = NetworkRequest
-     */
-    private static final int EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE = 39;
-
      /**
       * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
       * been tested.
@@ -753,7 +748,7 @@
      * The BPF program attached to the tc-police hook to account for to-be-dropped traffic.
      */
     private static final String TC_POLICE_BPF_PROG_PATH =
-            "/sys/fs/bpf/prog_netd_schedact_ingress_account";
+            "/sys/fs/bpf/netd_shared/prog_netd_schedact_ingress_account";
 
     private static String eventName(int what) {
         return sMagicDecoderRing.get(what, Integer.toString(what));
@@ -1192,6 +1187,7 @@
     /**
      * Keeps track of the number of requests made under different uids.
      */
+    // TODO: Remove the hack and use com.android.net.module.util.PerUidCounter instead.
     public static class PerUidCounter {
         private final int mMaxCountPerUid;
 
@@ -1405,6 +1401,19 @@
         }
 
         /**
+         * @see ClatCoordinator
+         */
+        public ClatCoordinator getClatCoordinator(INetd netd) {
+            return new ClatCoordinator(
+                new ClatCoordinator.Dependencies() {
+                    @NonNull
+                    public INetd getNetd() {
+                        return netd;
+                    }
+                });
+        }
+
+        /**
          * Wraps {@link TcUtils#tcFilterAddDevIngressPolice}
          */
         public void enableIngressRateLimit(String iface, long rateInBytesPerSecond) {
@@ -2613,7 +2622,7 @@
         verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
         enforceChangePermission(callingPackageName, callingAttributionTag);
         if (mProtectedNetworks.contains(networkType)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         }
 
         InetAddress addr;
@@ -2865,7 +2874,7 @@
 
     private void enforceNetworkFactoryPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
-        if (getCallingUid() == Process.BLUETOOTH_UID) return;
+        if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
@@ -2873,7 +2882,7 @@
 
     private void enforceNetworkFactoryOrSettingsPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
-        if (getCallingUid() == Process.BLUETOOTH_UID) return;
+        if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2882,7 +2891,7 @@
 
     private void enforceNetworkFactoryOrTestNetworksPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
-        if (getCallingUid() == Process.BLUETOOTH_UID) return;
+        if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2896,7 +2905,7 @@
                 android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
                 || PERMISSION_GRANTED == mContext.checkPermission(
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid)
-                || uid == Process.BLUETOOTH_UID;
+                || UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
     }
 
     private boolean checkSettingsPermission() {
@@ -2967,18 +2976,35 @@
                 android.Manifest.permission.NETWORK_SETTINGS);
     }
 
-    private void enforceConnectivityRestrictedNetworksPermission() {
-        try {
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
-                    "ConnectivityService");
-            return;
-        } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
-        //  TODO: Remove this fallback check after all apps have declared
-        //   CONNECTIVITY_USE_RESTRICTED_NETWORKS.
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CONNECTIVITY_INTERNAL,
-                "ConnectivityService");
+    private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+            boolean checkUidsAllowedList) {
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
+            return true;
+        }
+
+        // fallback to ConnectivityInternalPermission
+        // TODO: Remove this fallback check after all apps have declared
+        //  CONNECTIVITY_USE_RESTRICTED_NETWORKS.
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+            return true;
+        }
+
+        // Check whether uid is in allowed on restricted networks list.
+        if (checkUidsAllowedList
+                && mPermissionMonitor.isUidAllowedOnRestrictedNetworks(callingUid)) {
+            return true;
+        }
+        return false;
+    }
+
+    private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
+        final int callingUid = mDeps.getCallingUid();
+        if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+            throw new SecurityException("ConnectivityService: user " + callingUid
+                    + " has no permission to access restricted network.");
+        }
     }
 
     private void enforceKeepalivePermission() {
@@ -3266,11 +3292,12 @@
             return;
         }
 
-        pw.print("NetworkProviders for:");
+        pw.println("NetworkProviders for:");
+        pw.increaseIndent();
         for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
-            pw.print(" " + npi.name);
+            pw.println(npi.providerId + ": " + npi.name);
         }
-        pw.println();
+        pw.decreaseIndent();
         pw.println();
 
         final NetworkAgentInfo defaultNai = getDefaultNetwork();
@@ -3319,6 +3346,14 @@
         pw.decreaseIndent();
         pw.println();
 
+        pw.println("Network Offers:");
+        pw.increaseIndent();
+        for (final NetworkOfferInfo offerInfo : mNetworkOffers) {
+            pw.println(offerInfo.offer);
+        }
+        pw.decreaseIndent();
+        pw.println();
+
         mLegacyTypeTracker.dump(pw);
 
         pw.println();
@@ -3387,12 +3422,27 @@
         pw.increaseIndent();
         mNetworkActivityTracker.dump(pw);
         pw.decreaseIndent();
+
+        // pre-T is logged by netd.
+        if (SdkLevel.isAtLeastT()) {
+            pw.println();
+            pw.println("BPF programs & maps:");
+            pw.increaseIndent();
+            // Flush is required. Otherwise, the traces in fd can interleave with traces in pw.
+            pw.flush();
+            dumpTrafficController(pw, fd, /*verbose=*/ true);
+            pw.decreaseIndent();
+        }
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
         for (NetworkAgentInfo nai : networksSortedById()) {
             pw.println(nai.toString());
             pw.increaseIndent();
+            pw.println("Nat464Xlat:");
+            pw.increaseIndent();
+            nai.dumpNat464Xlat(pw);
+            pw.decreaseIndent();
             pw.println(String.format(
                     "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
                     nai.numForegroundNetworkRequests(),
@@ -3669,7 +3719,7 @@
                 }
                 case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
                     if (mDscpPolicyTracker != null) {
-                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai, true);
                     }
                     break;
                 }
@@ -3833,7 +3883,6 @@
             }
 
             final boolean wasValidated = nai.lastValidated;
-            final boolean wasDefault = isDefaultNetwork(nai);
             final boolean wasPartial = nai.partialConnectivity;
             nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
             final boolean partialConnectivityChanged =
@@ -4410,6 +4459,9 @@
     }
 
     private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        if (mDscpPolicyTracker != null) {
+            mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
+        }
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -4464,7 +4516,7 @@
 
     private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
             @NonNull final NetworkCapabilities caps) {
-        if (SdkLevel.isAtLeastT() && mCarrierPrivilegeAuthenticator != null) {
+        if (mCarrierPrivilegeAuthenticator != null) {
             return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
                     callingUid, caps);
         }
@@ -4494,7 +4546,6 @@
 
     private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
         ensureRunningOnConnectivityServiceThread();
-        NetworkRequest requestToBeReleased = null;
         for (final NetworkRequestInfo nri : nris) {
             mNetworkRequestInfoLogs.log("REGISTER " + nri);
             checkNrisConsistency(nri);
@@ -4509,13 +4560,6 @@
                         }
                     }
                 }
-                if (req.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                    if (!hasCarrierPrivilegeForNetworkCaps(nri.mUid, req.networkCapabilities)
-                            && !checkConnectivityRestrictedNetworksPermission(
-                                    nri.mPid, nri.mUid)) {
-                        requestToBeReleased = req;
-                    }
-                }
             }
 
             // If this NRI has a satisfier already, it is replacing an older request that
@@ -4527,11 +4571,6 @@
             }
         }
 
-        if (requestToBeReleased != null) {
-            releaseNetworkRequestAndCallOnUnavailable(requestToBeReleased);
-            return;
-        }
-
         if (mFlags.noRematchAllRequestsOnRegister()) {
             rematchNetworksAndRequests(nris);
         } else {
@@ -5371,11 +5410,6 @@
                             /* callOnUnavailable */ false);
                     break;
                 }
-                case EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE: {
-                    handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
-                            /* callOnUnavailable */ true);
-                    break;
-                }
                 case EVENT_SET_ACCEPT_UNVALIDATED: {
                     Network network = (Network) msg.obj;
                     handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
@@ -5945,6 +5979,10 @@
                     + Arrays.toString(ranges) + "): netd command failed: " + e);
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
+        }
+
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
             final boolean curMetered = nai.networkCapabilities.isMetered();
             maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -6325,7 +6363,7 @@
             if (null != satisfier) {
                 // If the old NRI was satisfied by an NAI, then it may have had an active request.
                 // The active request is necessary to figure out what callbacks to send, in
-                // particular then a network updates its capabilities.
+                // particular when a network updates its capabilities.
                 // As this code creates a new NRI with a new set of requests, figure out which of
                 // the list of requests should be the active request. It is always the first
                 // request of the list that can be satisfied by the satisfier since the order of
@@ -6594,7 +6632,7 @@
             case REQUEST:
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
                 enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                        callingAttributionTag);
+                        callingAttributionTag, callingUid);
                 // TODO: this is incorrect. We mark the request as metered or not depending on
                 //  the state of the app when the request is filed, but we never change the
                 //  request if the app changes network state. http://b/29964605
@@ -6684,26 +6722,19 @@
     }
 
     private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
-            String callingPackageName, String callingAttributionTag) {
+            String callingPackageName, String callingAttributionTag, final int callingUid) {
         if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
-            if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                enforceConnectivityRestrictedNetworksPermission();
+            // For T+ devices, callers with carrier privilege could request with CBS capabilities.
+            if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                    && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
+                return;
             }
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         } else {
             enforceChangePermission(callingPackageName, callingAttributionTag);
         }
     }
 
-    private boolean checkConnectivityRestrictedNetworksPermission(int callerPid, int callerUid) {
-        if (checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)
-                || checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
-            return true;
-        }
-        return false;
-    }
-
     @Override
     public boolean requestBandwidthUpdate(Network network) {
         enforceAccessPermission();
@@ -6762,7 +6793,7 @@
         final int callingUid = mDeps.getCallingUid();
         networkCapabilities = new NetworkCapabilities(networkCapabilities);
         enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                callingAttributionTag);
+                callingAttributionTag, callingUid);
         enforceMeteredApnPolicy(networkCapabilities);
         ensureRequestableCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
@@ -6885,13 +6916,6 @@
                 EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
     }
 
-    private void releaseNetworkRequestAndCallOnUnavailable(NetworkRequest networkRequest) {
-        ensureNetworkRequestHasType(networkRequest);
-        mHandler.sendMessage(mHandler.obtainMessage(
-                EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE, mDeps.getCallingUid(), 0,
-                networkRequest));
-    }
-
     private void handleRegisterNetworkProvider(NetworkProviderInfo npi) {
         if (mNetworkProviderInfos.containsKey(npi.messenger)) {
             // Avoid creating duplicates. even if an app makes a direct AIDL call.
@@ -7722,10 +7746,10 @@
 
     private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
             NetworkAgentInfo nai) {
-        final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
-        final String newIface = newLp != null ? newLp.getInterfaceName() : null;
-        final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
-        final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+        final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
+        final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
+        final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
+        final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
 
         if (!wasFiltering && !needsFiltering) {
             // Nothing to do.
@@ -7738,6 +7762,10 @@
         }
 
         final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+        if (ranges == null || ranges.isEmpty()) {
+            return;
+        }
+
         final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
         // TODO: this create a window of opportunity for apps to receive traffic between the time
         // when the old rules are removed and the time when new rules are added. To fix this,
@@ -7803,6 +7831,7 @@
         }
         nai.declaredCapabilities = new NetworkCapabilities(nc);
         NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(nc, nai.creatorUid,
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE),
                 mCarrierPrivilegeAuthenticator);
     }
 
@@ -8031,15 +8060,14 @@
     }
 
     /**
-     * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
-     * network.
+     * Returns the interface which requires VPN isolation (ingress interface filtering).
      *
      * Ingress interface filtering enforces that all apps under the given network can only receive
      * packets from the network's interface (and loopback). This is important for VPNs because
      * apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
      * non-VPN interfaces.
      *
-     * As a result, this method should return true iff
+     * As a result, this method should return Non-null interface iff
      *  1. the network is an app VPN (not legacy VPN)
      *  2. the VPN does not allow bypass
      *  3. the VPN is fully-routed
@@ -8048,15 +8076,34 @@
      * @see INetd#firewallAddUidInterfaceRules
      * @see INetd#firewallRemoveUidInterfaceRules
      */
-    private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+    @Nullable
+    private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
             LinkProperties lp) {
-        if (nc == null || lp == null) return false;
-        return nai.isVPN()
+        if (nc == null || lp == null) return null;
+        if (nai.isVPN()
                 && !nai.networkAgentConfig.allowBypass
                 && nc.getOwnerUid() != Process.SYSTEM_UID
                 && lp.getInterfaceName() != null
                 && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
-                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
+                && !lp.hasExcludeRoute()) {
+            return lp.getInterfaceName();
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether we need to set interface filtering rule or not
+     */
+    private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+            String isolationIface) {
+        // Allow rules are always needed if VPN isolation is enabled.
+        if (isolationIface != null) return true;
+
+        // On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
+        // wildcard to allow apps to receive packets on all interfaces. This is required to accept
+        // incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
+        return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8184,9 +8231,10 @@
             if (!prevRanges.isEmpty()) {
                 updateVpnUidRanges(false, nai, prevRanges);
             }
-            final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
-            final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
-            final String iface = nai.linkProperties.getInterfaceName();
+            final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
+            final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
+            final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
+            final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
             // For VPN uid interface filtering, old ranges need to be removed before new ranges can
             // be added, due to the range being expanded and stored as individual UIDs. For example
             // the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8199,10 +8247,11 @@
             // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
             // to be removed will never overlap with the new range to be added.
             if (wasFiltering && !prevRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+                        prevNc.getOwnerUid());
             }
             if (shouldFilter && !newRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
             }
         } catch (Exception e) {
             // Never crash!
@@ -9207,7 +9256,18 @@
             params.networkCapabilities = networkAgent.networkCapabilities;
             params.linkProperties = new LinkProperties(networkAgent.linkProperties,
                     true /* parcelSensitiveFields */);
-            networkAgent.networkMonitor().notifyNetworkConnected(params);
+            // isAtLeastT() is conservative here, as recent versions of NetworkStack support the
+            // newer callback even before T. However getInterfaceVersion is a synchronized binder
+            // call that would cause a Log.wtf to be emitted from the system_server process, and
+            // in the absence of a satisfactory, scalable solution which follows an easy/standard
+            // process to check the interface version, just use an SDK check. NetworkStack will
+            // always be new enough when running on T+.
+            if (SdkLevel.isAtLeastT()) {
+                networkAgent.networkMonitor().notifyNetworkConnected(params);
+            } else {
+                networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
+                        params.networkCapabilities);
+            }
             scheduleUnvalidatedPrompt(networkAgent);
 
             // Whether a particular NetworkRequest listen should cause signal strength thresholds to
@@ -10593,7 +10653,11 @@
         if (callback == null) throw new IllegalArgumentException("callback must be non-null");
 
         if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            // TODO: Check allowed list here and ensure that either a) any QoS callback registered
+            //  on this network is unregistered when the app loses permission or b) no QoS
+            //  callbacks are sent for restricted networks unless the app currently has permission
+            //  to access restricted networks.
+            enforceConnectivityRestrictedNetworksPermission(false /* checkUidsAllowedList */);
         }
         mQosCallbackTracker.registerCallback(callback, filter, nai);
     }
@@ -10609,13 +10673,29 @@
         mQosCallbackTracker.unregisterCallback(callback);
     }
 
+    private boolean isNetworkPreferenceAllowedForProfile(@NonNull UserHandle profile) {
+        // UserManager.isManagedProfile returns true for all apps in managed user profiles.
+        // Enterprise device can be fully managed like device owner and such use case
+        // also should be supported. Calling app check for work profile and fully managed device
+        // is already done in DevicePolicyManager.
+        // This check is an extra caution to be sure device is fully managed or not.
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+        if (um.isManagedProfile(profile.getIdentifier())) {
+            return true;
+        }
+        if (SdkLevel.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
+        return false;
+    }
+
     /**
-     * Request that a user profile is put by default on a network matching a given preference.
+     * Set a list of default network selection policies for a user profile or device owner.
      *
      * See the documentation for the individual preferences for a description of the supported
      * behaviors.
      *
-     * @param profile the user profile for whih the preference is being set.
+     * @param profile If the device owner is set, any profile is allowed.
+              Otherwise, the given profile can only be managed profile.
      * @param preferences the list of profile network preferences for the
      *        provided profile.
      * @param listener an optional listener to listen for completion of the operation.
@@ -10629,7 +10709,10 @@
         Objects.requireNonNull(profile);
 
         if (preferences.size() == 0) {
-            preferences.add((new ProfileNetworkPreference.Builder()).build());
+            final ProfileNetworkPreference pref = new ProfileNetworkPreference.Builder()
+                    .setPreference(ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    .build();
+            preferences.add(pref);
         }
 
         PermissionUtils.enforceNetworkStackPermission(mContext);
@@ -10640,19 +10723,21 @@
             throw new IllegalArgumentException("Must explicitly specify a user handle ("
                     + "UserHandle.CURRENT not supported)");
         }
-        final UserManager um = mContext.getSystemService(UserManager.class);
-        if (!um.isManagedProfile(profile.getIdentifier())) {
-            throw new IllegalArgumentException("Profile must be a managed profile");
+        if (!isNetworkPreferenceAllowedForProfile(profile)) {
+            throw new IllegalArgumentException("Profile must be a managed profile "
+                    + "or the device owner must be set. ");
         }
 
         final List<ProfileNetworkPreferenceList.Preference> preferenceList =
                 new ArrayList<ProfileNetworkPreferenceList.Preference>();
-        boolean allowFallback = true;
+        boolean hasDefaultPreference = false;
         for (final ProfileNetworkPreference preference : preferences) {
             final NetworkCapabilities nc;
+            boolean allowFallback = true;
             switch (preference.getPreference()) {
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
                     nc = null;
+                    hasDefaultPreference = true;
                     if (preference.getPreferenceEnterpriseId() != 0) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10662,6 +10747,14 @@
                     allowFallback = false;
                     // continue to process the enterprise preference.
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                    // This code is needed even though there is a check later on,
+                    // because isRangeAlreadyInPreferenceList assumes that every preference
+                    // has a UID list.
+                    if (hasDefaultPreference) {
+                        throw new IllegalArgumentException(
+                                "Default profile preference should not be set along with other "
+                                        + "preference");
+                    }
                     if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10685,6 +10778,10 @@
             }
             preferenceList.add(new ProfileNetworkPreferenceList.Preference(
                     profile, nc, allowFallback));
+            if (hasDefaultPreference && preferenceList.size() > 1) {
+                throw new IllegalArgumentException(
+                        "Default profile preference should not be set along with other preference");
+            }
         }
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
                 new Pair<>(preferenceList, listener)));
@@ -10729,12 +10826,6 @@
         return false;
     }
 
-    private void validateNetworkCapabilitiesOfProfileNetworkPreference(
-            @Nullable final NetworkCapabilities nc) {
-        if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
-        ensureRequestableCapabilities(nc);
-    }
-
     private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
             @NonNull final ProfileNetworkPreferenceList prefs) {
         final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
@@ -10785,10 +10876,19 @@
     private void handleSetProfileNetworkPreference(
             @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
             @Nullable final IOnCompleteListener listener) {
+        /*
+         * handleSetProfileNetworkPreference is always called for single user.
+         * preferenceList only contains preferences for different uids within the same user
+         * (enforced by getUidListToBeAppliedForNetworkPreference).
+         * Clear all the existing preferences for the user before applying new preferences.
+         *
+         */
+        mProfileNetworkPreferences = mProfileNetworkPreferences.withoutUser(
+                preferenceList.get(0).user);
         for (final ProfileNetworkPreferenceList.Preference preference : preferenceList) {
-            validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
             mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
         }
+
         removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_PROFILE);
         addPerAppDefaultNetworkRequests(
                 createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences));
@@ -11264,6 +11364,9 @@
         final int defaultRule;
         switch (chain) {
             case ConnectivityManager.FIREWALL_CHAIN_STANDBY:
+            case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+            case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+            case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
                 defaultRule = FIREWALL_RULE_ALLOW;
                 break;
             case ConnectivityManager.FIREWALL_CHAIN_DOZABLE:
@@ -11292,6 +11395,13 @@
     }
 
     @Override
+    public boolean getFirewallChainEnabled(final int chain) {
+        enforceNetworkStackOrSettingsPermission();
+
+        return mBpfNetMaps.isChainEnabled(chain);
+    }
+
+    @Override
     public void replaceFirewallChain(final int chain, final int[] uids) {
         enforceNetworkStackOrSettingsPermission();
 
@@ -11313,6 +11423,15 @@
                     mBpfNetMaps.replaceUidChain("fw_low_power_standby", true /* isAllowList */,
                             uids);
                     break;
+                case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+                    mBpfNetMaps.replaceUidChain("fw_oem_deny_1", false /* isAllowList */, uids);
+                    break;
+                case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+                    mBpfNetMaps.replaceUidChain("fw_oem_deny_2", false /* isAllowList */, uids);
+                    break;
+                case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
+                    mBpfNetMaps.replaceUidChain("fw_oem_deny_3", false /* isAllowList */, uids);
+                    break;
                 default:
                     throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
                             + chain);
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index ccc2776..1209579 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.net.TestNetworkManager.CLAT_INTERFACE_PREFIX;
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
 
@@ -49,6 +50,7 @@
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 
+import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -75,7 +77,11 @@
     @NonNull private final NetworkProvider mNetworkProvider;
 
     // Native method stubs
-    private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+    private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
+            @NonNull String iface);
+
+    private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+            boolean enabled);
 
     @VisibleForTesting
     protected TestNetworkService(@NonNull Context context) {
@@ -98,6 +104,14 @@
         }
     }
 
+    // TODO: find a way to allow the caller to pass in non-clat interface names, ensuring that
+    // those names do not conflict with names created by callers that do not pass in an interface
+    // name.
+    private static boolean isValidInterfaceName(@NonNull final String iface) {
+        return iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TUN_PREFIX)
+                || iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TAP_PREFIX);
+    }
+
     /**
      * Create a TUN or TAP interface with the specified parameters.
      *
@@ -105,30 +119,36 @@
      * interface.
      */
     @Override
-    public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
-            LinkAddress[] linkAddrs) {
+    public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+            LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
         Objects.requireNonNull(linkAddrs, "missing linkAddrs");
 
-        String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
-        String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
+        String interfaceName = iface;
+        if (iface == null) {
+            String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
+            interfaceName = ifacePrefix + sTestTunIndex.getAndIncrement();
+        } else if (!isValidInterfaceName(iface)) {
+            throw new IllegalArgumentException("invalid interface name requested: " + iface);
+        }
+
         final long token = Binder.clearCallingIdentity();
         try {
-            ParcelFileDescriptor tunIntf =
-                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
+            ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
+                    nativeCreateTunTap(isTun, hasCarrier, interfaceName));
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
-                        iface,
+                        interfaceName,
                         addr.getAddress().getHostAddress(),
                         addr.getPrefixLength());
             }
 
             if (bringUp) {
-                NetdUtils.setInterfaceUp(mNetd, iface);
+                NetdUtils.setInterfaceUp(mNetd, interfaceName);
             }
 
-            return new TestNetworkInterface(tunIntf, iface);
+            return new TestNetworkInterface(tunIntf, interfaceName);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } finally {
@@ -360,4 +380,20 @@
     public static void enforceTestNetworkPermissions(@NonNull Context context) {
         context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
     }
+
+    /** Enable / disable TestNetworkInterface carrier */
+    @Override
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        enforceTestNetworkPermissions(mContext);
+        nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
+                enabled);
+        // Explicitly close fd after use to prevent StrictMode from complaining.
+        // Also, explicitly referencing iface guarantees that the object is not garbage collected
+        // before nativeSetTunTapCarrierEnabled() executes.
+        try {
+            iface.getFileDescriptor().close();
+        } catch (IOException e) {
+            // if the close fails, there is not much that can be done -- move on.
+        }
+    }
 }
diff --git a/service/src/com/android/server/UidOwnerValue.java b/service/src/com/android/server/UidOwnerValue.java
new file mode 100644
index 0000000..f89e354
--- /dev/null
+++ b/service/src/com/android/server/UidOwnerValue.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for per uid traffic control configuration map  */
+public class UidOwnerValue extends Struct {
+    // Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    // A bitmask of match type.
+    @Field(order = 1, type = Type.U32)
+    public final long rule;
+
+    public UidOwnerValue(final long iif, final long rule) {
+        this.iif = iif;
+        this.rule = rule;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 8aa5990..5ea586a 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
@@ -100,11 +101,11 @@
     private static final String CLAT_INGRESS6_MAP_PATH = makeMapPath("ingress6");
 
     private static String makeMapPath(String which) {
-        return "/sys/fs/bpf/map_clatd_clat_" + which + "_map";
+        return "/sys/fs/bpf/net_shared/map_clatd_clat_" + which + "_map";
     }
 
     private static String makeProgPath(boolean ingress, boolean ether) {
-        String path = "/sys/fs/bpf/prog_clatd_schedcls_"
+        String path = "/sys/fs/bpf/net_shared/prog_clatd_schedcls_"
                 + (ingress ? "ingress6" : "egress4")
                 + "_clat_"
                 + (ether ? "ether" : "rawip");
@@ -122,8 +123,12 @@
     @Nullable
     private ClatdTracker mClatdTracker = null;
 
+    /**
+     * Dependencies of ClatCoordinator which makes ConnectivityService injection
+     * in tests.
+     */
     @VisibleForTesting
-    abstract static class Dependencies {
+    public abstract static class Dependencies {
         /**
           * Get netd.
           */
@@ -342,6 +347,19 @@
                     && this.pid == that.pid
                     && this.cookie == that.cookie;
         }
+
+        @Override
+        public String toString() {
+            return "iface: " + iface
+                    + " (" + ifIndex + ")"
+                    + ", v4iface: " + v4iface
+                    + " (" + v4ifIndex + ")"
+                    + ", v4: " + v4
+                    + ", v6: " + v6
+                    + ", pfx96: " + pfx96
+                    + ", pid: " + pid
+                    + ", cookie: " + cookie;
+        }
     };
 
     @VisibleForTesting
@@ -493,6 +511,31 @@
         }
     }
 
+    private void maybeCleanUp(ParcelFileDescriptor tunFd, ParcelFileDescriptor readSock6,
+            ParcelFileDescriptor writeSock6) {
+        if (tunFd != null) {
+            try {
+                tunFd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close tun file descriptor " + e);
+            }
+        }
+        if (readSock6 != null) {
+            try {
+                readSock6.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close read socket " + e);
+            }
+        }
+        if (writeSock6 != null) {
+            try {
+                writeSock6.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close write socket " + e);
+            }
+        }
+    }
+
     /**
      * Start clatd for a given interface and NAT64 prefix.
      */
@@ -541,8 +584,15 @@
 
         // [3] Open, configure and bring up the tun interface.
         // Create the v4-... tun interface.
+
+        // Initialize all required file descriptors with null pointer. This makes the following
+        // error handling easier. Simply always call #maybeCleanUp for closing file descriptors,
+        // if any valid ones, in error handling.
+        ParcelFileDescriptor tunFd = null;
+        ParcelFileDescriptor readSock6 = null;
+        ParcelFileDescriptor writeSock6 = null;
+
         final String tunIface = CLAT_PREFIX + iface;
-        final ParcelFileDescriptor tunFd;
         try {
             tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
         } catch (IOException e) {
@@ -551,7 +601,7 @@
 
         final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
         if (tunIfIndex == INVALID_IFINDEX) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Fail to get interface index for interface " + tunIface);
         }
 
@@ -559,24 +609,27 @@
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
             Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
         }
 
         // Detect ipv4 mtu.
         final Integer fwmark = getFwmark(netId);
-        final int detectedMtu = mDeps.detectMtu(pfx96Str,
+        final int detectedMtu;
+        try {
+            detectedMtu = mDeps.detectMtu(pfx96Str,
                 ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+        } catch (IOException e) {
+            maybeCleanUp(tunFd, readSock6, writeSock6);
+            throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
+        }
         final int mtu = adjustMtu(detectedMtu);
         Log.i(TAG, "ipv4 mtu is " + mtu);
 
-        // TODO: add setIptablesDropRule
-
         // Config tun interface mtu, address and bring up.
         try {
             mNetd.interfaceSetMtu(tunIface, mtu);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
         }
         final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -588,14 +641,13 @@
         try {
             mNetd.interfaceSetCfg(ifConfig);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
                     + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
         }
 
         // [4] Open and configure local 464xlat read/write sockets.
         // Opens a packet socket to receive IPv6 packets in clatd.
-        final ParcelFileDescriptor readSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket() because we would
             // like to use ParcelFileDescriptor to manage file descriptor. But ctor
@@ -603,27 +655,23 @@
             // descriptor to initialize ParcelFileDescriptor object instead.
             readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
         } catch (IOException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Open packet socket failed: " + e);
         }
 
         // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
-        final ParcelFileDescriptor writeSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket(). See above
             // reason why we use jniOpenPacketSocket6().
             writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Open raw socket failed: " + e);
         }
 
         final int ifIndex = mDeps.getInterfaceIndex(iface);
         if (ifIndex == INVALID_IFINDEX) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Fail to get interface index for interface " + iface);
         }
 
@@ -631,9 +679,7 @@
         try {
             mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("add anycast sockopt failed: " + e);
         }
 
@@ -642,9 +688,7 @@
         try {
             cookie = mDeps.tagSocketAsClat(writeSock6.getFileDescriptor());
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("tag raw socket failed: " + e);
         }
 
@@ -652,9 +696,7 @@
         try {
             mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("configure packet socket failed: " + e);
         }
 
@@ -668,9 +710,9 @@
             mDeps.untagSocket(cookie);
             throw new IOException("Error start clatd on " + iface + ": " + e);
         } finally {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            // The file descriptors have been duplicated (dup2) to clatd in native_startClatd().
+            // Close these file descriptor stubs which are unused anymore.
+            maybeCleanUp(tunFd, readSock6, writeSock6);
         }
 
         // [6] Initialize and store clatd tracker object.
@@ -738,6 +780,69 @@
         mClatdTracker = null;
     }
 
+    private void dumpBpfIngress(@NonNull IndentingPrintWriter pw) {
+        if (mIngressMap == null) {
+            pw.println("No BPF ingress6 map");
+            return;
+        }
+
+        try {
+            if (mIngressMap.isEmpty()) {
+                pw.println("<empty>");
+            }
+            pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif");
+            pw.increaseIndent();
+            mIngressMap.forEach((k, v) -> {
+                // TODO: print interface name
+                pw.println(String.format("%d %s/96 %s -> %s %d", k.iif, k.pfx96, k.local6,
+                        v.local4, v.oif));
+            });
+            pw.decreaseIndent();
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF ingress6 map: " + e);
+        }
+    }
+
+    private void dumpBpfEgress(@NonNull IndentingPrintWriter pw) {
+        if (mEgressMap == null) {
+            pw.println("No BPF egress4 map");
+            return;
+        }
+
+        try {
+            if (mEgressMap.isEmpty()) {
+                pw.println("<empty>");
+            }
+            pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif");
+            pw.increaseIndent();
+            mEgressMap.forEach((k, v) -> {
+                // TODO: print interface name
+                pw.println(String.format("%d %s -> %s %s/96 %d %s", k.iif, k.local4, v.local6,
+                        v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip"));
+            });
+            pw.decreaseIndent();
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF egress4 map: " + e);
+        }
+    }
+
+    /**
+     * Dump the cordinator information.
+     *
+     * @param pw print writer.
+     */
+    public void dump(@NonNull IndentingPrintWriter pw) {
+        // TODO: move map dump to a global place to avoid duplicate dump while there are two or
+        // more IPv6 only networks.
+        pw.println("CLAT tracker: " + mClatdTracker.toString());
+        pw.println("Forwarding rules:");
+        pw.increaseIndent();
+        dumpBpfIngress(pw);
+        dumpBpfEgress(pw);
+        pw.decreaseIndent();
+        pw.println();
+    }
+
     /**
      * Get clatd tracker. For test only.
      */
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cde6ea7..c1ba40e 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -47,10 +47,11 @@
     private static final String TAG = ConnectivityNativeService.class.getSimpleName();
     private static final String CGROUP_PATH = "/sys/fs/cgroup";
     private static final String V4_PROG_PATH =
-            "/sys/fs/bpf/prog_block_bind4_block_port";
+            "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
     private static final String V6_PROG_PATH =
-            "/sys/fs/bpf/prog_block_bind6_block_port";
-    private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map";
+            "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+    private static final String BLOCKED_PORTS_MAP_PATH =
+            "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
 
     private final Context mContext;
 
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 53b276e..7829d1a 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_REQUEST_DECLINED;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS;
 import static android.system.OsConstants.ETH_P_ALL;
 
@@ -37,6 +38,7 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.NetworkInterface;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -50,7 +52,7 @@
 
     private static final String TAG = DscpPolicyTracker.class.getSimpleName();
     private static final String PROG_PATH =
-            "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+            "/sys/fs/bpf/net_shared/prog_dscp_policy_schedcls_set_dscp";
     // Name is "map + *.o + map_name + map". Can probably shorten this
     private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
             "dscp_policy_ipv4_dscp_policies");
@@ -59,37 +61,75 @@
     private static final int MAX_POLICIES = 16;
 
     private static String makeMapPath(String which) {
-        return "/sys/fs/bpf/map_" + which + "_map";
+        return "/sys/fs/bpf/net_shared/map_" + which + "_map";
     }
 
     private Set<String> mAttachedIfaces;
 
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
-    private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+    // The actual policy rules used by the BPF code to process packets
+    // are in mBpfDscpIpv4Policies and mBpfDscpIpv4Policies. Both of
+    // these can contain up to MAX_POLICIES rules.
+    //
+    // A given policy always consumes one entry in both the IPv4 and
+    // IPv6 maps even if if's an IPv4-only or IPv6-only policy.
+    //
+    // Each interface index has a SparseIntArray of rules which maps a
+    // policy ID to the index of the corresponding rule in the maps.
+    // mIfaceIndexToPolicyIdBpfMapIndex maps the interface index to
+    // the per-interface SparseIntArray.
+    private final HashMap<Integer, SparseIntArray> mIfaceIndexToPolicyIdBpfMapIndex;
 
     public DscpPolicyTracker() throws ErrnoException {
         mAttachedIfaces = new HashSet<String>();
-
-        mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+        mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
         mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
         mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
     }
 
+    private boolean isUnusedIndex(int index) {
+        for (SparseIntArray ifacePolicies : mIfaceIndexToPolicyIdBpfMapIndex.values()) {
+            if (ifacePolicies.indexOfValue(index) >= 0) return false;
+        }
+        return true;
+    }
+
     private int getFirstFreeIndex() {
+        if (mIfaceIndexToPolicyIdBpfMapIndex.size() == 0) return 0;
         for (int i = 0; i < MAX_POLICIES; i++) {
-            if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+            if (isUnusedIndex(i)) {
+                return i;
+            }
         }
         return MAX_POLICIES;
     }
 
+    private int findIndex(int policyId, int ifIndex) {
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies != null) {
+            final int existingIndex = ifacePolicies.get(policyId, -1);
+            if (existingIndex != -1) {
+                return existingIndex;
+            }
+        }
+
+        final int firstIndex = getFirstFreeIndex();
+        if (firstIndex >= MAX_POLICIES) {
+            // New policy is being added, but max policies has already been reached.
+            return -1;
+        }
+        return firstIndex;
+    }
+
     private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
         try {
             nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
         } catch (RemoteException e) {
-            Log.d(TAG, "Failed update policy status: ", e);
+            Log.e(TAG, "Failed update policy status: ", e);
         }
     }
 
@@ -107,37 +147,43 @@
                         || policy.getSourceAddress() instanceof Inet6Address));
     }
 
-    private int addDscpPolicyInternal(DscpPolicy policy) {
+    private int getIfaceIndex(NetworkAgentInfo nai) {
+        String iface = nai.linkProperties.getInterfaceName();
+        NetworkInterface netIface;
+        try {
+            netIface = NetworkInterface.getByName(iface);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to get iface index for " + iface + ": " + e);
+            netIface = null;
+        }
+        return (netIface != null) ? netIface.getIndex() : 0;
+    }
+
+    private int addDscpPolicyInternal(DscpPolicy policy, int ifIndex) {
         // If there is no existing policy with a matching ID, and we are already at
         // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
-        final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
-        if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
-            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies == null) {
+            ifacePolicies = new SparseIntArray(MAX_POLICIES);
         }
 
         // Currently all classifiers are supported, if any are removed return
         // DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
         // and for any other generic error DSCP_POLICY_STATUS_REQUEST_DECLINED
 
-        int addIndex = 0;
-        // If a policy with a matching ID exists, replace it, otherwise use the next free
-        // index for the policy.
-        if (existingIndex != -1) {
-            addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
-        } else {
-            addIndex = getFirstFreeIndex();
+        final int addIndex = findIndex(policy.getPolicyId(), ifIndex);
+        if (addIndex == -1) {
+            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
         }
 
         try {
-            mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
-
             // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
-            // are both null or if they are both instances of Inet6Address.
+            // are both null or if they are both instances of Inet4Address.
             if (matchesIpv4(policy)) {
                 mBpfDscpIpv4Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                            policy.getDestinationAddress(),
+                            policy.getDestinationAddress(), ifIndex,
                             policy.getSourcePort(), policy.getDestinationPortRange(),
                             (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
@@ -148,10 +194,16 @@
                 mBpfDscpIpv6Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                                policy.getDestinationAddress(),
+                                policy.getDestinationAddress(), ifIndex,
                                 policy.getSourcePort(), policy.getDestinationPortRange(),
                                 (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
+
+            ifacePolicies.put(policy.getPolicyId(), addIndex);
+            // Only add the policy to the per interface map if the policy was successfully
+            // added to both bpf maps above. It is safe to assume that if insert fails for
+            // one map then it fails for both.
+            mIfaceIndexToPolicyIdBpfMapIndex.put(ifIndex, ifacePolicies);
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to insert policy into map: ", e);
             return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
@@ -166,6 +218,7 @@
      *
      * DSCP_POLICY_STATUS_SUCCESS - if policy was added successfully
      * DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     * DSCP_POLICY_STATUS_REQUEST_DECLINED - Interface index was invalid
      */
     public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
@@ -177,11 +230,19 @@
             }
         }
 
-        int status = addDscpPolicyInternal(policy);
+        final int ifIndex = getIfaceIndex(nai);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Iface index is invalid");
+            sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
+            return;
+        }
+
+        int status = addDscpPolicyInternal(policy, ifIndex);
         sendStatus(nai, policy.getPolicyId(), status);
     }
 
-    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index,
+            boolean sendCallback) {
         int status = DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
         try {
             mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
@@ -191,7 +252,9 @@
             Log.e(TAG, "Failed to delete policy from map: ", e);
         }
 
-        sendStatus(nai, policyId, status);
+        if (sendCallback) {
+            sendStatus(nai, policyId, status);
+        }
     }
 
     /**
@@ -204,36 +267,44 @@
             return;
         }
 
-        if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
-            removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
-            mPolicyIdToBpfMapIndex.delete(policyId);
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+
+        final int existingIndex = ifacePolicies.get(policyId, -1);
+        if (existingIndex == -1) {
+            Log.e(TAG, "Policy " + policyId + " does not exist in map.");
+            sendStatus(nai, policyId, DSCP_POLICY_STATUS_POLICY_NOT_FOUND);
+            return;
         }
 
-        // TODO: detach should only occur if no more policies are present on the nai's iface.
-        if (mPolicyIdToBpfMapIndex.size() == 0) {
+        removePolicyFromMap(nai, policyId, existingIndex, true);
+        ifacePolicies.delete(policyId);
+
+        if (ifacePolicies.size() == 0) {
             detachProgram(nai.linkProperties.getInterfaceName());
         }
     }
 
     /**
-     * Remove all DSCP policies and detach program.
+     * Remove all DSCP policies and detach program. Send callback if requested.
      */
-    // TODO: Remove all should only remove policies from corresponding nai iface.
-    public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+    public void removeAllDscpPolicies(NetworkAgentInfo nai, boolean sendCallback) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
             // Nothing to remove since program is not attached. Send update for policy
             // id 0. The status update must contain a policy ID, and 0 is an invalid id.
-            sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+            if (sendCallback) {
+                sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+            }
             return;
         }
 
-        for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
-            removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
-                    mPolicyIdToBpfMapIndex.valueAt(i));
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+        for (int i = 0; i < ifacePolicies.size(); i++) {
+            removePolicyFromMap(nai, ifacePolicies.keyAt(i), ifacePolicies.valueAt(i),
+                    sendCallback);
         }
-        mPolicyIdToBpfMapIndex.clear();
-
-        // Can detach program since no policies are active.
+        ifacePolicies.clear();
         detachProgram(nai.linkProperties.getInterfaceName());
     }
 
@@ -241,12 +312,12 @@
      * Attach BPF program
      */
     private boolean attachProgram(@NonNull String iface) {
-        // TODO: attach needs to be per iface not program.
-
         try {
             NetworkInterface netIface = NetworkInterface.getByName(iface);
+            boolean isEth = TcUtils.isEthernet(iface);
+            String path = PROG_PATH + (isEth ? "_ether" : "_raw_ip");
             TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
-                    PROG_PATH);
+                    path);
         } catch (IOException e) {
             Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
             return false;
@@ -264,9 +335,9 @@
             if (netIface != null) {
                 TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
             }
+            mAttachedIfaces.remove(iface);
         } catch (IOException e) {
             Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
         }
-        mAttachedIfaces.remove(iface);
     }
 }
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index cb40306..6e4e7eb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -31,29 +31,31 @@
 public class DscpPolicyValue extends Struct {
     private static final String TAG = DscpPolicyValue.class.getSimpleName();
 
-    // TODO: add the interface index.
     @Field(order = 0, type = Type.ByteArray, arraysize = 16)
     public final byte[] src46;
 
     @Field(order = 1, type = Type.ByteArray, arraysize = 16)
     public final byte[] dst46;
 
-    @Field(order = 2, type = Type.UBE16)
-    public final int srcPort;
+    @Field(order = 2, type = Type.U32)
+    public final long ifIndex;
 
     @Field(order = 3, type = Type.UBE16)
-    public final int dstPortStart;
+    public final int srcPort;
 
     @Field(order = 4, type = Type.UBE16)
+    public final int dstPortStart;
+
+    @Field(order = 5, type = Type.UBE16)
     public final int dstPortEnd;
 
-    @Field(order = 5, type = Type.U8)
+    @Field(order = 6, type = Type.U8)
     public final short proto;
 
-    @Field(order = 6, type = Type.U8)
+    @Field(order = 7, type = Type.U8)
     public final short dscp;
 
-    @Field(order = 7, type = Type.U8, padding = 3)
+    @Field(order = 8, type = Type.U8, padding = 3)
     public final short mask;
 
     private static final int SRC_IP_MASK = 0x1;
@@ -69,6 +71,7 @@
         return true;
     }
 
+    // TODO:  move to frameworks/libs/net and have this and BpfCoordinator import it.
     private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
         final byte[] addr6 = new byte[16];
         if (ia != null) {
@@ -117,13 +120,12 @@
         return mask;
     }
 
-    // This constructor is necessary for BpfMap#getValue since all values must be
-    // in the constructor.
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final int dstPortStart, final int dstPortEnd, final short proto,
+    private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
             final short dscp) {
         this.src46 = toAddressField(src46);
         this.dst46 = toAddressField(dst46);
+        this.ifIndex = ifIndex;
 
         // These params need to be stored as 0 because uints are used in BpfMap.
         // If they are -1 BpfMap write will throw errors.
@@ -138,15 +140,15 @@
         this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
     }
 
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final Range<Integer> dstPort, final short proto,
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final Range<Integer> dstPort, final short proto,
             final short dscp) {
-        this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+        this(src46, dst46, ifIndex, srcPort, dstPort != null ? dstPort.getLower() : -1,
                 dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
     }
 
     public static final DscpPolicyValue NONE = new DscpPolicyValue(
-            null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+            null /* src46 */, null /* dst46 */, 0 /* ifIndex */, -1 /* srcPort */,
             -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
             (short) 0 /* dscp */);
 
@@ -170,9 +172,9 @@
 
         try {
             return String.format(
-                    "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
-                    + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
-                    dstPortEnd, proto, dscp);
+                    "src46: %s, dst46: %s, ifIndex: %d, srcPort: %d, dstPortStart: %d,"
+                    + " dstPortEnd: %d, protocol: %d, dscp %s", srcIpString, dstIpString,
+                    ifIndex, srcPort, dstPortStart, dstPortEnd, proto, dscp);
         } catch (IllegalArgumentException e) {
             return String.format("String format error: " + e);
         }
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 7b06682..e4ad391 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
 import static com.android.net.module.util.CollectionUtils.contains;
 
@@ -36,9 +37,12 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.server.ConnectivityService;
 
+import java.io.IOException;
 import java.net.Inet6Address;
 import java.util.Objects;
 
@@ -96,6 +100,7 @@
     private String mIface;
     private Inet6Address mIPv6Address;
     private State mState = State.IDLE;
+    private ClatCoordinator mClatCoordinator;
 
     private boolean mEnableClatOnCellular;
     private boolean mPrefixDiscoveryRunning;
@@ -106,6 +111,7 @@
         mNetd = netd;
         mNetwork = nai;
         mEnableClatOnCellular = deps.getCellular464XlatEnabled();
+        mClatCoordinator = deps.getClatCoordinator(mNetd);
     }
 
     /**
@@ -122,6 +128,11 @@
         final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType());
         final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState());
 
+        // Allow to run clat on test network.
+        // TODO: merge to boolean "supported" once boolean "supported" is migrated to
+        // NetworkCapabilities.TRANSPORT_*.
+        final boolean isTestNetwork = nai.networkCapabilities.hasTransport(TRANSPORT_TEST);
+
         // Only run clat on networks that have a global IPv6 address and don't have a native IPv4
         // address.
         LinkProperties lp = nai.linkProperties;
@@ -132,8 +143,8 @@
         final boolean skip464xlat = (nai.netAgentConfig() != null)
                 && nai.netAgentConfig().skip464xlat;
 
-        return supported && connected && isIpv6OnlyNetwork && !skip464xlat && !nai.destroyed
-                && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+        return (supported || isTestNetwork) && connected && isIpv6OnlyNetwork && !skip464xlat
+                && !nai.destroyed && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
                 ? isCellular464XlatEnabled() : true);
     }
 
@@ -179,10 +190,18 @@
     private void enterStartingState(String baseIface) {
         mNat64PrefixInUse = selectNat64Prefix();
         String addrStr = null;
-        try {
-            addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                addrStr = mClatCoordinator.clatStart(baseIface, getNetId(), mNat64PrefixInUse);
+            } catch (IOException e) {
+                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+            }
+        } else {
+            try {
+                addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+            }
         }
         mIface = CLAT_PREFIX + baseIface;
         mBaseIface = baseIface;
@@ -256,10 +275,18 @@
         }
 
         Log.i(TAG, "Stopping clatd on " + mBaseIface);
-        try {
-            mNetd.clatdStop(mBaseIface);
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                mClatCoordinator.clatStop();
+            } catch (IOException e) {
+                Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+            }
+        } else {
+            try {
+                mNetd.clatdStop(mBaseIface);
+            } catch (RemoteException | ServiceSpecificException e) {
+                Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+            }
         }
 
         String iface = mIface;
@@ -506,6 +533,24 @@
         mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
     }
 
+    /**
+     * Dump the NAT64 xlat information.
+     *
+     * @param pw print writer.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        if (SdkLevel.isAtLeastT()) {
+            if (isStarted()) {
+                pw.println("ClatCoordinator:");
+                pw.increaseIndent();
+                mClatCoordinator.dump(pw);
+                pw.decreaseIndent();
+            } else {
+                pw.println("<not started>");
+            }
+        }
+    }
+
     @Override
     public String toString() {
         return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 1fc5a8f..b40b6e0 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -19,6 +19,7 @@
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
@@ -59,6 +60,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.WakeupMessage;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.server.ConnectivityService;
@@ -1186,6 +1188,15 @@
     }
 
     /**
+     * Dump the NAT64 xlat information.
+     *
+     * @param pw print writer.
+     */
+    public void dumpNat464Xlat(IndentingPrintWriter pw) {
+        clatd.dump(pw);
+    }
+
+    /**
      * Sets the most recent ConnectivityReport for this network.
      *
      * <p>This should only be called from the ConnectivityService thread.
@@ -1214,20 +1225,22 @@
      *
      * @param nc the capabilities to sanitize
      * @param creatorUid the UID of the process creating this network agent
+     * @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
      * @param authenticator the carrier privilege authenticator to check for telephony constraints
      */
     public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
-            final int creatorUid, @NonNull final CarrierPrivilegeAuthenticator authenticator) {
+            final int creatorUid, final boolean hasAutomotiveFeature,
+            @Nullable final CarrierPrivilegeAuthenticator authenticator) {
         if (nc.hasTransport(TRANSPORT_TEST)) {
             nc.restrictCapabilitiesForTestNetwork(creatorUid);
         }
-        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, authenticator)) {
+        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
             nc.setAllowedUids(new ArraySet<>());
         }
     }
 
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
-            @NonNull final NetworkCapabilities nc,
+            @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
         // NCs without access UIDs are fine.
         if (!nc.hasAllowedUids()) return true;
@@ -1242,6 +1255,11 @@
         // access UIDs
         if (nc.hasTransport(TRANSPORT_TEST)) return true;
 
+        // Factories that make ethernet networks can allow UIDs for automotive devices.
+        if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
+            return true;
+        }
+
         // Factories that make cell networks can allow the UID for the carrier service package.
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
@@ -1252,8 +1270,6 @@
             return true;
         }
 
-        // TODO : accept Railway callers
-
         return false;
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index 1e975dd..eea382e 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -22,6 +22,7 @@
 import android.net.NetworkRequest;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
@@ -143,6 +144,11 @@
 
     @Override
     public String toString() {
-        return "NetworkOffer [ Score " + score + " Caps " + caps + "]";
+        final ArrayList<Integer> neededRequestIds = new ArrayList<>();
+        for (final NetworkRequest request : mCurrentlyNeeded) {
+            neededRequestIds.add(request.requestId);
+        }
+        return "NetworkOffer [ Provider Id (" + providerId + ") " + score + " Caps "
+                + caps + " Needed by " + neededRequestIds + "]";
     }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 62b3add..fd1ed60 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -37,6 +37,7 @@
 import static com.android.net.module.util.CollectionUtils.toIntArray;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -50,7 +51,6 @@
 import android.net.INetd;
 import android.net.UidRange;
 import android.net.Uri;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Process;
 import android.os.RemoteException;
@@ -69,12 +69,12 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ProcessShimImpl;
 import com.android.networkstack.apishim.common.ProcessShim;
 import com.android.server.BpfNetMaps;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -108,10 +108,19 @@
     @GuardedBy("this")
     private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
 
-    // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
-    // for apps under the VPN
+    // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+    // ranges for apps under the VPNs which enable interface filtering.
+    // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+    // enable interface filtering.
     @GuardedBy("this")
-    private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+    private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+    // Items are uid ranges for apps under the VPN Lockdown
+    // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+    // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+    // So we need to maintain uid range with multiset.
+    @GuardedBy("this")
+    private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
 
     // A set of appIds for apps across all users on the device. We track appIds instead of uids
     // directly to reduce its size and also eliminate the need to update this set when user is
@@ -134,7 +143,9 @@
 
     // Store appIds traffic permissions for each user.
     // Keys are users, Values are SparseArrays where each entry maps an appId to the permissions
-    // that appId has within that user.
+    // that appId has within that user. The permissions are a bitmask of PERMISSION_INTERNET and
+    // PERMISSION_UPDATE_DEVICE_STATS, or 0 (PERMISSION_NONE) if the app has neither of those
+    // permissions. They can never be PERMISSION_UNINSTALLED.
     @GuardedBy("this")
     private final Map<UserHandle, SparseIntArray> mUsersTrafficPermissions = new ArrayMap<>();
 
@@ -199,6 +210,38 @@
         }
     }
 
+    private static class MultiSet<T> {
+        private final Map<T, Integer> mMap = new ArrayMap<>();
+
+        /**
+         * Returns the number of key in the set before this addition.
+         */
+        public int add(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            mMap.put(key, oldCount + 1);
+            return oldCount;
+        }
+
+        /**
+         * Return the number of key in the set before this removal.
+         */
+        public int remove(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            if (oldCount == 0) {
+                Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+            } else if (oldCount == 1) {
+                mMap.remove(key);
+            } else {
+                mMap.put(key, oldCount - 1);
+            }
+            return oldCount;
+        }
+
+        public Set<T> getSet() {
+            return mMap.keySet();
+        }
+    }
+
     public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
             @NonNull final BpfNetMaps bpfNetMaps) {
         this(context, netd, bpfNetMaps, new Dependencies());
@@ -419,7 +462,14 @@
         if (appInfo == null) return false;
         // Check whether package's uid is in allowed on restricted networks uid list. If so, this
         // uid can have netd system permission.
-        return mUidsAllowedOnRestrictedNetworks.contains(appInfo.uid);
+        return isUidAllowedOnRestrictedNetworks(appInfo.uid);
+    }
+
+    /**
+     * Returns whether the given uid is in allowed on restricted networks list.
+     */
+    public synchronized boolean isUidAllowedOnRestrictedNetworks(final int uid) {
+        return mUidsAllowedOnRestrictedNetworks.contains(uid);
     }
 
     @VisibleForTesting
@@ -545,17 +595,21 @@
 
         // Remove appIds traffic permission that belongs to the user
         final SparseIntArray removedUserAppIds = mUsersTrafficPermissions.remove(user);
-        // Generate appIds from left users.
+        // Generate appIds from the remaining users.
         final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+
+        if (removedUserAppIds == null) {
+            Log.wtf(TAG, "onUserRemoved: Receive unknown user=" + user);
+            return;
+        }
+
         // Clear permission on those appIds belong to this user only, set the permission to
         // PERMISSION_UNINSTALLED.
-        if (removedUserAppIds != null) {
-            for (int i = 0; i < removedUserAppIds.size(); i++) {
-                final int appId = removedUserAppIds.keyAt(i);
-                // Need to clear permission if the removed appId is not found in the array.
-                if (appIds.indexOfKey(appId) < 0) {
-                    appIds.put(appId, PERMISSION_UNINSTALLED);
-                }
+        for (int i = 0; i < removedUserAppIds.size(); i++) {
+            final int appId = removedUserAppIds.keyAt(i);
+            // Need to clear permission if the removed appId is not found in the array.
+            if (appIds.indexOfKey(appId) < 0) {
+                appIds.put(appId, PERMISSION_UNINSTALLED);
             }
         }
         sendAppIdsTrafficPermission(appIds);
@@ -613,16 +667,30 @@
     }
 
     private synchronized void updateVpnUid(int uid, boolean add) {
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             if (UidRange.containsUid(vpn.getValue(), uid)) {
                 final Set<Integer> changedUids = new HashSet<>();
                 changedUids.add(uid);
-                removeBypassingUids(changedUids, -1 /* vpnAppUid */);
                 updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
             }
         }
     }
 
+    private synchronized void updateLockdownUid(int uid, boolean add) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+
+        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)) {
+            updateLockdownUidRule(uid, add);
+        }
+    }
+
     /**
      * This handles both network and traffic permission, because there is no overlap in actual
      * values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -650,7 +718,6 @@
     }
 
     private synchronized void updateAppIdTrafficPermission(int uid) {
-        final int appId = UserHandle.getAppId(uid);
         final int uidTrafficPerm = getTrafficPermissionForUid(uid);
         final SparseIntArray userTrafficPerms =
                 mUsersTrafficPermissions.get(UserHandle.getUserHandleForUid(uid));
@@ -661,6 +728,7 @@
         // Do not put PERMISSION_UNINSTALLED into the array. If no package left on the uid
         // (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
         // permission to the appId.
+        final int appId = UserHandle.getAppId(uid);
         if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
             userTrafficPerms.delete(appId);
         } else {
@@ -716,13 +784,14 @@
 
         // If the newly-installed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen after the mUidToNetworkPerm update above, since
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, true /* add */);
+        updateLockdownUid(uid, true /* add */);
         mAllApps.add(appId);
 
         // Log package added.
-        mPermissionUpdateLogs.log("Package add: name=" + packageName + ", uid=" + uid
+        mPermissionUpdateLogs.log("Package add: uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
                 + ", tPerm=" + permissionToString(appIdTrafficPerm));
@@ -762,9 +831,10 @@
 
         // If the newly-removed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen before the mUidToNetworkPerm update below, since
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, false /* add */);
+        updateLockdownUid(uid, false /* add */);
         // If the package has been removed from all users on the device, clear it form mAllApps.
         if (mPackageManager.getNameForUid(uid) == null) {
             mAllApps.remove(appId);
@@ -774,7 +844,7 @@
         final int permission = highestUidNetworkPermission(uid);
 
         // Log package removed.
-        mPermissionUpdateLogs.log("Package remove: name=" + packageName + ", uid=" + uid
+        mPermissionUpdateLogs.log("Package remove: uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
                 + ", tPerm=" + permissionToString(appIdTrafficPerm));
@@ -846,48 +916,100 @@
     /**
      * Called when a new set of UID ranges are added to an active VPN network
      *
-     * @param iface The active VPN network's interface name
+     * @param iface The active VPN network's interface name. Null iface indicates that the app is
+     *              allowed to receive packets on all interfaces.
      * @param rangesToAdd The new UID ranges to be added to the network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+    public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
             int vpnAppUid) {
         // Calculate the list of new app uids under the VPN due to the new UID ranges and update
         // Netd about them. Because mAllApps only contains appIds instead of uids, the result might
         // be an overestimation if an app is not installed on the user on which the VPN is running,
-        // but that's safe.
+        // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+        // packets to that UID is fine.
         final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
-        if (mVpnUidRanges.containsKey(iface)) {
-            mVpnUidRanges.get(iface).addAll(rangesToAdd);
+        if (mVpnInterfaceUidRanges.containsKey(iface)) {
+            mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
         } else {
-            mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+            mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
         }
     }
 
     /**
      * Called when a set of UID ranges are removed from an active VPN network
      *
-     * @param iface The VPN network's interface name
+     * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+     *              to receive packets on all interfaces.
      * @param rangesToRemove Existing UID ranges to be removed from the VPN network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+    public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
             Set<UidRange> rangesToRemove, int vpnAppUid) {
         // Calculate the list of app uids that are no longer under the VPN due to the removed UID
         // ranges and update Netd about them.
         final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
-        Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+        Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
         if (existingRanges == null) {
             loge("Attempt to remove unknown vpn uid Range iface = " + iface);
             return;
         }
         existingRanges.removeAll(rangesToRemove);
         if (existingRanges.size() == 0) {
-            mVpnUidRanges.remove(iface);
+            mVpnInterfaceUidRanges.remove(iface);
+        }
+    }
+
+    /**
+     * Called when UID ranges under VPN Lockdown are updated
+     *
+     * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+     *        are to be removed from the Lockdown.
+     * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+     *               app's UID in any special way. The caller is responsible for excluding the VPN
+     *               app UID from the passed-in ranges.
+     *               Ranges can have duplications and/or contain the range that is already subject
+     *               to lockdown. However, ranges can not have overlaps with other ranges including
+     *               ranges that are currently subject to lockdown.
+     */
+    public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+        final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+        for (final UidRange range : ranges) {
+            if (add) {
+                // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+                // currently.
+                if (mVpnLockdownUidRanges.add(range) == 0) {
+                    affectedUidRanges.add(range);
+                }
+            } else {
+                // Rule will be removed if the number of the range in the set is 1 before the
+                // removal.
+                if (mVpnLockdownUidRanges.remove(range) == 1) {
+                    affectedUidRanges.add(range);
+                }
+            }
+        }
+
+        // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+        // apps that are installed only on some users but not others. But that's safe: if an app is
+        // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+        final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+        // We skip adding rule to privileged apps and allow them to bypass incoming packet
+        // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+        // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+        // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+        // filtering, privileged apps can still bypass outgoing packet filtering because the
+        // prohibit rules observe the protected from VPN bit.
+        for (final int uid: affectedUids) {
+            if (!hasRestrictedNetworksPermission(uid)) {
+                updateLockdownUidRule(uid, add);
+            }
         }
     }
 
@@ -926,7 +1048,7 @@
      */
     private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
         uids.remove(vpnAppUid);
-        uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+        uids.removeIf(this::hasRestrictedNetworksPermission);
     }
 
     /**
@@ -935,6 +1057,7 @@
      *
      * This is to instruct netd to set up appropriate filtering rules for these uids, such that they
      * can only receive ingress packets from the VPN's tunnel interface (and loopback).
+     * Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
      *
      * @param iface the interface name of the active VPN connection
      * @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -955,6 +1078,14 @@
         }
     }
 
+    private void updateLockdownUidRule(int uid, boolean add) {
+        try {
+            mBpfNetMaps.updateUidLockdownRule(uid, add);
+        } catch (ServiceSpecificException e) {
+            loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
+        }
+    }
+
     /**
      * Send the updated permission information to netd. Called upon package install/uninstall.
      *
@@ -984,10 +1115,6 @@
      */
     @VisibleForTesting
     void sendAppIdsTrafficPermission(SparseIntArray netdPermissionsAppIds) {
-        if (mNetd == null) {
-            Log.e(TAG, "Failed to get the netd service");
-            return;
-        }
         final ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
@@ -1046,8 +1173,14 @@
 
     /** Should only be used by unit tests */
     @VisibleForTesting
-    public Set<UidRange> getVpnUidRanges(String iface) {
-        return mVpnUidRanges.get(iface);
+    public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+        return mVpnInterfaceUidRanges.get(iface);
+    }
+
+    /** Should only be used by unit tests */
+    @VisibleForTesting
+    public Set<UidRange> getVpnLockdownUidRanges() {
+        return mVpnLockdownUidRanges.getSet();
     }
 
     private synchronized void onSettingChanged() {
@@ -1112,7 +1245,7 @@
     public void dump(IndentingPrintWriter pw) {
         pw.println("Interface filtering rules:");
         pw.increaseIndent();
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             pw.println("Interface: " + vpn.getKey());
             pw.println("UIDs: " + vpn.getValue().toString());
             pw.println();
@@ -1120,6 +1253,14 @@
         pw.decreaseIndent();
 
         pw.println();
+        pw.println("Lockdown filtering rules:");
+        pw.increaseIndent();
+        for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
+            pw.println("UIDs: " + range);
+        }
+        pw.decreaseIndent();
+
+        pw.println();
         pw.println("Update logs:");
         pw.increaseIndent();
         mPermissionUpdateLogs.reverseDump(pw);
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
index 71f342d..5bafef9 100644
--- a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -70,23 +70,33 @@
     /**
      * Returns a new object consisting of this object plus the passed preference.
      *
-     * If a preference already exists for the same user, it will be replaced by the passed
-     * preference. Passing a Preference object containing a null capabilities object is equivalent
-     * to (and indeed, implemented as) removing the preference for this user.
+     * It is not expected that unwanted preference already exists for the same user.
+     * All preferences for the user that were previously configured should be cleared before
+     * adding a new preference.
+     * Passing a Preference object containing a null capabilities object is equivalent
+     * to removing the preference for this user.
      */
     public ProfileNetworkPreferenceList plus(@NonNull final Preference pref) {
-        final ArrayList<Preference> newPrefs = new ArrayList<>();
-        for (final Preference existingPref : preferences) {
-            if (!existingPref.user.equals(pref.user)) {
-                newPrefs.add(existingPref);
-            }
-        }
+        final ArrayList<Preference> newPrefs = new ArrayList<>(preferences);
         if (null != pref.capabilities) {
             newPrefs.add(pref);
         }
         return new ProfileNetworkPreferenceList(newPrefs);
     }
 
+    /**
+     * Remove all preferences corresponding to a user.
+     */
+    public ProfileNetworkPreferenceList withoutUser(UserHandle user) {
+        final ArrayList<Preference> newPrefs = new ArrayList<>();
+        for (final Preference existingPref : preferences) {
+            if (!existingPref.user.equals(user)) {
+                newPrefs.add(existingPref);
+            }
+        }
+        return new ProfileNetworkPreferenceList(newPrefs);
+    }
+
     public boolean isEmpty() {
         return preferences.isEmpty();
     }
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
index 534dbe7..e682026 100644
--- a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -30,6 +30,8 @@
 import android.telephony.data.NrQosSessionAttributes;
 import android.util.Log;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Objects;
 
 /**
@@ -149,6 +151,7 @@
 
     void sendEventEpsQosSessionAvailable(final QosSession session,
             final EpsBearerQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
             mCallback.onQosEpsBearerSessionAvailable(session, attributes);
@@ -159,6 +162,7 @@
 
     void sendEventNrQosSessionAvailable(final QosSession session,
             final NrQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventNrQosSessionAvailable: sending...");
             mCallback.onNrQosSessionAvailable(session, attributes);
@@ -168,6 +172,7 @@
     }
 
     void sendEventQosSessionLost(@NonNull final QosSession session) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventQosSessionLost: sending...");
             mCallback.onQosSessionLost(session);
@@ -185,6 +190,21 @@
         }
     }
 
+    private boolean validateOrSendErrorAndUnregister() {
+        final int exceptionType = mFilter.validate();
+        if (exceptionType != EX_TYPE_FILTER_NONE) {
+             log("validation fail before sending QosCallback.");
+             // Error callback is returned from Android T to prevent any disruption of application
+             // running on Android S.
+             if (SdkLevel.isAtLeastT()) {
+                sendEventQosCallbackError(exceptionType);
+                mQosCallbackTracker.unregisterCallback(mCallback);
+            }
+            return false;
+        }
+        return true;
+    }
+
     private static void log(@NonNull final String msg) {
         Log.d(TAG, msg);
     }
diff --git a/service/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
index 35dc455..41cb419 100644
--- a/service/src/com/android/server/net/DelayedDiskWrite.java
+++ b/service/src/com/android/server/net/DelayedDiskWrite.java
@@ -21,6 +21,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.BufferedOutputStream;
 import java.io.DataOutputStream;
 import java.io.FileOutputStream;
@@ -32,11 +34,42 @@
 public class DelayedDiskWrite {
     private static final String TAG = "DelayedDiskWrite";
 
+    private final Dependencies mDeps;
+
     private HandlerThread mDiskWriteHandlerThread;
     private Handler mDiskWriteHandler;
     /* Tracks multiple writes on the same thread */
     private int mWriteSequence = 0;
 
+    public DelayedDiskWrite() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting
+    DelayedDiskWrite(Dependencies deps) {
+        mDeps = deps;
+    }
+
+    /**
+     * Dependencies class of DelayedDiskWrite, used for injection in tests.
+     */
+    @VisibleForTesting
+    static class Dependencies {
+        /**
+         * Create a HandlerThread to use in DelayedDiskWrite.
+         */
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread("DelayedDiskWriteThread");
+        }
+
+        /**
+         * Quit the HandlerThread looper.
+         */
+        public void quitHandlerThread(HandlerThread handlerThread) {
+            handlerThread.getLooper().quit();
+        }
+    }
+
     /**
      * Used to do a delayed data write to a given {@link OutputStream}.
      */
@@ -65,7 +98,7 @@
         /* Do a delayed write to disk on a separate handler thread */
         synchronized (this) {
             if (++mWriteSequence == 1) {
-                mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread");
+                mDiskWriteHandlerThread = mDeps.makeHandlerThread();
                 mDiskWriteHandlerThread.start();
                 mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
             }
@@ -99,9 +132,9 @@
             // Quit if no more writes sent
             synchronized (this) {
                 if (--mWriteSequence == 0) {
-                    mDiskWriteHandler.getLooper().quit();
-                    mDiskWriteHandler = null;
+                    mDeps.quitHandlerThread(mDiskWriteHandlerThread);
                     mDiskWriteHandlerThread = null;
+                    mDiskWriteHandler = null;
                 }
             }
         }
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 509e881..58731e0 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -140,6 +140,30 @@
     ],
 }
 
+// defaults for tests that need to build against framework-connectivity's @hide APIs, but also
+// using fully @hide classes that are jarjared (because they have no API member). Similar to
+// framework-connectivity-test-defaults above but uses pre-jarjar class names.
+// Only usable from targets that have visibility on framework-connectivity-pre-jarjar, and apply
+// connectivity jarjar rules so that references to jarjared classes still match: this is limited to
+// connectivity internal tests only.
+java_defaults {
+    name: "framework-connectivity-internal-test-defaults",
+    sdk_version: "core_platform", // tests can use @CorePlatformApi's
+    libs: [
+        // order matters: classes in framework-connectivity are resolved before framework,
+        // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+        // stubs in framework
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-tethering.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
+    defaults_visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
 // Defaults for tests that want to run in mainline-presubmit.
 // Not widely used because many of our tests have AndroidTest.xml files and
 // use the mainline-param config-descriptor metadata in AndroidTest.xml.
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index d4898b2..48d26b8 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -14,7 +14,8 @@
 -->
 <configuration description="Runs coverage tests for Connectivity">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+      <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+      <option name="install-arg" value="-t" />
     </target_preparer>
 
     <option name="test-tag" value="ConnectivityCoverageTests" />
diff --git a/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
new file mode 100644
index 0000000..84b6e54
--- /dev/null
+++ b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.net;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class EthernetNetworkManagementExceptionTest {
+    private static final String ERROR_MESSAGE = "Test error message";
+
+    @Test
+    public void testEthernetNetworkManagementExceptionParcelable() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertParcelingIsLossless(e);
+    }
+
+    @Test
+    public void testEthernetNetworkManagementExceptionHasExpectedErrorMessage() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertEquals(ERROR_MESSAGE, e.getMessage());
+    }
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 8fc636a..9ed2bb3 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,7 +20,6 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 
@@ -46,12 +45,14 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk31;
 
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
@@ -67,11 +68,13 @@
 @SmallTest
 @ConnectivityModuleTest
 public class LinkPropertiesTest {
+    // Use a RuleChain to explicitly specify the order of rules. DevSdkIgnoreRule must run before
+    // PlatformCompatChange rule, because otherwise tests with that should be skipped when targeting
+    // target SDK 33 will still attempt to override compat changes (which on user builds will crash)
+    // before being skipped.
     @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    @Rule
-    public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+    public final RuleChain chain = RuleChain.outerRule(
+            new DevSdkIgnoreRule()).around(new PlatformCompatChangeRule());
 
     private static final InetAddress ADDRV4 = address("75.208.6.1");
     private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
@@ -1261,7 +1264,21 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testHasExcludeRoute() {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 24), RTN_UNICAST));
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_UNICAST));
+        assertFalse(lp.hasExcludeRoute());
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 32), RTN_THROW));
+        assertTrue(lp.hasExcludeRoute());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
@@ -1278,7 +1295,8 @@
         assertEquals(2, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesEnabled() {
         final LinkProperties lp = new LinkProperties();
@@ -1294,7 +1312,8 @@
         assertEquals(3, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabled() {
         final LinkProperties lp = new LinkProperties();
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9ae5fab..c30e1d3 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -82,12 +82,11 @@
 import android.util.ArraySet;
 import android.util.Range;
 
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -99,8 +98,12 @@
 import java.util.List;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+// NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
+// is self-contained within NetworkCapabilities.java, so it does not need to be run on, or
+// compatible with, earlier releases.
+@IgnoreUpTo(Build.VERSION_CODES.R)
 @ConnectivityModuleTest
 public class NetworkCapabilitiesTest {
     private static final String TEST_SSID = "TEST_SSID";
@@ -489,7 +492,7 @@
         assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testOemPrivate() {
         NetworkCapabilities nc = new NetworkCapabilities();
         // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is
@@ -516,7 +519,7 @@
         assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testForbiddenCapabilities() {
         NetworkCapabilities network = new NetworkCapabilities();
 
@@ -630,7 +633,7 @@
         return new Range<Integer>(from, to);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUids() {
         NetworkCapabilities nc =
                 new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3});
@@ -638,7 +641,7 @@
         assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUidsWithDuplicates() {
         try {
             new NetworkCapabilities().setAdministratorUids(new int[] {1, 1});
@@ -750,7 +753,7 @@
                 () -> nc2.addTransportType(TRANSPORT_WIFI));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // New behavior in updatable NetworkCapabilities (S+)
+    @Test
     public void testSetNetworkSpecifierOnTestMultiTransportNc() {
         final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
         NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -859,7 +862,7 @@
         assertEquals(TRANSPORT_TEST, transportTypes[3]);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTelephonyNetworkSpecifier() {
         final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
         final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
@@ -970,7 +973,7 @@
         assertEquals(specifier, nc.getNetworkSpecifier());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testAdministratorUidsAndOwnerUid() {
         // Test default owner uid.
         // If the owner uid is not set, the default value should be Process.INVALID_UID.
@@ -1014,7 +1017,7 @@
         return nc;
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testSubIds() throws Exception {
         final NetworkCapabilities ncWithoutId = capsWithSubIds();
         final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1);
@@ -1036,7 +1039,7 @@
         assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testEqualsSubIds() throws Exception {
         assertEquals(capsWithSubIds(), capsWithSubIds());
         assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1));
@@ -1185,7 +1188,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testBuilder() {
         final int ownerUid = 1001;
         final int signalStrength = -80;
@@ -1255,7 +1258,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testBuilderWithoutDefaultCap() {
         final NetworkCapabilities nc =
                 NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
@@ -1266,12 +1269,12 @@
         assertEquals(0, nc.getCapabilities().length);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(true /* isOwner */);
     }
@@ -1316,12 +1319,12 @@
         assertEquals(expectedNcBuilder.build(), nonRestrictedNc);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(true /* isOwner */);
     }
diff --git a/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
new file mode 100644
index 0000000..368a519
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 android.net.netstats
+
+import android.net.NetworkIdentity
+import android.net.NetworkStatsCollection
+import android.net.NetworkStatsHistory
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsCollectionTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilder() {
+        val ident = setOf<NetworkIdentity>()
+        val key1 = NetworkStatsCollection.Key(ident, /* uid */ 0, /* set */ 0, /* tag */ 0)
+        val key2 = NetworkStatsCollection.Key(ident, /* uid */ 1, /* set */ 0, /* tag */ 0)
+        val bucketDuration = 10L
+        val entry1 = NetworkStatsHistory.Entry(10, 10, 40, 4, 50, 5, 60)
+        val entry2 = NetworkStatsHistory.Entry(30, 10, 3, 41, 7, 1, 0)
+        val history1 = NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry1)
+                .addEntry(entry2)
+                .build()
+        val history2 = NetworkStatsHistory(10, 5)
+        val actualCollection = NetworkStatsCollection.Builder(bucketDuration)
+                .addEntry(key1, history1)
+                .addEntry(key2, history2)
+                .build()
+
+        // The builder will omit any entry with empty history. Thus, only history1
+        // is expected in the result collection.
+        val actualEntries = actualCollection.entries
+        assertEquals(1, actualEntries.size)
+        val actualHistory = actualEntries[key1] ?: fail("There should be an entry for $key1")
+        assertEquals(history1.entries, actualHistory.entries)
+    }
+}
diff --git a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
new file mode 100644
index 0000000..a6c9f3c
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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 android.net.netstats
+
+import android.net.NetworkStatsHistory
+import android.text.format.DateUtils
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsHistoryTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilder() {
+        val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+        val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+        val entry3 = NetworkStatsHistory.Entry(7, 301, 11, 14, 31, 2, 80)
+        val statsEmpty = NetworkStatsHistory
+                .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 10).build()
+        assertEquals(0, statsEmpty.entries.size)
+        assertEquals(DateUtils.HOUR_IN_MILLIS, statsEmpty.bucketDuration)
+        val statsSingle = NetworkStatsHistory
+                .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 8)
+                .addEntry(entry1)
+                .build()
+        statsSingle.assertEntriesEqual(entry1)
+        assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
+
+        val statsMultiple = NetworkStatsHistory
+                .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+                .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+                .build()
+        assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+        // Verify the entries exist and sorted.
+        statsMultiple.assertEntriesEqual(entry3, entry1, entry2)
+    }
+
+    @Test
+    fun testBuilderSortAndDeduplicate() {
+        val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+        val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+        val entry3 = NetworkStatsHistory.Entry(30, 999, 11, 14, 31, 2, 80)
+        val entry4 = NetworkStatsHistory.Entry(10, 15, 1, 17, 5, 33, 10)
+        val entry5 = NetworkStatsHistory.Entry(6, 1, 9, 11, 29, 1, 7)
+
+        // Entries for verification.
+        // Note that active time of 2 + 3 is truncated to bucket duration since the active time
+        // should not go over bucket duration.
+        val entry2and3 = NetworkStatsHistory.Entry(30, 1000, 14, 55, 38, 3, 80)
+        val entry1and4 = NetworkStatsHistory.Entry(10, 45, 41, 21, 55, 38, 70)
+
+        val statsMultiple = NetworkStatsHistory
+                .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+                .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+                .addEntry(entry4).addEntry(entry5).build()
+        assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+        // Verify the entries sorted and deduplicated.
+        statsMultiple.assertEntriesEqual(entry5, entry1and4, entry2and3)
+    }
+
+    fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
+        assertEquals(entries.size, this.entries.size)
+        entries.forEachIndexed { i, element ->
+            assertEquals(element, this.entries[i])
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
new file mode 100644
index 0000000..192694b
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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 android.net.netstats
+
+import android.net.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_BLUETOOTH
+import android.net.NetworkTemplate.MATCH_CARRIER
+import android.net.NetworkTemplate.MATCH_ETHERNET
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_PROXY
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.telephony.TelephonyManager
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+private const val TEST_IMSI1 = "imsi"
+private const val TEST_WIFI_KEY1 = "wifiKey1"
+private const val TEST_WIFI_KEY2 = "wifiKey2"
+
+@RunWith(JUnit4::class)
+@ConnectivityModuleTest
+class NetworkTemplateTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilderMatchRules() {
+        // Verify unknown match rules cannot construct templates.
+        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify hidden match rules cannot construct templates.
+        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify template which matches metered cellular and carrier networks with
+        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify template which matches roaming cellular and carrier networks with
+        // the given IMSI.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setRoaming(ROAMING_YES).setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_YES, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify carrier template cannot be created without IMSI.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_CARRIER).build()
+        }
+
+        // Verify template which matches metered cellular networks,
+        // regardless of IMSI. See buildTemplateMobileWildcard.
+        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches metered cellular networks and ratType.
+        // See NetworkTemplate#buildTemplateMobileWithRatType.
+        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
+                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+                .build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches all wifi networks,
+        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
+        NetworkTemplate.Builder(MATCH_WIFI).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches wifi networks with the given Wifi Network Key.
+        // See buildTemplateWifi(wifiNetworkKey).
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches all wifi networks with the
+        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
+        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
+                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
+                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches ethernet and bluetooth networks.
+        // See buildTemplateEthernet and buildTemplateBluetooth.
+        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).build().let {
+                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
+                        null /*subscriberIds*/, arrayOf<String>(),
+                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+                assertEquals(expectedTemplate, it)
+            }
+        }
+    }
+
+    @Test
+    fun testBuilderWifiNetworkKeys() {
+        // Verify template builder which generates same template with the given different
+        // sequence keys.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
+            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches non-wifi networks with the given key is invalid.
+        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
+                Integer.MAX_VALUE).forEach { matchRule ->
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+            }
+        }
+
+        // Verify template which matches wifi networks with the given null key is invalid.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
+        }
+
+        // Verify template which matches wifi wildcard with the given empty key set.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+    }
+}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index b684068..ac84e57 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -26,6 +26,7 @@
         "tradefed",
     ],
     static_libs: [
+        "CompatChangeGatingTestBase",
         "modules-utils-build-testing",
     ],
     // Tag this module as a cts test artifact
@@ -34,4 +35,12 @@
         "general-tests",
         "sts"
     ],
+    data: [
+        ":CtsHostsideNetworkTestsApp",
+        ":CtsHostsideNetworkTestsApp2",
+        ":CtsHostsideNetworkTestsApp3",
+        ":CtsHostsideNetworkTestsApp3PreT",
+        ":CtsHostsideNetworkTestsAppNext",
+    ],
+    per_testcase_directory: true,
 }
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index fcec483..ab6de82 100644
--- a/tests/cts/hostside/TEST_MAPPING
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -4,9 +4,6 @@
       "name": "CtsHostsideNetworkTests",
       "options": [
         {
-          "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests"
-        },
-        {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         },
         {
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index 28437c2..e7b2815 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -28,5 +28,5 @@
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
     void unregisterNetworkCallback();
-    void scheduleJob(in JobInfo jobInfo);
+    int scheduleJob(in JobInfo jobInfo);
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index f460180..93e9dcd 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -16,6 +16,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
 import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
@@ -151,7 +152,7 @@
 
     protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
 
-    private static final long BROADCAST_TIMEOUT_MS = 15_000;
+    private static final long BROADCAST_TIMEOUT_MS = 5_000;
 
     protected Context mContext;
     protected Instrumentation mInstrumentation;
@@ -216,7 +217,10 @@
             Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
                     + attempts + " attempts; sleeping "
                     + SLEEP_TIME_SEC + " seconds before trying again");
-            SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (attempts <= maxAttempts) {
+                SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            }
         } while (attempts <= maxAttempts);
         assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
                 + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
@@ -327,7 +331,10 @@
             }
             Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on background state after "
                 + maxTries + " attempts: " + state);
@@ -346,7 +353,10 @@
             Log.d(TAG, "App not on foreground state on attempt #" + i
                     + "; sleeping 1s before trying again");
             turnScreenOn();
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground state after "
                 + maxTries + " attempts: " + state);
@@ -364,7 +374,10 @@
             }
             Log.d(TAG, "App not on foreground service state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground service state after "
                 + maxTries + " attempts: " + state);
@@ -505,7 +518,10 @@
             Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
                     + checker.getExpected() + "' on attempt #" + i
                     + "; sleeping " + napTimeSeconds + "s before trying again");
-            SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            }
         }
         fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
                 + maxTries
@@ -577,7 +593,10 @@
             }
             Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
                     + expected + ", got " + actual + "); sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
                 + ". Full list: " + uids);
@@ -737,7 +756,8 @@
 
     protected void assertAppIdle(boolean enabled) throws Exception {
         try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
+                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
         } catch (Throwable e) {
             throw e;
         }
@@ -764,7 +784,10 @@
                 return;
             }
             Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("app2 receiver is not ready in " + mUid);
     }
@@ -813,8 +836,6 @@
             return;
         } else if (type == TYPE_COMPONENT_ACTIVTIY) {
             turnScreenOn();
-            // Wait for screen-on state to propagate through the system.
-            SystemClock.sleep(2000);
             final CountDownLatch latch = new CountDownLatch(1);
             final Intent launchIntent = getIntentForComponent(type);
             final Bundle extras = new Bundle();
@@ -855,7 +876,8 @@
                     .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                     .setTransientExtras(extras)
                     .build();
-            mServiceClient.scheduleJob(jobInfo);
+            assertEquals("Error scheduling " + jobInfo,
+                    RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
             forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
             if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                 final int resultCode = result.get(0).first;
@@ -896,7 +918,7 @@
         final Intent intent = new Intent();
         if (type == TYPE_COMPONENT_ACTIVTIY) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
         } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
                     .setFlags(1);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
new file mode 100644
index 0000000..10775d0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.cts.net.hostside;
+
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
+    private static final int TEST_ITERATION_COUNT = 5;
+
+    @Before
+    public final void setUp() throws Exception {
+        super.setUp();
+        resetDeviceState();
+    }
+
+    @After
+    public final void tearDown() throws Exception {
+        super.tearDown();
+        resetDeviceState();
+    }
+
+    private void resetDeviceState() throws Exception {
+        resetBatteryState();
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+        setAppIdle(false);
+        setDozeMode(false);
+    }
+
+
+    @Test
+    @RequiredProperties({BATTERY_SAVER_MODE})
+    public void testStartActivity_batterySaver() throws Exception {
+        setBatterySaverMode(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+    }
+
+    @Test
+    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+    public void testStartActivity_dataSaver() throws Exception {
+        setRestrictBackground(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+    }
+
+    @Test
+    @RequiredProperties({DOZE_MODE})
+    public void testStartActivity_doze() throws Exception {
+        setDozeMode(true);
+        // TODO (235284115): We need to turn on Doze every time before starting
+        // the activity.
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+    }
+
+    @Test
+    @RequiredProperties({APP_STANDBY_MODE})
+    public void testStartActivity_appStandby() throws Exception {
+        turnBatteryOn();
+        setAppIdle(true);
+        // TODO (235284115): We need to put the app into app standby mode every
+        // time before starting the activity.
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+    }
+
+    private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+            Log.i(TAG, testName + " start #" + i);
+            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+            getUiDevice().pressHome();
+            assertBackgroundState();
+            Log.i(TAG, testName + " end #" + i);
+        }
+    }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 8b70f9b..0610774 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -107,7 +107,7 @@
         mService.unregisterNetworkCallback();
     }
 
-    public void scheduleJob(JobInfo jobInfo) throws RemoteException {
-        mService.scheduleJob(jobInfo);
+    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+        return mService.scheduleJob(jobInfo);
     }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index b6218d2..c53276b 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -25,7 +25,7 @@
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
 
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
 
 import static org.junit.Assert.assertEquals;
@@ -57,6 +57,7 @@
 import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
 
 import com.android.compatibility.common.util.AppStandbyUtils;
 import com.android.compatibility.common.util.BatteryUtils;
@@ -390,7 +391,7 @@
     }
 
     public static String executeShellCommand(String command) {
-        final String result = runShellCommand(command).trim();
+        final String result = runShellCommandOrThrow(command).trim();
         Log.d(TAG, "Output of '" + command + "': '" + result + "'");
         return result;
     }
@@ -438,6 +439,10 @@
         return InstrumentationRegistry.getInstrumentation();
     }
 
+    public static UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
     // When power saver mode or restrict background enabled or adding any white/black list into
     // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
     // this function and using PollingCheck to try to make sure the uid has updated and reduce the
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index dc67c70..dd8b523 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -378,10 +378,6 @@
         if (mNetwork == null) {
             fail("VPN did not become available after " + TIMEOUT_MS + "ms");
         }
-
-        // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
-        // configured. Give the system some time to do so. http://b/18436087 .
-        try { Thread.sleep(3000); } catch(InterruptedException e) {}
     }
 
     private void stopVpn() {
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 01c8cd2..edfaf9f 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -23,6 +23,7 @@
     defaults: ["cts_support_defaults"],
     sdk_version: "test_current",
     static_libs: [
+        "androidx.annotation_annotation",
         "CtsHostsideNetworkTestsAidl",
         "NetworkStackApiStableShims",
     ],
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 9fdb9c9..82f13ae 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -29,6 +29,9 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.annotation.GuardedBy;
 
 import com.android.cts.net.hostside.INetworkStateObserver;
 
@@ -37,27 +40,24 @@
  */
 public class MyActivity extends Activity {
 
+    @GuardedBy("this")
     private BroadcastReceiver finishCommandReceiver = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "MyActivity.onCreate()");
-        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
-        finishCommandReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.d(TAG, "Finishing MyActivity");
-                MyActivity.this.finish();
-            }
-        };
-        registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
     }
 
     @Override
     public void finish() {
-        if (finishCommandReceiver != null) {
-            unregisterReceiver(finishCommandReceiver);
+        synchronized (this) {
+            if (finishCommandReceiver != null) {
+                unregisterReceiver(finishCommandReceiver);
+                finishCommandReceiver = null;
+            }
         }
         super.finish();
     }
@@ -69,6 +69,31 @@
     }
 
     @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        Log.d(TAG, "MyActivity.onNewIntent()");
+        setIntent(intent);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Log.d(TAG, "MyActivity.onResume(): " + getIntent());
+        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
+        synchronized (this) {
+            finishCommandReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Log.d(TAG, "Finishing MyActivity");
+                    MyActivity.this.finish();
+                }
+            };
+            registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
+                    Context.RECEIVER_EXPORTED);
+        }
+    }
+
+    @Override
     protected void onDestroy() {
         Log.d(TAG, "MyActivity.onDestroy()");
         super.onDestroy();
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index f2a7b3f..3ed5391 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -165,10 +165,10 @@
         }
 
         @Override
-        public void scheduleJob(JobInfo jobInfo) {
+        public int scheduleJob(JobInfo jobInfo) {
             final JobScheduler jobScheduler = getApplicationContext()
                     .getSystemService(JobScheduler.class);
-            jobScheduler.schedule(jobInfo);
+            return jobScheduler.schedule(jobInfo);
         }
       };
 
diff --git a/tests/cts/hostside/app3/Android.bp b/tests/cts/hostside/app3/Android.bp
new file mode 100644
index 0000000..141cf03
--- /dev/null
+++ b/tests/cts/hostside/app3/Android.bp
@@ -0,0 +1,54 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsHostsideNetworkTestsApp3Defaults",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "junit",
+    ],
+    static_libs: [
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3PreT",
+    target_sdk_version: "31",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
diff --git a/tests/cts/hostside/app3/AndroidManifest.xml b/tests/cts/hostside/app3/AndroidManifest.xml
new file mode 100644
index 0000000..eabcacb
--- /dev/null
+++ b/tests/cts/hostside/app3/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.net.hostside.app3">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.net.hostside.app3" />
+
+</manifest>
diff --git a/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
new file mode 100644
index 0000000..a1a8209
--- /dev/null
+++ b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.cts.net.hostside.app3;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify {@link LinkProperties#getRoutes} behavior, depending on
+ * {@LinkProperties#EXCLUDED_ROUTES} change state.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExcludedRoutesGatingTest {
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testExcludedRoutesChangeEnabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is enabled: non-RTN_UNICAST routes are visible.
+        assertEquals(2, lp.getRoutes().size());
+        assertEquals(2, lp.getAllRoutes().size());
+    }
+
+    @Test
+    public void testExcludedRoutesChangeDisabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is disabled: non-RTN_UNICAST routes are filtered out.
+        assertEquals(0, lp.getRoutes().size());
+        assertEquals(0, lp.getAllRoutes().size());
+    }
+
+    private LinkProperties makeLinkPropertiesWithExcludedRoutes() {
+        final LinkProperties lp = new LinkProperties();
+
+        lp.addRoute(new RouteInfo(new IpPrefix("10.0.0.0/8"), null, null, RouteInfo.RTN_THROW));
+        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null,
+                RouteInfo.RTN_UNREACHABLE));
+
+        return lp;
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
new file mode 100644
index 0000000..cfd3130
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.cts.net;
+
+import android.platform.test.annotations.FlakyTest;
+
+public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
+    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        uninstallPackage(TEST_APP2_PKG, false);
+        installPackage(TEST_APP2_APK);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        uninstallPackage(TEST_APP2_PKG, true);
+    }
+
+    public void testStartActivity_batterySaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+    }
+
+    public void testStartActivity_dataSaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+    }
+
+    @FlakyTest(bugId = 231440256)
+    public void testStartActivity_doze() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+    }
+
+    public void testStartActivity_appStandby() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
new file mode 100644
index 0000000..9a1fa42
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -0,0 +1,85 @@
+/*
+ * 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.cts.net;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import java.util.Set;
+
+/**
+ * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ *
+ * TODO: see if we can delete this cumbersome host test by moving the coverage to CtsNetTestCases
+ * and CtsNetTestCasesMaxTargetSdk31.
+ */
+public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
+    private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
+    private static final String TEST_APK_PRE_T = "CtsHostsideNetworkTestsApp3PreT.apk";
+    private static final String TEST_PKG = "com.android.cts.net.hostside.app3";
+    private static final String TEST_CLASS = ".ExcludedRoutesGatingTest";
+
+    private static final long EXCLUDED_ROUTES_CHANGE_ID = 186082280;
+
+    protected void tearDown() throws Exception {
+        uninstallPackage(TEST_PKG, true);
+    }
+
+    public void testExcludedRoutesChangeEnabled() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTest("testExcludedRoutesChangeEnabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledPreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTest("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledByOverrideOnDebugBuild() throws Exception {
+        // Must install APK even when skipping test, because tearDown expects uninstall to succeed.
+        installPackage(TEST_APK, true);
+
+        // This test uses an app with a target SDK where the compat change is on by default.
+        // Because user builds do not allow overriding compat changes, only run this test on debug
+        // builds. This seems better than deleting this test and not running it anywhere because we
+        // could in the future run this test on userdebug builds in presubmit.
+        //
+        // We cannot use assumeXyz here because CompatChangeGatingTestCase ultimately inherits from
+        // junit.framework.TestCase, which does not understand assumption failures.
+        if ("user".equals(getDevice().getProperty("ro.build.type"))) return;
+
+        runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeEnabledByOverridePreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTestWithChangeEnabled("testExcludedRoutesChangeEnabled");
+    }
+
+    private void runDeviceCompatTest(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(), Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeEnabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(EXCLUDED_ROUTES_CHANGE_ID),
+                Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeDisabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(),
+                Set.of(EXCLUDED_ROUTES_CHANGE_ID));
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index cc07fd1..d0567ae 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -34,8 +34,6 @@
 
 import java.io.FileNotFoundException;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
         IBuildReceiver {
@@ -171,18 +169,19 @@
         }
     }
 
-    private static final Pattern UID_PATTERN =
-            Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
-
     protected int getUid(String packageName) throws DeviceNotAvailableException {
-        final String output = runCommand("dumpsys package " + packageName);
-        final Matcher matcher = UID_PATTERN.matcher(output);
-        while (matcher.find()) {
-            final String match = matcher.group(1);
-            return Integer.parseInt(match);
+        final int currentUser = getDevice().getCurrentUser();
+        final String uidLines = runCommand(
+                "cmd package list packages -U --user " + currentUser + " " + packageName);
+        for (String uidLine : uidLines.split("\n")) {
+            if (uidLine.startsWith("package:" + packageName + " uid:")) {
+                final String[] uidLineParts = uidLine.split(":");
+                // 3rd entry is package uid
+                return Integer.parseInt(uidLineParts[2].trim());
+            }
         }
-        throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
-                + output);
+        throw new IllegalStateException("Failed to find the test app on the device; pkg="
+                + packageName + ", u=" + currentUser);
     }
 
     protected String runCommand(String command) throws DeviceNotAvailableException {
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index e979a3b..a6ed762 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -62,6 +62,7 @@
     // sdk_version: "current",
     platform_apis: true,
     required: ["ConnectivityChecker"],
+    test_config_template: "AndroidTestTemplate.xml",
 }
 
 // Networking CTS tests for development and release. These tests always target the platform SDK
@@ -79,7 +80,16 @@
         "cts",
         "general-tests",
     ],
-    test_config_template: "AndroidTestTemplate.xml",
+}
+
+java_defaults {
+    name: "CtsNetTestCasesApiStableDefaults",
+    // TODO: CTS should not depend on the entirety of the networkstack code.
+    static_libs: [
+        "NetworkStackApiStableLib",
+    ],
+    jni_uses_sdk_apis: true,
+    min_sdk_version: "29",
 }
 
 // Networking CTS tests that target the latest released SDK. These tests can be installed on release
@@ -87,14 +97,11 @@
 // on release devices.
 android_test {
     name: "CtsNetTestCasesLatestSdk",
-    defaults: ["CtsNetTestCasesDefaults"],
-    // TODO: CTS should not depend on the entirety of the networkstack code.
-    static_libs: [
-        "NetworkStackApiStableLib",
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "CtsNetTestCasesApiStableDefaults",
     ],
-    jni_uses_sdk_apis: true,
-    min_sdk_version: "29",
-    target_sdk_version: "30",
+    target_sdk_version: "33",
     test_suites: [
         "general-tests",
         "mts-dnsresolver",
@@ -102,5 +109,21 @@
         "mts-tethering",
         "mts-wifi",
     ],
-    test_config_template: "AndroidTestTemplate.xml",
 }
+
+android_test {
+    name: "CtsNetTestCasesMaxTargetSdk31",  // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "CtsNetTestCasesApiStableDefaults",
+    ],
+    target_sdk_version: "31",
+    package_name: "android.net.cts.maxtargetsdk31",  // CTS package names must be unique.
+    instrumentation_target_package: "android.net.cts.maxtargetsdk31",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts-networking",
+    ],
+}
+
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 3b47100..6b5bb93 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -44,7 +44,8 @@
              android.permission.MANAGE_TEST_NETWORKS
     -->
 
-    <application android:usesCleartextTraffic="true">
+    <application android:debuggable="true"
+                 android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
     </application>
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 48a1c79..d2fb04a 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -33,9 +33,36 @@
     <target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.net.cts" />
+        <option name="package" value="{PACKAGE}" />
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
+        <!-- Test filter that allows test APKs to select which tests they want to run by annotating
+             those tests with an annotation matching the name of the APK.
+
+             This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
+             and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
+             CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+
+             This relies on the fact that if the class specified in include-annotation exists, then
+             the runner will only run the tests annotated with that annotation, but if it does not,
+             the runner will run all the tests. -->
+        <option name="include-annotation" value="com.android.testutils.filters.{MODULE}" />
     </test>
+    <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+        one of the Mainline modules below is present on the device used for testing. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <!-- Tethering Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+        <!-- Tethering Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.tethering" />
+        <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+        <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.android.networkstack" />
+        <!-- Resolver Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.resolv" />
+        <!-- Resolver Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.resolv" />
+    </object>
 </configuration>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 5b37294..9b81a56 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -51,5 +51,8 @@
         "cts",
         "general-tests",
     ],
-
+    data: [
+        ":CtsNetTestAppForApi23",
+    ],
+    per_testcase_directory: true,
 }
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 97ecb9e..e52533b 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,8 +31,13 @@
   std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
   ASSERT_TRUE(elfFile.is_open());
 
-  EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-  EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  if (android::modules::sdklevel::IsAtLeastT()) {
+    EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  } else {
+    EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  }
 }
 
 TEST(BpfTest, bpfStructSizeTestPreT) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 0344604..1b77d5f 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -33,7 +33,6 @@
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
 import android.net.Uri
-import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
@@ -60,6 +59,8 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
 import org.junit.runner.RunWith
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
@@ -99,34 +100,42 @@
 
     private val server = TestHttpServer("localhost")
 
+    @get:Rule
+    val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+
+    companion object {
+        @JvmStatic @BeforeClass
+        fun setUpClass() {
+            runAsShell(READ_DEVICE_CONFIG) {
+                // Verify that the test URLs are not normally set on the device, but do not fail if
+                // the test URLs are set to what this test uses (URLs on localhost), in case the
+                // test was interrupted manually and rerun.
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+            }
+            NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
+        }
+
+        private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+            val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+            assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+                    "$urlKey must not be set in production scenarios (current value: $url)")
+        }
+    }
+
     @Before
     fun setUp() {
-        runAsShell(READ_DEVICE_CONFIG) {
-            // Verify that the test URLs are not normally set on the device, but do not fail if the
-            // test URLs are set to what this test uses (URLs on localhost), in case the test was
-            // interrupted manually and rerun.
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
-        }
-        clearValidationTestUrlsDeviceConfig()
         server.start()
     }
 
     @After
     fun tearDown() {
-        clearValidationTestUrlsDeviceConfig()
         if (pm.hasSystemFeature(FEATURE_WIFI)) {
-            reconnectWifi()
+            deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
         }
         server.stop()
     }
 
-    private fun assertEmptyOrLocalhostUrl(urlKey: String) {
-        val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
-        assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
-                "$urlKey must not be set in production scenarios (current value: $url)")
-    }
-
     @Test
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -154,12 +163,13 @@
         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
         val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
         server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
-        setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
-        setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+        setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
+        setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
         Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
         // URL expiration needs to be in the next 10 minutes
         assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
-        setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+        setUrlExpirationDeviceConfig(deviceConfigRule,
+                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
 
         // Wait for a captive portal to be detected on the network
         val wifiNetworkFuture = CompletableFuture<Network>()
@@ -215,4 +225,4 @@
         utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
         utils.ensureWifiConnected()
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 68fa38d..7d1e13f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -113,7 +113,7 @@
     private static final int UNKNOWN_DETECTION_METHOD = 4;
     private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
     private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
-    private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+    private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 5000;
 
     private static final Executor INLINE_EXECUTOR = x -> x.run();
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 9055861..766d62f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -37,6 +37,11 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.EXTRA_NETWORK;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -52,6 +57,7 @@
 import static android.net.ConnectivityManager.TYPE_PROXY;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -151,6 +157,7 @@
 import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.Process;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -163,7 +170,6 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.InstrumentationRegistry;
@@ -181,6 +187,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.TestHttpServer;
@@ -202,6 +209,8 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
 import java.net.HttpURLConnection;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -216,6 +225,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -239,6 +249,10 @@
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
+    @Rule
+    public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
+            5 /* retryCountBeforeSIfConfigChanged */);
+
     private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
 
     public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
@@ -253,6 +267,7 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
     private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 100;
+    private static final int SOCKET_TIMEOUT_MS = 100;
     private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
     private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
     // device could have only one interface: data, wifi.
@@ -661,10 +676,12 @@
 
             // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
             // NETWORK_SETTINGS permission.
-            assertEquals(capportUrl,
+            assertNotNull(lp.getCaptivePortalApiUrl());
+            assertNotNull(lp.getCaptivePortalData());
+            assertEquals(lp.getCaptivePortalApiUrl(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalApiUrl());
-            assertEquals(capportData,
+            assertEquals(lp.getCaptivePortalData(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalData());
         });
@@ -887,9 +904,21 @@
         //
         // Note that this test this will still fail in instant mode if a device supports Ethernet
         // via other hardware means. We are not currently aware of any such device.
-        return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
-            mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
-            mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+        return hasEthernetService()
+                || mPackageManager.hasSystemFeature(FEATURE_ETHERNET)
+                || mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+    }
+
+    private boolean hasEthernetService() {
+        // On Q creating EthernetManager from a thread that does not have a looper (like the test
+        // thread) crashes because it tried to use Looper.myLooper() through the default Handler
+        // constructor to run onAvailabilityChanged callbacks. Use ServiceManager to check whether
+        // the service exists instead.
+        // TODO: remove once Q is no longer supported in MTS, as ServiceManager is hidden API
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            return ServiceManager.getService(Context.ETHERNET_SERVICE) != null;
+        }
+        return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
     }
 
     private boolean shouldBeSupported(int networkType) {
@@ -1590,51 +1619,7 @@
 
     private static boolean isTcpKeepaliveSupportedByKernel() {
         final String kVersionString = VintfRuntimeInfo.getKernelVersion();
-        return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
-    }
-
-    private static Pair<Integer, Integer> getVersionFromString(String version) {
-        // Only gets major and minor number of the version string.
-        final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
-        final Matcher m = versionPattern.matcher(version);
-        if (m.matches()) {
-            final int major = Integer.parseInt(m.group(1));
-            final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
-            return new Pair<>(major, minor);
-        } else {
-            return new Pair<>(0, 0);
-        }
-    }
-
-    // TODO: Move to util class.
-    private static int compareMajorMinorVersion(final String s1, final String s2) {
-        final Pair<Integer, Integer> v1 = getVersionFromString(s1);
-        final Pair<Integer, Integer> v2 = getVersionFromString(s2);
-
-        if (v1.first == v2.first) {
-            return Integer.compare(v1.second, v2.second);
-        } else {
-            return Integer.compare(v1.first, v2.first);
-        }
-    }
-
-    /**
-     * Verifies that version string compare logic returns expected result for various cases.
-     * Note that only major and minor number are compared.
-     */
-    @Test
-    public void testMajorMinorVersionCompare() {
-        assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
-        assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("5", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("5", "5.0"));
-        assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
-        assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
-        assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.8") >= 0;
     }
 
     /**
@@ -2784,9 +2769,8 @@
             // Accept partial connectivity network should result in a validated network
             expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
         } finally {
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2811,11 +2795,13 @@
             // Reject partial connectivity network should cause the network being torn down
             assertEquals(network, cb.waitForLost());
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2851,11 +2837,13 @@
             });
             waitForLost(wifiCb);
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             /// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2915,9 +2903,8 @@
             wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
         } finally {
             resetAvoidBadWifi(previousAvoidBadWifi);
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2961,11 +2948,6 @@
         return future.get(timeout, TimeUnit.MILLISECONDS);
     }
 
-    private void resetValidationConfig() {
-        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
-        mHttpServer.stop();
-    }
-
     private void prepareHttpServer() throws Exception {
         runAsShell(READ_DEVICE_CONFIG, () -> {
             // Verify that the test URLs are not normally set on the device, but do not fail if the
@@ -3038,9 +3020,11 @@
         mHttpServer.addResponse(new TestHttpServer.Request(
                 TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
                 httpStatusCode, null /* locationHeader */, "" /* content */);
-        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
-        NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
-        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTPS_URL_PATH));
+        NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTP_URL_PATH));
+        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
                 System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
     }
 
@@ -3197,7 +3181,7 @@
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test
     public void testUidsAllowedOnRestrictedNetworks() throws Exception {
-        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTestSApis();
 
         // TODO (b/175199465): figure out a reasonable permission check for
         //  setUidsAllowedOnRestrictedNetworks that allows tests but not system-external callers.
@@ -3210,10 +3194,10 @@
         // because it has been just installed to device. In case the uid is existed in setting
         // mistakenly, try to remove the uid and set correct uids to setting.
         originalUidsAllowedOnRestrictedNetworks.remove(uid);
-        runWithShellPermissionIdentity(() ->
-                ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                        mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+        runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
 
+        // File a restricted network request with permission first to hold the connection.
         final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
@@ -3225,6 +3209,19 @@
         runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
 
+        // File another restricted network request without permission.
+        final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
+        final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+                        TEST_RESTRICTED_NW_IFACE_NAME))
+                .build();
+        // Uid is not in allowed list and no permissions. Expect that SecurityException will throw.
+        assertThrows(SecurityException.class,
+                () -> mCm.requestNetwork(restrictedRequest, restrictedNetworkCb));
+
         final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
         final Network network = agent.getNetwork();
 
@@ -3244,19 +3241,28 @@
             final Set<Integer> newUidsAllowedOnRestrictedNetworks =
                     new ArraySet<>(originalUidsAllowedOnRestrictedNetworks);
             newUidsAllowedOnRestrictedNetworks.add(uid);
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
             // Wait a while for sending allowed uids on the restricted network to netd.
-            // TODD: Have a significant signal to know the uids has been send to netd.
+            // TODD: Have a significant signal to know the uids has been sent to netd.
             assertBindSocketToNetworkSuccess(network);
+
+            if (TestUtils.shouldTestTApis()) {
+                // Uid is in allowed list. Try file network request again.
+                requestNetwork(restrictedRequest, restrictedNetworkCb);
+                // Verify that the network is restricted.
+                restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+                        NETWORK_CALLBACK_TIMEOUT_MS,
+                        entry -> network.equals(entry.getNetwork())
+                                && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+            }
         } finally {
             agent.unregister();
 
             // Restore setting.
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
         }
     }
 
@@ -3280,6 +3286,119 @@
         assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
     }
 
+    private void checkFirewallBlocking(final DatagramSocket srcSock, final DatagramSocket dstSock,
+            final boolean expectBlock) throws Exception {
+        final Random random = new Random();
+        final byte[] sendData = new byte[100];
+        random.nextBytes(sendData);
+
+        final DatagramPacket pkt = new DatagramPacket(sendData, sendData.length,
+                InetAddresses.parseNumericAddress("::1"), dstSock.getLocalPort());
+        try {
+            srcSock.send(pkt);
+        } catch (IOException e) {
+            if (expectBlock) {
+                return;
+            }
+            fail("Expect not to be blocked by firewall but sending packet was blocked");
+        }
+
+        if (expectBlock) {
+            fail("Expect to be blocked by firewall but sending packet was not blocked");
+        }
+
+        dstSock.receive(pkt);
+        assertArrayEquals(sendData, pkt.getData());
+    }
+
+    private static final boolean EXPECT_PASS = false;
+    private static final boolean EXPECT_BLOCK = true;
+
+    private void doTestFirewallBlockingDenyRule(final int chain) {
+        runWithShellPermissionIdentity(() -> {
+            try (DatagramSocket srcSock = new DatagramSocket();
+                 DatagramSocket dstSock = new DatagramSocket()) {
+                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+                // No global config, No uid config
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, No uid config
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, Has uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+                // No global config, Has uid config
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, No uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+            } finally {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+            }
+        }, NETWORK_SETTINGS);
+    }
+
+    private void doTestFirewallBlockingAllowRule(final int chain) {
+        runWithShellPermissionIdentity(() -> {
+            try (DatagramSocket srcSock = new DatagramSocket();
+                 DatagramSocket dstSock = new DatagramSocket()) {
+                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+                // No global config, No uid config
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // Has global config, No uid config
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+                // Has global config, Has uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, Has uid config
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+                // No global config, No uid config
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+            } finally {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+            }
+        }, NETWORK_SETTINGS);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Socket cannot bind in instant app mode")
+    public void testFirewallBlocking() {
+        // Following tests affect the actual state of networking on the device after the test.
+        // This might cause unexpected behaviour of the device. So, we skip them for now.
+        // We will enable following tests after adding the logic of firewall state restoring.
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_DOZABLE);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_POWERSAVE);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_RESTRICTED);
+        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+
+        // doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_STANDBY);
+        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    private void assumeTestSApis() {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
new file mode 100644
index 0000000..d31a4e0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -0,0 +1,124 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ *        the configs that were set through this rule were changed, and retry the test
+ *        up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+    val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+    // Maps (namespace, key) -> value
+    private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+    private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+    /**
+     * Actions to be run after cleanup of the config, for the current test only.
+     */
+    private val currentTestCleanupActions = mutableListOf<Runnable>()
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TestValidationUrlStatement(base, description)
+    }
+
+    private inner class TestValidationUrlStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+            while (retryCount > 0) {
+                retryCount--
+                tryTest {
+                    base.evaluate()
+                    // Can't use break/return out of a loop here because this is a tryTest lambda,
+                    // so set retryCount to exit instead
+                    retryCount = 0
+                }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+                    if (retryCount == 0) throw e
+                    usedConfig.forEach { (key, value) ->
+                        val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+                            DeviceConfig.getProperty(key.first, key.second)
+                        }
+                        if (currentValue != value) {
+                            Log.w(TAG, "Test failed with unexpected device config change, retrying")
+                            return@catch
+                        }
+                    }
+                    throw e
+                } cleanupStep {
+                    runAsShell(WRITE_DEVICE_CONFIG) {
+                        originalConfig.forEach { (key, value) ->
+                            DeviceConfig.setProperty(
+                                    key.first, key.second, value, false /* makeDefault */)
+                        }
+                    }
+                } cleanupStep {
+                    originalConfig.clear()
+                    usedConfig.clear()
+                } cleanup {
+                    currentTestCleanupActions.forEach { it.run() }
+                    currentTestCleanupActions.clear()
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a configuration key/value. After the test case ends, it will be restored to the value it
+     * had when this method was first called.
+     */
+    fun setConfig(namespace: String, key: String, value: String?) {
+        runAsShell(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) {
+            val keyPair = Pair(namespace, key)
+            if (!originalConfig.containsKey(keyPair)) {
+                originalConfig[keyPair] = DeviceConfig.getProperty(namespace, key)
+            }
+            usedConfig[keyPair] = value
+            DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+        }
+    }
+
+    /**
+     * Add an action to be run after config cleanup when the current test case ends.
+     */
+    fun runAfterNextCleanup(action: Runnable) {
+        currentTestCleanupActions.add(action)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index c6fc38f..0c53411 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -49,6 +49,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -727,6 +728,18 @@
 
     @Test
     public void testPrivateDnsBypass() throws InterruptedException {
+        final String dataStallSetting = Settings.Global.getString(mCR,
+                Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK);
+        Settings.Global.putInt(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK, 0);
+        try {
+            doTestPrivateDnsBypass();
+        } finally {
+            Settings.Global.putString(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK,
+                    dataStallSetting);
+        }
+    }
+
+    private void doTestPrivateDnsBypass() throws InterruptedException {
         final Network[] testNetworks = getTestableNetworks();
 
         // Set an invalid private DNS server
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index e8add6b..621b743 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -48,9 +48,11 @@
 import android.platform.test.annotations.AppModeFull
 import android.system.Os
 import android.system.OsConstants.AF_INET
+import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
 import android.util.Range
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
@@ -71,6 +73,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.util.regex.Pattern
@@ -81,6 +85,9 @@
 
 private const val MAX_PACKET_LENGTH = 1500
 
+private const val IP4_PREFIX_LEN = 32
+private const val IP6_PREFIX_LEN = 128
+
 private val instrumentation: Instrumentation
     get() = InstrumentationRegistry.getInstrumentation()
 
@@ -97,6 +104,9 @@
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val TEST_TARGET_IPV4_ADDR =
             InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
+    private val LOCAL_IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::1")
+    private val TEST_TARGET_IPV6_ADDR =
+            InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
 
     private val realContext = InstrumentationRegistry.getContext()
     private val cm = realContext.getSystemService(ConnectivityManager::class.java)
@@ -126,13 +136,15 @@
 
     @Before
     fun setUp() {
-        // For BPF support kernel needs to be at least 5.4.
-        assumeTrue(kernelIsAtLeast(5, 4))
+        // For BPF support kernel needs to be at least 5.15.
+        assumeTrue(kernelIsAtLeast(5, 15))
 
         runAsShell(MANAGE_TEST_NETWORKS) {
             val tnm = realContext.getSystemService(TestNetworkManager::class.java)
 
-            iface = tnm.createTunInterface(Array(1) { LinkAddress(LOCAL_IPV4_ADDRESS, 32) })
+            iface = tnm.createTunInterface(arrayOf(
+                    LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN),
+                    LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN)))
             assertNotNull(iface)
         }
 
@@ -146,7 +158,7 @@
 
     @After
     fun tearDown() {
-        if (!kernelIsAtLeast(5, 4)) {
+        if (!kernelIsAtLeast(5, 15)) {
             return;
         }
         agentsToCleanUp.forEach { it.unregister() }
@@ -154,6 +166,8 @@
 
         // reader.stop() cleans up tun fd
         reader.handler.post { reader.stop() }
+        if (iface.fileDescriptor.fileDescriptor != null)
+            Os.close(iface.fileDescriptor.fileDescriptor)
         handlerThread.quitSafely()
     }
 
@@ -196,9 +210,11 @@
             }
         }
         val lp = LinkProperties().apply {
-            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+            addLinkAddress(LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
-            setInterfaceName(iface.getInterfaceName())
+            addRoute(RouteInfo(InetAddress.getByName("fe80::1234")))
+            setInterfaceName(specifier)
         }
         val config = NetworkAgentConfig.Builder().build()
         val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
@@ -218,47 +234,114 @@
         eachByte -> "%02x".format(eachByte)
     }
 
-    fun checkDscpValue(
+    fun sendPacket(
         agent: TestableNetworkAgent,
-        callback: TestableNetworkCallback,
-        dscpValue: Int = 0,
-        dstPort: Int = 0
+        sendV6: Boolean,
+        dstPort: Int = 0,
     ) {
         val testString = "test string"
         val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
         var packetFound = false
 
-        val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+        val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
+                IPPROTO_UDP)
         agent.network.bindSocket(socket)
 
         val originalPacket = testPacket.readAsArray()
-        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
-                0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
-
+        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+                if(sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
         Os.close(socket)
+    }
+
+    fun parseV4PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tos = buffer.get()
+        val length = buffer.getShort()
+        val id = buffer.getShort()
+        val offset = buffer.getShort()
+        val ttl = buffer.get()
+        val ipType = buffer.get()
+        val checksum = buffer.getShort()
+        return tos.toInt().shr(2)
+    }
+
+    fun parseV6PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tc = buffer.get()
+        val fl = buffer.getShort()
+        val length = buffer.getShort()
+        val proto = buffer.get()
+        val hop = buffer.get()
+        // DSCP is bottom 4 bits of ip_ver and top 2 of tc.
+        val ip_ver_bottom = ip_ver.toInt().and(0xf)
+        val tc_dscp = tc.toInt().shr(6)
+        return ip_ver_bottom.toInt().shl(2) + tc_dscp
+    }
+
+    fun parsePacketIp(
+        buffer : ByteBuffer,
+        sendV6 : Boolean,
+    ) : Boolean {
+        val ipAddr = if (sendV6) ByteArray(16) else ByteArray(4)
+        buffer.get(ipAddr)
+        val srcIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+        buffer.get(ipAddr)
+        val dstIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+
+        Log.e(TAG, "IP Src:" + srcIp + " dst: " + dstIp)
+
+        if ((sendV6 && srcIp == LOCAL_IPV6_ADDRESS && dstIp == TEST_TARGET_IPV6_ADDR) ||
+                (!sendV6 && srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR)) {
+            Log.e(TAG, "IP return true");
+            return true
+        }
+        Log.e(TAG, "IP return false");
+        return false
+    }
+
+    fun parsePacketPort(
+        buffer : ByteBuffer,
+        srcPort : Int,
+        dstPort : Int
+    ) : Boolean {
+        if (srcPort == 0 && dstPort == 0) return true
+
+        val packetSrcPort = buffer.getShort().toInt()
+        val packetDstPort = buffer.getShort().toInt()
+
+        Log.e(TAG, "Port Src:" + packetSrcPort + " dst: " + packetDstPort)
+
+        if ((srcPort == 0 || (srcPort != 0 && srcPort == packetSrcPort)) &&
+                (dstPort == 0 || (dstPort != 0 && dstPort == packetDstPort))) {
+            Log.e(TAG, "Port return true");
+            return true
+        }
+        Log.e(TAG, "Port return false");
+        return false
+    }
+
+    fun validatePacket(
+        agent : TestableNetworkAgent,
+        sendV6 : Boolean = false,
+        dscpValue : Int = 0,
+        dstPort : Int = 0,
+    ) {
+        var packetFound = false;
+        sendPacket(agent, sendV6, dstPort)
+        // TODO: grab source port from socket in sendPacket
+
+        Log.e(TAG, "find DSCP value:" + dscpValue)
         generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
             val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
-            val ip_ver = buffer.get()
-            val tos = buffer.get()
-            val length = buffer.getShort()
-            val id = buffer.getShort()
-            val offset = buffer.getShort()
-            val ttl = buffer.get()
-            val ipType = buffer.get()
-            val checksum = buffer.getShort()
+            val dscp = if (sendV6) parseV6PacketDscp(buffer) else parseV4PacketDscp(buffer)
+            Log.e(TAG, "DSCP value:" + dscp)
 
-            val ipAddr = ByteArray(4)
-            buffer.get(ipAddr)
-            val srcIp = Inet4Address.getByAddress(ipAddr)
-            buffer.get(ipAddr)
-            val dstIp = Inet4Address.getByAddress(ipAddr)
-            val packetSrcPort = buffer.getShort().toInt()
-            val packetDstPort = buffer.getShort().toInt()
-
-            // TODO: Add source port comparison.
-            if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
-                    packetDstPort == dstPort) {
-                assertEquals(dscpValue, (tos.toInt().shr(2)))
+            // TODO: Add source port comparison. Use 0 for now.
+            if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
+                Log.e(TAG, "DSCP value found")
+                assertEquals(dscpValue, dscp)
                 packetFound = true
             }
         }
@@ -275,12 +358,12 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(policyId, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = portNumber)
         }
     }
 
     @Test
-    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(4444, 4444)).build()
         agent.sendAddDscpPolicy(policy)
@@ -288,8 +371,7 @@
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-
-        checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+        validatePacket(agent, dscpValue = 1, dstPort = 4444)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -298,15 +380,54 @@
         }
 
         val policy2 = DscpPolicy.Builder(1, 4)
-                .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
-                .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourceAddress(LOCAL_IPV4_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
 
-        checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+        validatePacket(agent, dscpValue = 4, dstPort = 5555)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+    }
+
+    @Test
+    fun testDscpPolicyAddV6Policies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1)
+                .setDestinationPortRange(Range(4444, 4444)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+
+        val policy2 = DscpPolicy.Builder(1, 4)
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV6_ADDR)
+                .setSourceAddress(LOCAL_IPV6_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -324,7 +445,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -332,7 +453,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -340,13 +461,16 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
         doRemovePolicyTest(agent, callback, 1)
+        validatePacket(agent, dscpValue = 0, dstPort = 1111)
         doRemovePolicyTest(agent, callback, 2)
+        validatePacket(agent, dscpValue = 0, dstPort = 2222)
         doRemovePolicyTest(agent, callback, 3)
+        validatePacket(agent, dscpValue = 0, dstPort = 3333)
     }
 
     @Test
@@ -357,7 +481,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
         doRemovePolicyTest(agent, callback, 1)
 
@@ -366,7 +490,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
         doRemovePolicyTest(agent, callback, 2)
 
@@ -375,7 +499,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
         doRemovePolicyTest(agent, callback, 3)
     }
@@ -389,7 +513,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -397,7 +521,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -405,7 +529,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
@@ -423,14 +547,15 @@
     }
 
     @Test
-    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(1111, 1111)).build()
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1)
@@ -439,7 +564,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1)
@@ -448,24 +573,24 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         agent.sendRemoveAllDscpPolicies()
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 1111)
+            validatePacket(agent, false, dstPort = 1111)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 2222)
+            validatePacket(agent, false, dstPort = 2222)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 3333)
+            validatePacket(agent, false, dstPort = 3333)
         }
     }
 
@@ -477,12 +602,9 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+            validatePacket(agent, dscpValue = 1, dstPort = 4444)
         }
 
-        // TODO: send packet on socket and confirm that changing the DSCP policy
-        // updates the mark to the new value.
-
         val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -490,8 +612,8 @@
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
 
             // Sending packet with old policy should fail
-            checkDscpValue(agent, callback, dstPort = 4444)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+            validatePacket(agent, dscpValue = 0, dstPort = 4444)
+            validatePacket(agent, dscpValue = 1, dstPort = 5555)
         }
 
         agent.sendRemoveDscpPolicy(1)
@@ -504,15 +626,31 @@
     @Test
     fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
                 (agent, callback) ->
+        val policyId = 1
+        val dscpValue = 1
+        val range = Range(4444, 4444)
+        val srcPort = 555
+
         // Check that policy with partial parameters is lossless.
-        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+        val policy = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range).build()
+        assertEquals(policyId, policy.policyId)
+        assertEquals(dscpValue, policy.dscpValue)
+        assertEquals(range, policy.destinationPortRange)
         assertParcelingIsLossless(policy)
 
         // Check that policy with all parameters is lossless.
-        val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+        val policy2 = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range)
                 .setSourceAddress(LOCAL_IPV4_ADDRESS)
                 .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourcePort(srcPort)
                 .setProtocol(IPPROTO_UDP).build()
+        assertEquals(policyId, policy2.policyId)
+        assertEquals(dscpValue, policy2.dscpValue)
+        assertEquals(range, policy2.destinationPortRange)
+        assertEquals(TEST_TARGET_IPV4_ADDR, policy2.destinationAddress)
+        assertEquals(LOCAL_IPV4_ADDRESS, policy2.sourceAddress)
+        assertEquals(srcPort, policy2.sourcePort)
+        assertEquals(IPPROTO_UDP, policy2.protocol)
         assertParcelingIsLossless(policy2)
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 30e0015..1748612 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -15,83 +15,126 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.ROLE_CLIENT
+import android.net.EthernetManager.ROLE_NONE
+import android.net.EthernetManager.ROLE_SERVER
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_DOWN
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.EthernetManager.TetheredInterfaceCallback
+import android.net.EthernetManager.TetheredInterfaceRequest
+import android.net.EthernetNetworkManagementException
+import android.net.EthernetNetworkSpecifier
+import android.net.EthernetNetworkUpdateRequest
 import android.net.InetAddresses
 import android.net.IpConfiguration
 import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
+import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.OutcomeReceiver
 import android.platform.test.annotations.AppModeFull
+import android.util.ArraySet
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
+import com.android.testutils.anyNetwork
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
 import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
-import com.android.testutils.runAsShell
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import android.content.Context
-import org.junit.runner.RunWith
-import kotlin.test.assertNull
-import kotlin.test.fail
-import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
-import android.os.Handler
-import android.os.HandlerExecutor
-import android.os.Looper
-import com.android.networkstack.apishim.common.EthernetManagerShim.InterfaceStateListener
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_ABSENT
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_DOWN
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_UP
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
-import com.android.networkstack.apishim.EthernetManagerShimImpl
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RouterAdvertisementResponder
 import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
 import com.android.testutils.waitForIdle
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
 import java.net.Inet6Address
-import java.util.concurrent.Executor
-import kotlin.test.assertFalse
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.TimeUnit
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import java.net.NetworkInterface
+import kotlin.test.fail
 
-private const val TIMEOUT_MS = 1000L
+// TODO: try to lower this timeout in the future. Currently, ethernet tests are still flaky because
+// the interface is not ready fast enough (mostly due to the up / up / down / up issue).
+private const val TIMEOUT_MS = 2000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
     IpConfiguration.ProxySettings.NONE, null, null)
+private val ETH_REQUEST: NetworkRequest = NetworkRequest.Builder()
+    .addTransportType(TRANSPORT_TEST)
+    .addTransportType(TRANSPORT_ETHERNET)
+    .removeCapability(NET_CAPABILITY_TRUSTED)
+    .build()
 
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
-@RunWith(AndroidJUnit4::class)
+// EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
+@RunWith(DevSdkIgnoreRunner::class)
+// This test depends on behavior introduced post-T as part of connectivity module updates
+@ConnectivityModuleTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class EthernetManagerTest {
-    // EthernetManager is not updatable before T, so tests do not need to be backwards compatible
-    @get:Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    private val em by lazy { EthernetManagerShimImpl.newInstance(context) }
+    private val em by lazy { context.getSystemService(EthernetManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val handler by lazy { Handler(Looper.getMainLooper()) }
 
+    private val ifaceListener = EthernetStateListener()
     private val createdIfaces = ArrayList<EthernetTestInterface>()
-    private val addedListeners = ArrayList<InterfaceStateListener>()
+    private val addedListeners = ArrayList<EthernetStateListener>()
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+    private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
 
     private class EthernetTestInterface(
         context: Context,
-        private val handler: Handler
+        private val handler: Handler,
+        hasCarrier: Boolean
     ) {
         private val tapInterface: TestNetworkInterface
         private val packetReader: TapPacketReader
         private val raResponder: RouterAdvertisementResponder
-        val interfaceName get() = tapInterface.interfaceName
+        private val tnm: TestNetworkManager
+        val name get() = tapInterface.interfaceName
 
         init {
-            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
-                val tnm = context.getSystemService(TestNetworkManager::class.java)
-                tnm.createTapInterface(false /* bringUp */)
+            tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+                context.getSystemService(TestNetworkManager::class.java)
             }
-            val mtu = NetworkInterface.getByName(tapInterface.interfaceName).getMTU()
+            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+            }
+            val mtu = tapInterface.mtu
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
             raResponder = RouterAdvertisementResponder(packetReader)
             raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
@@ -101,6 +144,14 @@
             raResponder.start()
         }
 
+        // WARNING: this function requires kernel support. Call assumeChangingCarrierSupported() at
+        // the top of your test.
+        fun setCarrierEnabled(enabled: Boolean) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.setCarrierEnabled(tapInterface, enabled)
+            }
+        }
+
         fun destroy() {
             raResponder.stop()
             handler.post({ packetReader.stop() })
@@ -141,23 +192,90 @@
         }
 
         fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
-            expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
-                if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
+            expectCallback(createChangeEvent(iface.name, state, role))
         }
 
+        fun createChangeEvent(iface: String, state: Int, role: Int) =
+                InterfaceStateChanged(iface, state, role,
+                        if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
+
         fun pollForNextCallback(): CallbackEntry {
             return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
         }
 
+        fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
+
+        fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
+            assertNotNull(eventuallyExpect(createChangeEvent(iface.name, state, role)))
+        }
+
         fun assertNoCallback() {
             val cb = events.poll(NO_CALLBACK_TIMEOUT_MS)
             assertNull(cb, "Expected no callback but got $cb")
         }
     }
 
+    private class TetheredInterfaceListener : TetheredInterfaceCallback {
+        private val available = CompletableFuture<String>()
+
+        override fun onAvailable(iface: String) {
+            available.complete(iface)
+        }
+
+        override fun onUnavailable() {
+            available.completeExceptionally(IllegalStateException("onUnavailable was called"))
+        }
+
+        fun expectOnAvailable(): String {
+            return available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        }
+
+        fun expectOnUnavailable() {
+            // Assert that the future fails with the IllegalStateException from the
+            // completeExceptionally() call inside onUnavailable.
+            assertFailsWith(IllegalStateException::class) {
+                try {
+                    available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+                } catch (e: ExecutionException) {
+                    throw e.cause!!
+                }
+            }
+        }
+    }
+
+    private class EthernetOutcomeReceiver :
+        OutcomeReceiver<String, EthernetNetworkManagementException> {
+        private val result = CompletableFuture<String>()
+
+        override fun onResult(iface: String) {
+            result.complete(iface)
+        }
+
+        override fun onError(e: EthernetNetworkManagementException) {
+            result.completeExceptionally(e)
+        }
+
+        fun expectResult(expected: String) {
+            assertEquals(expected, result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS))
+        }
+
+        fun expectError() {
+            // Assert that the future fails with EthernetNetworkManagementException from the
+            // completeExceptionally() call inside onUnavailable.
+            assertFailsWith(EthernetNetworkManagementException::class) {
+                try {
+                    result.get()
+                } catch (e: ExecutionException) {
+                    throw e.cause!!
+                }
+            }
+        }
+    }
+
     @Before
     fun setUp() {
         setIncludeTestInterfaces(true)
+        addInterfaceStateListener(ifaceListener)
     }
 
     @After
@@ -165,19 +283,40 @@
         setIncludeTestInterfaces(false)
         for (iface in createdIfaces) {
             iface.destroy()
+            ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
         }
         for (listener in addedListeners) {
             em.removeInterfaceStateListener(listener)
         }
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+        releaseTetheredInterface()
     }
 
-    private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
-        em.addInterfaceStateListener(executor, listener)
+    // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
+    private fun assumeChangingCarrierSupported() = assumeTrue(isKernelVersionAtLeast("5.0.0"))
+
+    private fun addInterfaceStateListener(listener: EthernetStateListener) {
+        runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+            em.addInterfaceStateListener(handler::post, listener)
+        }
         addedListeners.add(listener)
     }
 
-    private fun createInterface(): EthernetTestInterface {
-        return EthernetTestInterface(context, Handler(Looper.getMainLooper()))
+    // WARNING: setting hasCarrier to false requires kernel support. Call
+    // assumeChangingCarrierSupported() at the top of your test.
+    private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
+        val iface = EthernetTestInterface(
+            context,
+            handler,
+            hasCarrier
+        ).also { createdIfaces.add(it) }
+
+        // when an interface comes up, we should always see a down cb before an up cb.
+        ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+        if (hasCarrier) {
+            ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+        }
+        return iface
     }
 
     private fun setIncludeTestInterfaces(value: Boolean) {
@@ -189,56 +328,357 @@
     private fun removeInterface(iface: EthernetTestInterface) {
         iface.destroy()
         createdIfaces.remove(iface)
+        ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
     }
 
-    @Test
-    public fun testCallbacks() {
-        val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+    private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.requestNetwork(request, it)
+            registeredCallbacks.add(it)
+        }
+    }
 
+    private fun registerNetworkListener(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.registerNetworkCallback(request, it)
+            registeredCallbacks.add(it)
+        }
+    }
+
+    private fun requestTetheredInterface() = TetheredInterfaceListener().also {
+        tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) {
+            em.requestTetheredInterface(handler::post, it)
+        }
+    }
+
+    private fun releaseTetheredInterface() {
+        runAsShell(NETWORK_SETTINGS) {
+            tetheredInterfaceRequest?.release()
+            tetheredInterfaceRequest = null
+        }
+    }
+
+    private fun releaseRequest(cb: TestableNetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        registeredCallbacks.remove(cb)
+    }
+
+    private fun disableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.disableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun enableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.enableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun updateConfiguration(
+        iface: EthernetTestInterface,
+        ipConfig: IpConfiguration? = null,
+        capabilities: NetworkCapabilities? = null
+    ) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.updateConfiguration(
+                iface.name,
+                EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(ipConfig)
+                    .setNetworkCapabilities(capabilities).build(),
+                handler::post,
+                it)
+        }
+    }
+
+    // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
+    // keep ETH_REQUEST as it is, a defensive copy is created here.
+    private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
+        NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
+            .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
+
+    // It can take multiple seconds for the network to become available.
+    private fun TestableNetworkCallback.expectAvailable() =
+        expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
+
+    // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
+    // eventuallyExpect(Lost::class) instead.
+    private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
+        eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
+
+    private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) =
+        assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
+
+    private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) =
+        assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) }
+
+    private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) =
+        expectCapabilitiesThat(anyNetwork()) {
+            it.networkSpecifier == EthernetNetworkSpecifier(name)
+        }
+
+    @Test
+    fun testCallbacks() {
         // If an interface exists when the callback is registered, it is reported on registration.
         val iface = createInterface()
-        val listener = EthernetStateListener()
-        addInterfaceStateListener(executor, listener)
-        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+        val listener1 = EthernetStateListener()
+        addInterfaceStateListener(listener1)
+        validateListenerOnRegistration(listener1)
 
         // If an interface appears, existing callbacks see it.
         // TODO: fix the up/up/down/up callbacks and only send down/up.
         val iface2 = createInterface()
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+        // Register a new listener, it should see state of all existing interfaces immediately.
+        val listener2 = EthernetStateListener()
+        addInterfaceStateListener(listener2)
+        validateListenerOnRegistration(listener2)
 
         // Removing interfaces first sends link down, then STATE_ABSENT/ROLE_NONE.
         removeInterface(iface)
-        listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
-        listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+        for (listener in listOf(listener1, listener2)) {
+            listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+            listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+        }
 
         removeInterface(iface2)
-        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+        for (listener in listOf(listener1, listener2)) {
+            listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+            listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+            listener.assertNoCallback()
+        }
+    }
+
+    private fun assumeNoInterfaceForTetheringAvailable() {
+        // Interfaces that have configured NetworkCapabilities will never be used for tethering,
+        // see aosp/2123900.
+        try {
+            // assumeException does not exist.
+            requestTetheredInterface().expectOnAvailable()
+            // interface used for tethering is available, throw an assumption error.
+            assumeTrue(false)
+        } catch (e: TimeoutException) {
+            // do nothing -- the TimeoutException indicates that no interface is available for
+            // tethering.
+            releaseTetheredInterface()
+        }
+    }
+
+    @Test
+    fun testCallbacks_forServerModeInterfaces() {
+        // do not run this test if an interface that can be used for tethering already exists.
+        assumeNoInterfaceForTetheringAvailable()
+
+        val iface = createInterface()
+        requestTetheredInterface().expectOnAvailable()
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+        // TODO(b/236895792): THIS IS A BUG! Existing server mode interfaces are not reported when
+        // an InterfaceStateListener is registered.
+        // Note: using eventuallyExpect as there may be other interfaces present.
+        // listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+        releaseTetheredInterface()
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        requestTetheredInterface().expectOnAvailable()
+        // This should be changed to expectCallback, once b/236895792 is fixed.
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+        releaseTetheredInterface()
+        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+    }
+
+    /**
+     * Validate all interfaces are returned for an EthernetStateListener upon registration.
+     */
+    private fun validateListenerOnRegistration(listener: EthernetStateListener) {
+        // Get all tracked interfaces to validate on listener registration. Ordering and interface
+        // state (up/down) can't be validated for interfaces not created as part of testing.
+        val ifaces = em.getInterfaceList()
+        val polledIfaces = ArraySet<String>()
+        for (i in ifaces) {
+            val event = (listener.pollForNextCallback() as InterfaceStateChanged)
+            val iface = event.iface
+            assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
+            assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
+            // If the event's iface was created in the test, additional criteria can be validated.
+            createdIfaces.find { it.name.equals(iface) }?.let {
+                assertEquals(event, listener.createChangeEvent(it.name, STATE_LINK_UP, ROLE_CLIENT))
+            }
+        }
+        // Assert all callbacks are accounted for.
         listener.assertNoCallback()
     }
 
     @Test
-    public fun testGetInterfaceList() {
-        setIncludeTestInterfaces(true)
-
+    fun testGetInterfaceList() {
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
         val iface2 = createInterface()
         var ifaces = em.getInterfaceList()
         assertTrue(ifaces.size > 0)
-        assertTrue(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertTrue(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         // Remove one existing test interface and check the return list doesn't contain the
         // removed interface name.
         removeInterface(iface1)
         ifaces = em.getInterfaceList()
-        assertFalse(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertFalse(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         removeInterface(iface2)
     }
+
+    @Test
+    fun testNetworkRequest_withSingleExistingInterface() {
+        createInterface()
+
+        // install a listener which will later be used to verify the Lost callback
+        val listenerCb = registerNetworkListener(ETH_REQUEST)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        cb.assertNeverLost()
+        releaseRequest(cb)
+        listenerCb.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_beforeSingleInterfaceIsUp() {
+        val cb = requestNetwork(ETH_REQUEST)
+
+        // bring up interface after network has been requested.
+        // Note: there is no guarantee that the NetworkRequest has been processed before the
+        // interface is actually created. That being said, it takes a few seconds between calling
+        // createInterface and the interface actually being properly registered with the ethernet
+        // module, so it is extremely unlikely that the CS handler thread has not run until then.
+        val iface = createInterface()
+        val network = cb.expectAvailable()
+
+        // remove interface before network request has been removed
+        cb.assertNeverLost()
+        removeInterface(iface)
+        cb.eventuallyExpectLost()
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfaces() {
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
+
+        val network = cb.expectAvailable()
+        cb.expectCapabilitiesWithInterfaceName(iface2.name)
+
+        removeInterface(iface1)
+        cb.assertNeverLost()
+        removeInterface(iface2)
+        cb.eventuallyExpectLost()
+    }
+
+    @Test
+    fun testNetworkRequest_withInterfaceBeingReplaced() {
+        val iface1 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        // create another network and verify the request sticks to the current network
+        val iface2 = createInterface()
+        cb.assertNeverLost()
+
+        // remove iface1 and verify the request brings up iface2
+        removeInterface(iface1)
+        cb.eventuallyExpectLost(network)
+        val network2 = cb.expectAvailable()
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfacesAndRequests() {
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.name))
+        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
+        val cb3 = requestNetwork(ETH_REQUEST)
+
+        cb1.expectAvailable()
+        cb1.expectCapabilitiesWithInterfaceName(iface1.name)
+        cb2.expectAvailable()
+        cb2.expectCapabilitiesWithInterfaceName(iface2.name)
+        // this request can be matched by either network.
+        cb3.expectAvailable()
+
+        cb1.assertNeverLost()
+        cb2.assertNeverLost()
+        cb3.assertNeverLost()
+    }
+
+    @Test
+    fun testNetworkRequest_ensureProperRefcounting() {
+        // create first request before interface is up / exists; create another request after it has
+        // been created; release one of them and check that the network stays up.
+        val listener = registerNetworkListener(ETH_REQUEST)
+        val cb1 = requestNetwork(ETH_REQUEST)
+
+        val iface = createInterface()
+        val network = cb1.expectAvailable()
+
+        val cb2 = requestNetwork(ETH_REQUEST)
+        cb2.expectAvailable()
+
+        // release the first request; this used to trigger b/197548738
+        releaseRequest(cb1)
+
+        cb2.assertNeverLost()
+        releaseRequest(cb2)
+        listener.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
+        assumeChangingCarrierSupported()
+
+        val iface = createInterface(false /* hasCarrier */)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        cb.assertNeverAvailable()
+
+        iface.setCarrierEnabled(true)
+        cb.expectAvailable()
+
+        iface.setCarrierEnabled(false)
+        cb.eventuallyExpectLost()
+    }
+
+    @Test
+    fun testRemoveInterface_whileInServerMode() {
+        assumeNoInterfaceForTetheringAvailable()
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+
+        val iface = createInterface()
+        val ifaceName = requestTetheredInterface().expectOnAvailable()
+
+        assertEquals(iface.name, ifaceName)
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+        removeInterface(iface)
+
+        // Note: removeInterface already verifies that a STATE_ABSENT, ROLE_NONE callback is
+        // received, but it can't hurt to explicitly check for it.
+        listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+        releaseTetheredInterface()
+        listener.assertNoCallback()
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
new file mode 100644
index 0000000..c8ee0c7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 android.net.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+public class EthernetNetworkUpdateRequestTest {
+    private static final NetworkCapabilities DEFAULT_CAPS =
+            new NetworkCapabilities.Builder()
+                    .removeCapability(NET_CAPABILITY_NOT_RESTRICTED).build();
+    private static final StaticIpConfiguration DEFAULT_STATIC_IP_CONFIG =
+            new StaticIpConfiguration.Builder().setDomains("test").build();
+    private static final IpConfiguration DEFAULT_IP_CONFIG =
+            new IpConfiguration.Builder()
+                    .setStaticIpConfiguration(DEFAULT_STATIC_IP_CONFIG).build();
+
+    private EthernetNetworkUpdateRequest createRequest(@NonNull final NetworkCapabilities nc,
+            @NonNull final IpConfiguration ipConfig) {
+        return new EthernetNetworkUpdateRequest.Builder()
+                .setNetworkCapabilities(nc)
+                .setIpConfiguration(ipConfig)
+                .build();
+    }
+
+    @Test
+    public void testGetNetworkCapabilities() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        assertEquals(DEFAULT_CAPS, r.getNetworkCapabilities());
+    }
+
+    @Test
+    public void testGetIpConfiguration() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        assertEquals(DEFAULT_IP_CONFIG, r.getIpConfiguration());
+    }
+
+    @Test
+    public void testBuilderWithRequest() {
+        final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+        final EthernetNetworkUpdateRequest rFromExisting =
+                new EthernetNetworkUpdateRequest.Builder(r).build();
+
+        assertNotSame(r, rFromExisting);
+        assertEquals(r.getIpConfiguration(), rFromExisting.getIpConfiguration());
+        assertEquals(r.getNetworkCapabilities(), rFromExisting.getNetworkCapabilities());
+    }
+
+    @Test
+    public void testNullIpConfigurationAndNetworkCapabilitiesThrows() {
+        assertThrows("Should not be able to build with null ip config and network capabilities.",
+                IllegalStateException.class,
+                () -> createRequest(null, null));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 9590f88..2b1d173 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -20,10 +20,9 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
 
@@ -51,6 +50,7 @@
 import android.net.TestNetworkInterface;
 import android.net.VpnManager;
 import android.net.cts.util.CtsNetUtils;
+import android.net.cts.util.IkeSessionTestUtils;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 import android.os.Process;
@@ -61,12 +61,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
 import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.apishim.common.VpnManagerShim;
 import com.android.networkstack.apishim.common.VpnProfileStateShim;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -87,6 +82,7 @@
 import java.security.KeyPairGenerator;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
@@ -196,6 +192,7 @@
 
     private final X509Certificate mServerRootCa;
     private final CertificateAndKey mUserCertKey;
+    private final List<TestableNetworkCallback> mCallbacksToUnregister = new ArrayList<>();
 
     public Ikev2VpnTest() throws Exception {
         // Build certificates
@@ -205,6 +202,9 @@
 
     @After
     public void tearDown() {
+        for (TestableNetworkCallback callback : mCallbacksToUnregister) {
+            sCM.unregisterNetworkCallback(callback);
+        }
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
     }
@@ -224,22 +224,17 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
             boolean requiresValidation) throws Exception {
 
-        builderShim.setBypassable(true)
+        builder.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
         if (TestUtils.shouldTestTApis()) {
-            builderShim.setRequiresInternetValidation(requiresValidation);
+            builder.setRequiresInternetValidation(requiresValidation);
         }
-
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and does not defined in shims.
-        // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -247,11 +242,31 @@
         return builder.build();
     }
 
+    private Ikev2VpnProfile buildIkev2VpnProfileIkeTunConnParams(
+            final boolean isRestrictedToTestNetworks, final boolean requiresValidation,
+            final boolean testIpv6) throws Exception {
+        final IkeTunnelConnectionParams params =
+                new IkeTunnelConnectionParams(testIpv6
+                        ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
+                        IkeSessionTestUtils.CHILD_PARAMS);
+
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(params)
+                        .setRequiresInternetValidation(requiresValidation)
+                        .setProxy(TEST_PROXY_INFO)
+                        .setMaxMtu(TEST_MTU)
+                        .setMetered(false);
+
+        if (isRestrictedToTestNetworks) {
+            builder.restrictToTestNetworks();
+        }
+        return builder.build();
+    }
+
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
             boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
-                        .setAuthPsk(TEST_PSK);
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 requiresValidation);
     }
@@ -259,8 +274,8 @@
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */);
@@ -268,8 +283,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -302,15 +317,8 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
-        if (TestUtils.shouldTestTApis()) {
-            assertEquals(requiresValidation, shim.isInternetValidationRequired());
-        } else {
-            try {
-                shim.isInternetValidationRequired();
-                fail("Only supported from API level 33");
-            } catch (UnsupportedApiLevelException expected) {
-            }
+        if (isAtLeastT()) {
+            assertEquals(requiresValidation, profile.isInternetValidationRequired());
         }
     }
 
@@ -320,10 +328,10 @@
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
         assumeTrue(TestUtils.shouldTestTApis());
 
-        final IkeTunnelConnectionParams expectedParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
-        final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+        final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
+                IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
+        final Ikev2VpnProfile.Builder ikeProfileBuilder =
+                new Ikev2VpnProfile.Builder(expectedParams);
         // Verify the other Ike options could not be set with IkeTunnelConnectionParams.
         final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
         assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
@@ -332,10 +340,9 @@
         assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
 
-        final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+        final Ikev2VpnProfile profile = ikeProfileBuilder.build();
 
-        assertEquals(expectedParams,
-                new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+        assertEquals(expectedParams, profile.getIkeTunnelConnectionParams());
     }
 
     @Test
@@ -467,7 +474,8 @@
     }
 
     private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
-            boolean testIpv6, boolean requiresValidation, boolean testSessionKey)
+            boolean testIpv6, boolean requiresValidation, boolean testSessionKey,
+            boolean testIkeTunConnParams)
             throws Exception {
         String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
         String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
@@ -477,14 +485,17 @@
         // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
         mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
 
-        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(serverAddr,
-                true /* isRestrictedToTestNetworks */, requiresValidation);
+        final Ikev2VpnProfile profile = testIkeTunConnParams
+                ? buildIkev2VpnProfileIkeTunConnParams(true /* isRestrictedToTestNetworks */,
+                        requiresValidation, testIpv6)
+                : buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */,
+                        requiresValidation);
         assertNull(sVpnMgr.provisionVpnProfile(profile));
 
         final TestableNetworkCallback cb = new TestableNetworkCallback(TIMEOUT_MS);
         final NetworkRequest nr = new NetworkRequest.Builder()
                 .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
-        sCM.registerNetworkCallback(nr, cb);
+        registerNetworkCallback(nr, cb);
 
         if (testSessionKey) {
             // testSessionKey will never be true if running on <T
@@ -520,7 +531,8 @@
             assertFalse(profileState.isLockdownEnabled());
         }
 
-        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS, caps -> caps.hasTransport(TRANSPORT_VPN)
+        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS,
+                caps -> caps.hasTransport(TRANSPORT_VPN)
                 && caps.hasCapability(NET_CAPABILITY_INTERNET)
                 && !caps.hasCapability(NET_CAPABILITY_VALIDATED)
                 && Process.myUid() == caps.getOwnerUid());
@@ -533,8 +545,10 @@
         // but unexpectedly sends this callback, expecting LOST below will fail because the next
         // callback will be the validated capabilities instead.
         // In S and below, |requiresValidation| is ignored, so this callback is always sent
-        // regardless of its value.
-        if (!requiresValidation || !TestUtils.shouldTestTApis()) {
+        // regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
+        // misuse VPN network itself as the underlying network. The fix is not available without
+        // SDK > T platform. Thus, verify this only on T+ platform.
+        if (!requiresValidation && TestUtils.shouldTestTApis()) {
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
                     entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                             .hasCapability(NET_CAPABILITY_VALIDATED));
@@ -547,10 +561,16 @@
                 lost -> vpnNetwork.equals(lost.getNetwork()));
     }
 
+    private void registerNetworkCallback(NetworkRequest request, TestableNetworkCallback callback) {
+        sCM.registerNetworkCallback(request, callback);
+        mCallbacksToUnregister.add(callback);
+    }
+
     private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
         private final boolean mTestIpv6Only;
         private final boolean mRequiresValidation;
         private final boolean mTestSessionKey;
+        private final boolean mTestIkeTunConnParams;
 
         /**
          * Constructs the test
@@ -560,10 +580,11 @@
          * @param testSessionKey if true, start VPN by calling startProvisionedVpnProfileSession()
          */
         VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation,
-                boolean testSessionKey) {
+                boolean testSessionKey, boolean testIkeTunConnParams) {
             mTestIpv6Only = testIpv6Only;
             mRequiresValidation = requiresValidation;
             mTestSessionKey = testSessionKey;
+            mTestIkeTunConnParams = testIkeTunConnParams;
         }
 
         @Override
@@ -571,8 +592,8 @@
                 throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(
-                    tunUtils, mTestIpv6Only, mRequiresValidation, mTestSessionKey);
+            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only, mRequiresValidation,
+                    mTestSessionKey, mTestIkeTunConnParams);
         }
 
         @Override
@@ -590,53 +611,83 @@
         }
     }
 
-    @Test
-    public void testStartStopVpnProfileV4() throws Exception {
+    private void doTestStartStopVpnProfile(boolean testIpv6Only, boolean requiresValidation,
+            boolean testSessionKey, boolean testIkeTunConnParams) throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
-
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
                 new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        false /* testIpv6Only */, false /* requiresValidation */,
-                        false /* testSessionKey */)));
+                        testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
+    }
 
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        false /* testIpv6Only */, true /* requiresValidation */,
-                        false /* testSessionKey */)));
+    @Test
+    public void testStartStopVpnProfileV4() throws Exception {
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileV4WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     @Test
     public void testStartStopVpnProfileV6() throws Exception {
-        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
 
-        // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        true /* testIpv6Only */, false /* requiresValidation */,
-                        false /* testSessionKey */)));
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        true /* testIpv6Only */, true /* requiresValidation */,
-                        false /* testSessionKey */)));
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileV6WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+                false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @IgnoreUpTo(SC_V2)
     @Test
-    public void testStartProvisionedVpnProfileSession() throws Exception {
-        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+    public void testStartProvisionedVpnV4ProfileSession() throws Exception {
         assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+                true /* testSessionKey */, false /* testIkeTunConnParams */);
+    }
 
-        // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        false /* testIpv6Only */, false /* requiresValidation */,
-                        true /* testSessionKey */)));
-
-        runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
-                        true /* testIpv6Only */, false /* requiresValidation */,
-                        true /* testSessionKey */)));
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testStartProvisionedVpnV6ProfileSession() throws Exception {
+        assumeTrue(TestUtils.shouldTestTApis());
+        doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+                true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 0504973..d4f3d57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -1275,4 +1275,23 @@
         matchAllCallback.expectCallback<Lost>(wifiNetwork)
         wifiAgent.expectCallback<OnNetworkUnwanted>()
     }
+
+    @Test
+    fun testUnregisterAgentBeforeAgentFullyConnected() {
+        val specifier = UUID.randomUUID().toString()
+        val callback = TestableNetworkCallback()
+        val transports = intArrayOf(TRANSPORT_CELLULAR)
+        // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+        val nc = makeTestNetworkCapabilities(specifier, transports)
+        val agent = createNetworkAgent(realContext, initialNc = nc)
+        // Connect the agent
+        agent.register()
+        // Mark agent connected then unregister agent immediately. Verify that both available and
+        // lost callback should be sent still.
+        agent.markConnected()
+        agent.unregister()
+        callback.expectCallback<Available>(agent.network!!)
+        callback.eventuallyExpect<Lost> { it.network == agent.network }
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fb720a7..f86c5cd 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -29,8 +29,20 @@
 import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
 import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
 import static android.app.usage.NetworkStats.Bucket.UID_ALL;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.AppOpsManager;
+import android.app.Instrumentation;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -40,7 +52,11 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkRequest;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
 import android.net.TrafficStats;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -48,11 +64,23 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
 import android.telephony.TelephonyManager;
-import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -62,8 +90,18 @@
 import java.net.UnknownHostException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
-public class NetworkStatsManagerTest extends InstrumentationTestCase {
+@ConnectivityModuleTest
+@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
+
     private static final String LOG_TAG = "NetworkStatsManagerTest";
     private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
     private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
@@ -164,9 +202,11 @@
             };
 
     private String mPkg;
+    private Context mContext;
     private NetworkStatsManager mNsm;
     private ConnectivityManager mCm;
     private PackageManager mPm;
+    private Instrumentation mInstrumentation;
     private long mStartTime;
     private long mEndTime;
 
@@ -224,44 +264,40 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mNsm = (NetworkStatsManager) getInstrumentation().getContext()
-                .getSystemService(Context.NETWORK_STATS_SERVICE);
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mNsm = mContext.getSystemService(NetworkStatsManager.class);
         mNsm.setPollForce(true);
 
-        mCm = (ConnectivityManager) getInstrumentation().getContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mPm = mContext.getPackageManager();
+        mPkg = mContext.getPackageName();
 
-        mPm = getInstrumentation().getContext().getPackageManager();
-
-        mPkg = getInstrumentation().getContext().getPackageName();
-
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS);
         setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow");
         mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mWriteSettingsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode);
         }
         if (mUsageStatsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode);
         }
-        super.tearDown();
     }
 
     private void setAppOpsMode(String appop, String mode) throws Exception {
         final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
-        SystemUtil.runShellCommand(command);
+        SystemUtil.runShellCommand(mInstrumentation, command);
     }
 
     private String getAppOpsMode(String appop) throws Exception {
         final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
-        String result = SystemUtil.runShellCommand(command);
+        String result = SystemUtil.runShellCommand(mInstrumentation, command);
         if (result == null) {
             Log.w(LOG_TAG, "App op " + appop + " could not be read.");
         }
@@ -269,7 +305,7 @@
     }
 
     private boolean isInForeground() throws IOException {
-        String result = SystemUtil.runShellCommand(getInstrumentation(),
+        String result = SystemUtil.runShellCommand(mInstrumentation,
                 "cmd activity get-uid-state " + Process.myUid());
         return result.contains("FOREGROUND");
     }
@@ -366,15 +402,14 @@
     private String getSubscriberId(int networkIndex) {
         int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType();
         if (ConnectivityManager.TYPE_MOBILE == networkType) {
-            TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext()
-                    .getSystemService(Context.TELEPHONY_SERVICE);
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
             return ShellIdentityUtils.invokeMethodWithShellPermissions(tm,
                     (telephonyManager) -> telephonyManager.getSubscriberId());
         }
         return "";
     }
 
-    @AppModeFull
+    @Test
     public void testDeviceSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -410,7 +445,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUserSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -446,7 +481,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Use tolerance value that large enough to make sure stats of at
@@ -522,7 +557,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -565,7 +600,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUidDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -619,7 +654,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testTagDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -726,7 +761,7 @@
                 bucket.getRxBytes(), bucket.getTxBytes()));
     }
 
-    @AppModeFull
+    @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -803,7 +838,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testCallback() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -825,6 +860,43 @@
             // storing files of >2MB in CTS.
 
             mNsm.unregisterUsageCallback(usageCallback);
+
+            // For T- devices, the registerUsageCallback invocation below will need a looper
+            // from the thread that calls into the API, which is not available in the test.
+            if (SdkLevel.isAtLeastT()) {
+                mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(),
+                        getSubscriberId(i), THRESHOLD_BYTES, usageCallback);
+                mNsm.unregisterUsageCallback(usageCallback);
+            }
+        }
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testDataMigrationUtils() throws Exception {
+        final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
+        for (final String prefix : prefixes) {
+            final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)
+                    : TimeUnit.HOURS.toMillis(2);
+
+            final NetworkStatsCollection collection =
+                    NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, duration);
+
+            final long now = System.currentTimeMillis();
+            final Set<Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory>> entries =
+                    collection.getEntries().entrySet();
+            for (final Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory> entry : entries) {
+                for (final NetworkStatsHistory.Entry historyEntry : entry.getValue().getEntries()) {
+                    // Verify all value fields are reasonable.
+                    assertTrue(historyEntry.getBucketStart() <= now);
+                    assertTrue(historyEntry.getActiveTime() <= duration);
+                    assertTrue(historyEntry.getRxBytes() >= 0);
+                    assertTrue(historyEntry.getRxPackets() >= 0);
+                    assertTrue(historyEntry.getTxBytes() >= 0);
+                    assertTrue(historyEntry.getTxPackets() >= 0);
+                    assertTrue(historyEntry.getOperations() >= 0);
+                }
+            }
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 391d03a..462c8a3 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -16,16 +16,11 @@
 
 package android.net.cts
 
-import android.Manifest
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
 import android.net.util.NetworkStackUtils
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
-import android.util.Log
 import com.android.testutils.runAsShell
-import com.android.testutils.tryTest
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
 
 /**
  * Collection of utility methods for configuring network validation.
@@ -38,9 +33,14 @@
      * Clear the test network validation URLs.
      */
     @JvmStatic fun clearValidationTestUrlsDeviceConfig() {
-        setHttpsUrlDeviceConfig(null)
-        setHttpUrlDeviceConfig(null)
-        setUrlExpirationDeviceConfig(null)
+        runAsShell(WRITE_DEVICE_CONFIG) {
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+        }
     }
 
     /**
@@ -48,71 +48,28 @@
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
      */
-    @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+    @JvmStatic
+    fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
 
     /**
      * Set the test validation HTTP URL.
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
      */
-    @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+    @JvmStatic
+    fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
 
     /**
      * Set the test validation URL expiration.
      *
      * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
      */
-    @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
-            setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
-
-    private fun setConfig(configKey: String, value: String?): String? {
-        Log.i(TAG, "Setting config \"$configKey\" to \"$value\"")
-        val readWritePermissions = arrayOf(
-                Manifest.permission.READ_DEVICE_CONFIG,
-                Manifest.permission.WRITE_DEVICE_CONFIG)
-
-        val existingValue = runAsShell(*readWritePermissions) {
-            DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, configKey)
-        }
-        if (existingValue == value) {
-            // Already the correct value. There may be a race if a change is already in flight,
-            // but if multiple threads update the config there is no way to fix that anyway.
-            Log.i(TAG, "\$configKey\" already had value \"$value\"")
-            return value
-        }
-
-        val future = CompletableFuture<String>()
-        val listener = DeviceConfig.OnPropertiesChangedListener {
-            // The listener receives updates for any change to any key, so don't react to
-            // changes that do not affect the relevant key
-            if (!it.keyset.contains(configKey)) return@OnPropertiesChangedListener
-            if (it.getString(configKey, null) == value) {
-                future.complete(value)
-            }
-        }
-
-        return tryTest {
-            runAsShell(*readWritePermissions) {
-                DeviceConfig.addOnPropertiesChangedListener(
-                        NAMESPACE_CONNECTIVITY,
-                        inlineExecutor,
-                        listener)
-                DeviceConfig.setProperty(
-                        NAMESPACE_CONNECTIVITY,
-                        configKey,
-                        value,
-                        false /* makeDefault */)
-                // Don't drop the permission until the config is applied, just in case
-                future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-            }.also {
-                Log.i(TAG, "Config \"$configKey\" successfully set to \"$value\"")
-            }
-        } cleanup {
-            DeviceConfig.removeOnPropertiesChangedListener(listener)
-        }
-    }
-
-    private val inlineExecutor get() = Executor { r -> r.run() }
+    @JvmStatic
+    fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index b139a9b..64cc97d 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,6 +22,7 @@
 import android.net.Network
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkRequest
@@ -45,17 +46,16 @@
 import android.net.nsd.NsdManager.RegistrationListener
 import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
+import android.os.Handler
 import android.os.HandlerThread
+import android.os.Process.myTid
 import android.platform.test.annotations.AppModeFull
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
-import com.android.networkstack.apishim.ConstantsShim
 import com.android.networkstack.apishim.NsdShimImpl
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
@@ -65,7 +65,6 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.ServerSocket
@@ -82,6 +81,7 @@
 private const val TAG = "NsdManagerTest"
 private const val SERVICE_TYPE = "_nmt._tcp"
 private const val TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private const val DBG = false
 
 private val nsdShim = NsdShimImpl.newInstance()
@@ -89,10 +89,6 @@
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(AndroidJUnit4::class)
 class NsdManagerTest {
-    // NsdManager is not updatable before S, so tests do not need to be backwards compatible
-    @get:Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
-
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
 
@@ -118,12 +114,20 @@
 
     private interface NsdEvent
     private open class NsdRecord<T : NsdEvent> private constructor(
-        private val history: ArrayTrackRecord<T>
+        private val history: ArrayTrackRecord<T>,
+        private val expectedThreadId: Int? = null
     ) : TrackRecord<T> by history {
-        constructor() : this(ArrayTrackRecord())
+        constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
 
         val nextEvents = history.newReadHead()
 
+        override fun add(e: T): Boolean {
+            if (expectedThreadId != null) {
+                assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
+            }
+            return history.add(e)
+        }
+
         inline fun <reified V : NsdEvent> expectCallbackEventually(
             crossinline predicate: (V) -> Boolean = { true }
         ): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
@@ -136,10 +140,15 @@
                     nextEvent.javaClass.simpleName)
             return nextEvent
         }
+
+        inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
+            val cb = nextEvents.poll(timeoutMs)
+            assertNull(cb, "Expected no callback but got $cb")
+        }
     }
 
-    private class NsdRegistrationRecord : RegistrationListener,
-            NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+    private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
+            NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
         sealed class RegistrationEvent : NsdEvent {
             abstract val serviceInfo: NsdServiceInfo
 
@@ -176,8 +185,8 @@
         }
     }
 
-    private class NsdDiscoveryRecord : DiscoveryListener,
-            NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+    private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
+            DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
         sealed class DiscoveryEvent : NsdEvent {
             data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
                 : DiscoveryEvent()
@@ -249,9 +258,11 @@
     fun setUp() {
         handlerThread.start()
 
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1 = createTestNetwork()
-            testNetwork2 = createTestNetwork()
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1 = createTestNetwork()
+                testNetwork2 = createTestNetwork()
+            }
         }
     }
 
@@ -290,9 +301,11 @@
 
     @After
     fun tearDown() {
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1.close(cm)
-            testNetwork2.close(cm)
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1.close(cm)
+                testNetwork2.close(cm)
+            }
         }
         handlerThread.quitSafely()
     }
@@ -393,14 +406,17 @@
         si2.serviceName = serviceName
         si2.port = localPort
         val registrationRecord2 = NsdRegistrationRecord()
-        val registeredInfo2 = registerService(registrationRecord2, si2)
+        nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
+        val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>().serviceInfo
 
         // Expect a service record to be discovered (and filter the ones
         // that are unrelated to this test)
         val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
 
         // Resolve the service
-        val resolvedService2 = resolveService(foundInfo2)
+        val resolveRecord2 = NsdResolveRecord()
+        nsdManager.resolveService(foundInfo2, resolveRecord2)
+        val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
 
         // Check that the resolved service doesn't have any TXT records
         assertEquals(0, resolvedService2.attributes.size)
@@ -416,7 +432,7 @@
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
         // This test requires shims supporting T+ APIs (discovering on specific network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -450,16 +466,19 @@
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
         // This test requires shims supporting T+ APIs (discovering on network request)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
         si.serviceName = this.serviceName
         si.port = 12345 // Test won't try to connect so port does not matter
 
-        val registrationRecord = NsdRegistrationRecord()
-        val registeredInfo1 = registerService(registrationRecord, si)
-        val discoveryRecord = NsdDiscoveryRecord()
+        val handler = Handler(handlerThread.looper)
+        val executor = Executor { handler.post(it) }
+
+        val registrationRecord = NsdRegistrationRecord(expectedThreadId = handlerThread.threadId)
+        val registeredInfo1 = registerService(registrationRecord, si, executor)
+        val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
 
         tryTest {
             val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
@@ -469,7 +488,7 @@
                             .addTransportType(TRANSPORT_TEST)
                             .setNetworkSpecifier(specifier)
                             .build(),
-                    Executor { it.run() }, discoveryRecord)
+                    executor, discoveryRecord)
 
             val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>()
             assertEquals(SERVICE_TYPE, discoveryStarted.serviceType)
@@ -485,7 +504,7 @@
             assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo))
 
             registrationRecord.expectCallback<ServiceUnregistered>()
-            val registeredInfo2 = registerService(registrationRecord, si)
+            val registeredInfo2 = registerService(registrationRecord, si, executor)
             val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
             assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
             assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
@@ -513,9 +532,42 @@
     }
 
     @Test
+    fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
+        // This test requires shims supporting T+ APIs (discovering on network request)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val handler = Handler(handlerThread.looper)
+        val executor = Executor { handler.post(it) }
+
+        val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
+        val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
+
+        tryTest {
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                    NetworkRequest.Builder()
+                            .removeCapability(NET_CAPABILITY_TRUSTED)
+                            .addTransportType(TRANSPORT_TEST)
+                            // Specified network does not have this capability
+                            .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+                            .setNetworkSpecifier(specifier)
+                            .build(),
+                    executor, discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStarted>()
+        } cleanup {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        }
+    }
+
+    @Test
     fun testNsdManager_ResolveOnNetwork() {
         // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -556,12 +608,99 @@
         }
     }
 
+    @Test
+    fun testNsdManager_RegisterOnNetwork() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.network = testNetwork1.network
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+        val discoveryRecord = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        val discoveryRecord3 = NsdDiscoveryRecord()
+
+        tryTest {
+            // Discover service on testNetwork1.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, Executor { it.run() }, discoveryRecord)
+            // Expect that service is found on testNetwork1
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(
+                serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+
+            // Discover service on testNetwork2.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork2.network, Executor { it.run() }, discoveryRecord2)
+            // Expect that discovery is started then no other callbacks.
+            discoveryRecord2.expectCallback<DiscoveryStarted>()
+            discoveryRecord2.assertNoCallback()
+
+            // Discover service on all networks (not specify any network).
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
+            // Expect that service is found on testNetwork1
+            val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
+                    serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
+        val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
+        val si = NsdServiceInfo().apply {
+            serviceType = SERVICE_TYPE
+            serviceName = serviceNames
+            port = 12345 // Test won't try to connect so port does not matter
+        }
+
+        // Register the service name which contains non-standard characters.
+        val registrationRecord = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>()
+
+        tryTest {
+            // Discover that service name.
+            val discoveryRecord = NsdDiscoveryRecord()
+            nsdManager.discoverServices(
+                SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
+            )
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+
+            // Expect that resolving the service name works properly even service name contains
+            // non-standard characters.
+            val resolveRecord = NsdResolveRecord()
+            nsdManager.resolveService(foundInfo, resolveRecord)
+            val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
+            assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+        } cleanup {
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     /**
      * Register a service and return its registration record.
      */
-    private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
-        nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() },
-                record)
+    private fun registerService(
+        record: NsdRegistrationRecord,
+        si: NsdServiceInfo,
+        executor: Executor = Executor { it.run() }
+    ): NsdServiceInfo {
+        nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, executor, record)
         // We may not always get the name that we tried to register;
         // This events tells us the name that was registered.
         val cb = record.expectCallback<ServiceRegistered>()
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
index 423f213..28cec1a 100644
--- a/tests/cts/net/src/android/net/cts/RateLimitTest.java
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -304,7 +304,7 @@
         // If this value is too low, this test might become flaky because of the burst value that
         // allows to send at a higher data rate for a short period of time. The faster the data rate
         // and the longer the test, the less this test will be affected.
-        final long dataLimitInBytesPerSecond = 1_000_000; // 1MB/s
+        final long dataLimitInBytesPerSecond = 2_000_000; // 2MB/s
         long resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
         assertGreaterThan("Failed initial test with rate limit disabled", resultInBytesPerSecond,
                 dataLimitInBytesPerSecond);
@@ -315,9 +315,9 @@
         waitForTcPoliceFilterInstalled(Duration.ofSeconds(1));
 
         resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(10));
-        // Add 1% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
+        // Add 10% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
         assertLessThan("Failed test with rate limit enabled", resultInBytesPerSecond,
-                (long) (dataLimitInBytesPerSecond * 1.01));
+                (long) (dataLimitInBytesPerSecond * 1.1));
 
         ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
 
diff --git a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
index b4ebcdb..244bfc5 100644
--- a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
@@ -16,44 +16,73 @@
 
 package android.net.cts.util;
 
+import static android.net.ipsec.ike.SaProposal.DH_GROUP_4096_BIT_MODP;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128;
 import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128;
-import static android.net.ipsec.ike.SaProposal.KEY_LEN_UNUSED;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256;
+import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC;
 
+import android.net.InetAddresses;
 import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeIpv4AddrIdentification;
+import android.net.ipsec.ike.IkeIpv6AddrIdentification;
 import android.net.ipsec.ike.IkeSaProposal;
 import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.SaProposal;
 import android.net.ipsec.ike.TunnelModeChildSessionParams;
 
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
 /** Shared testing parameters and util methods for testing IKE */
 public class IkeSessionTestUtils {
-    private static final String TEST_CLIENT_ADDR = "test.client.com";
-    private static final String TEST_SERVER_ADDR = "test.server.com";
-    private static final String TEST_SERVER = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+    private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2";
+    private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2";
+    private static final String TEST_IDENTITY = "client.cts.android.com";
+    private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes();
+    public static final IkeSessionParams IKE_PARAMS_V4 = getTestIkeSessionParams(false);
+    public static final IkeSessionParams IKE_PARAMS_V6 = getTestIkeSessionParams(true);
 
-    public static final IkeSaProposal SA_PROPOSAL = new IkeSaProposal.Builder()
-            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES, KEY_LEN_UNUSED)
-            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
-            .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
-            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-            .build();
-    public static final ChildSaProposal CHILD_PROPOSAL = new ChildSaProposal.Builder()
-            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128)
-            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
-            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-            .build();
+    public static final TunnelModeChildSessionParams CHILD_PARAMS = getChildSessionParams();
 
-    public static final IkeSessionParams IKE_PARAMS =
-            new IkeSessionParams.Builder()
-                    .setServerHostname(TEST_SERVER)
-                    .addSaProposal(SA_PROPOSAL)
-                    .setLocalIdentification(new IkeFqdnIdentification(TEST_CLIENT_ADDR))
-                    .setRemoteIdentification(new IkeFqdnIdentification(TEST_SERVER_ADDR))
-                    .setAuthPsk("psk".getBytes())
-                    .build();
-    public static final TunnelModeChildSessionParams CHILD_PARAMS =
-            new TunnelModeChildSessionParams.Builder()
-                    .addSaProposal(CHILD_PROPOSAL)
-                    .build();
+    private static TunnelModeChildSessionParams getChildSessionParams() {
+        final TunnelModeChildSessionParams.Builder childOptionsBuilder =
+                new TunnelModeChildSessionParams.Builder()
+                        .addSaProposal(getChildSaProposals());
+
+        return childOptionsBuilder.build();
+    }
+
+    private static IkeSessionParams getTestIkeSessionParams(boolean testIpv6) {
+        final String testServer = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
+        final InetAddress addr = InetAddresses.parseNumericAddress(testServer);
+        final IkeSessionParams.Builder ikeOptionsBuilder =
+                new IkeSessionParams.Builder()
+                        .setServerHostname(testServer)
+                        .setLocalIdentification(new IkeFqdnIdentification(TEST_IDENTITY))
+                        .setRemoteIdentification(testIpv6
+                                ? new IkeIpv6AddrIdentification((Inet6Address) addr)
+                                : new IkeIpv4AddrIdentification((Inet4Address) addr))
+                        .setAuthPsk(TEST_PSK)
+                        .addSaProposal(getIkeSaProposals());
+
+        return ikeOptionsBuilder.build();
+    }
+
+    private static IkeSaProposal getIkeSaProposals() {
+        return new IkeSaProposal.Builder()
+                .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256)
+                .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128)
+                .addDhGroup(DH_GROUP_4096_BIT_MODP)
+                .addPseudorandomFunction(PSEUDORANDOM_FUNCTION_AES128_XCBC).build();
+    }
+
+    private static ChildSaProposal getChildSaProposals() {
+        return new ChildSaProposal.Builder()
+                .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128)
+                .build();
+    }
 }
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index e9c4e5a..6096a8b 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -56,7 +56,7 @@
     defaults: ["CtsTetheringTestDefaults"],
 
     min_sdk_version: "30",
-    target_sdk_version: "30",
+    target_sdk_version: "33",
 
     static_libs: [
         "TetheringIntegrationTestsLatestSdkLib",
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 97c1265..b3684ac 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -71,7 +71,7 @@
         "net-tests-utils",
     ],
     libs: [
-        "service-connectivity",
+        "service-connectivity-for-tests",
         "services.core",
         "services.net",
     ],
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 80338aa..efc24d3 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -47,6 +47,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.connectivity.resources.R
+import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
@@ -208,6 +209,7 @@
         doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
         doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
         doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
+        doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any())
         doAnswer { inv ->
             object : MultinetworkPolicyTracker(inv.getArgument(0), inv.getArgument(1),
                     inv.getArgument(2)) {
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index c7cf040..361c968 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -22,8 +22,8 @@
 import android.net.INetworkMonitorCallbacks
 import android.net.Network
 import android.net.metrics.IpConnectivityLog
-import android.net.util.SharedLog
 import android.os.IBinder
+import com.android.net.module.util.SharedLog
 import com.android.networkstack.netlink.TcpSocketTracker
 import com.android.server.NetworkStackService
 import com.android.server.NetworkStackService.NetworkMonitorConnector
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 2bba282..db39e6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -42,6 +42,9 @@
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
+#define PRIVATE "/sys/fs/bpf/net_private/"
+#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD "/sys/fs/bpf/netd_shared/"
 
 class BpfExistenceTest : public ::testing::Test {
 };
@@ -84,6 +87,45 @@
 };
 
 static const set<string> INTRODUCED_T = {
+    SHARED "map_block_blocked_ports_map",
+    SHARED "map_clatd_clat_egress4_map",
+    SHARED "map_clatd_clat_ingress6_map",
+    SHARED "map_dscp_policy_ipv4_dscp_policies_map",
+    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_A",
+    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_B",
+    SHARED "map_dscp_policy_ipv6_dscp_policies_map",
+    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_A",
+    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_B",
+    SHARED "map_dscp_policy_switch_comp_map",
+    NETD "map_netd_app_uid_stats_map",
+    NETD "map_netd_configuration_map",
+    NETD "map_netd_cookie_tag_map",
+    NETD "map_netd_iface_index_name_map",
+    NETD "map_netd_iface_stats_map",
+    NETD "map_netd_stats_map_A",
+    NETD "map_netd_stats_map_B",
+    NETD "map_netd_uid_counterset_map",
+    NETD "map_netd_uid_owner_map",
+    NETD "map_netd_uid_permission_map",
+    SHARED "prog_clatd_schedcls_egress4_clat_ether",
+    SHARED "prog_clatd_schedcls_egress4_clat_rawip",
+    SHARED "prog_clatd_schedcls_ingress6_clat_ether",
+    SHARED "prog_clatd_schedcls_ingress6_clat_rawip",
+    NETD "prog_netd_cgroupskb_egress_stats",
+    NETD "prog_netd_cgroupskb_ingress_stats",
+    NETD "prog_netd_cgroupsock_inet_create",
+    NETD "prog_netd_schedact_ingress_account",
+    NETD "prog_netd_skfilter_allowlist_xtbpf",
+    NETD "prog_netd_skfilter_denylist_xtbpf",
+    NETD "prog_netd_skfilter_egress_xtbpf",
+    NETD "prog_netd_skfilter_ingress_xtbpf",
+};
+
+static const set<string> INTRODUCED_T_5_4 = {
+    SHARED "prog_block_bind4_block_port",
+    SHARED "prog_block_bind6_block_port",
+    SHARED "prog_dscp_policy_schedcls_set_dscp_ether",
+    SHARED "prog_dscp_policy_schedcls_set_dscp_raw_ip",
 };
 
 static const set<string> REMOVED_T = {
@@ -125,6 +167,7 @@
 
     if (IsAtLeastT()) {
         addAll(expected, INTRODUCED_T);
+        if (android::bpf::isAtLeastKernelVersion(5, 4, 0)) addAll(expected, INTRODUCED_T_5_4);
         removeAll(expected, REMOVED_T);
 
         addAll(unexpected, REMOVED_T);
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
index a8d908a..7d43aa8 100644
--- a/tests/native/Android.bp
+++ b/tests/native/Android.bp
@@ -31,3 +31,10 @@
     ],
     compile_multilib: "first",
 }
+
+filegroup {
+    name: "net_native_test_config_template",
+    srcs: [
+        "NetNativeTestConfigTemplate.xml",
+    ],
+}
diff --git a/tests/native/NetNativeTestConfigTemplate.xml b/tests/native/NetNativeTestConfigTemplate.xml
new file mode 100644
index 0000000..b71e9aa
--- /dev/null
+++ b/tests/native/NetNativeTestConfigTemplate.xml
@@ -0,0 +1,31 @@
+<!-- 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.
+-->
+<configuration description="Configuration for {MODULE} tests">
+    <option name="test-suite-tag" value="mts" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+    <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+    </test>
+</configuration>
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
index 8b089ab..3db5265 100644
--- a/tests/native/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test.cpp
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
 #include <android-modules-utils/sdk_level.h>
 #include <cutils/misc.h>  // FIRST_APPLICATION_UID
 #include <gtest/gtest.h>
 #include <netinet/in.h>
-#include <android/binder_manager.h>
-#include <android/binder_process.h>
 
-#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include "bpf/BpfUtils.h"
 
 using aidl::android::net::connectivity::aidl::IConnectivityNative;
 
@@ -40,6 +41,10 @@
         if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() <<
                 "Should be at least T device.";
 
+        // Skip test case if not on 5.4 kernel which is required by bpf prog.
+        if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
+                "Kernel should be at least 5.4.";
+
         ASSERT_NE(nullptr, mService.get());
 
         // If there are already ports being blocked on device unblockAllPortsForBind() store
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index df8ab74..4ab24fc 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -22,6 +22,6 @@
     static_libs: [
         "androidx.test.rules",
         "mockito-target-minus-junit4",
-        "service-connectivity",
+        "service-connectivity-for-tests",
     ],
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 545f7b9..0908ad2 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -54,41 +54,23 @@
 filegroup {
     name: "non-connectivity-module-test",
     srcs: [
-        "java/android/app/usage/*.java",
-        "java/android/net/EthernetNetworkUpdateRequestTest.java",
         "java/android/net/Ikev2VpnProfileTest.java",
         "java/android/net/IpMemoryStoreTest.java",
-        "java/android/net/IpSecAlgorithmTest.java",
-        "java/android/net/IpSecConfigTest.java",
-        "java/android/net/IpSecManagerTest.java",
-        "java/android/net/IpSecTransformTest.java",
-        "java/android/net/KeepalivePacketDataUtilTest.java",
-        "java/android/net/NetworkIdentityTest.kt",
-        "java/android/net/NetworkStats*.java",
-        "java/android/net/NetworkTemplateTest.kt",
         "java/android/net/TelephonyNetworkSpecifierTest.java",
         "java/android/net/VpnManagerTest.java",
         "java/android/net/ipmemorystore/*.java",
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
-        "java/android/net/nsd/*.java",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
-        "java/com/android/server/IpSecServiceParameterizedTest.java",
-        "java/com/android/server/IpSecServiceRefcountedResourceTest.java",
-        "java/com/android/server/IpSecServiceTest.java",
         "java/com/android/server/NetworkManagementServiceTest.java",
-        "java/com/android/server/NsdServiceTest.java",
+        "java/com/android/server/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
-        "java/com/android/server/ethernet/*.java",
         "java/com/android/server/net/ipmemorystore/*.java",
-        "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
-        "java/com/android/server/net/IpConfigStoreTest.java",
-        "java/com/android/server/net/NetworkStats*.java",
-        "java/com/android/server/net/TestableUsageCallback.kt",
+        "java/com/android/server/connectivity/mdns/**/*.java",
     ]
 }
 
@@ -107,7 +89,7 @@
     name: "FrameworksNetTestsDefaults",
     min_sdk_version: "30",
     defaults: [
-        "framework-connectivity-test-defaults",
+        "framework-connectivity-internal-test-defaults",
     ],
     srcs: [
         "java/**/*.java",
@@ -163,6 +145,7 @@
     static_libs: [
         "services.core",
         "services.net",
+        "service-mdns",
     ],
     jni_libs: [
         "libandroid_net_connectivity_com_android_net_module_util_jni",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 887f171..54e1cd0 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -50,7 +50,7 @@
     <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
     <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
 
-    <application>
+    <application android:testOnly="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="android.net.ipsec.ike" />
         <activity
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 939ae49..2d32e55 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -15,7 +15,8 @@
 -->
 <configuration description="Runs Frameworks Networking Tests.">
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="FrameworksNetTests.apk" />
+      <option name="test-file-name" value="FrameworksNetTests.apk" />
+      <option name="install-arg" value="-t" />
     </target_preparer>
 
     <option name="test-suite-tag" value="apct" />
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 561e621..b1b76ec 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -54,7 +54,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsManagerTest {
     private static final String TEST_SUBSCRIBER_ID = "subid";
 
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index f324630..c327868 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -41,6 +41,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -72,6 +73,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -82,6 +84,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.lang.ref.WeakReference;
+
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
@@ -461,4 +465,49 @@
         }
         fail("expected exception of type " + throwableType);
     }
+
+    private static class MockContext extends BroadcastInterceptingContext {
+        MockContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return mock(Context.class);
+        }
+    }
+
+    private WeakReference<Context> makeConnectivityManagerAndReturnContext() {
+        // Mockito may have an internal reference to the mock, creating MockContext for testing.
+        final Context c = new MockContext(mock(Context.class));
+
+        new ConnectivityManager(c, mService);
+
+        return new WeakReference<>(c);
+    }
+
+    private void forceGC() {
+        // First GC ensures that objects are collected for finalization, then second GC ensures
+        // they're garbage-collected after being finalized.
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    @Test
+    public void testConnectivityManagerDoesNotLeakContext() throws Exception {
+        final WeakReference<Context> ref = makeConnectivityManagerAndReturnContext();
+
+        final int attempts = 100;
+        final long waitIntervalMs = 50;
+        for (int i = 0; i < attempts; i++) {
+            forceGC();
+            if (ref.get() == null) break;
+
+            Thread.sleep(waitIntervalMs);
+        }
+
+        assertNull("ConnectivityManager weak reference still not null after " + attempts
+                    + " attempts", ref.get());
+    }
 }
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 8222ca1..5cb014f 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -17,7 +17,7 @@
 package android.net;
 
 import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -448,7 +448,7 @@
     @Test
     public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
         final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
         // Config authentication related fields is not required while building with
         // IkeTunnelConnectionParams.
         final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
@@ -464,9 +464,9 @@
 
         // Verify building with IkeTunnelConnectionParams
         final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
         final IkeTunnelConnectionParams tunnelParams2 =
-                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
         assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
                 new Ikev2VpnProfile.Builder(tunnelParams2).build());
     }
diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java
index c473e82..1482055 100644
--- a/tests/unit/java/android/net/IpSecAlgorithmTest.java
+++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java
@@ -47,7 +47,7 @@
 /** Unit tests for {@link IpSecAlgorithm}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecAlgorithmTest {
     private static final byte[] KEY_MATERIAL;
 
diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java
index b87cb48..9f83036 100644
--- a/tests/unit/java/android/net/IpSecConfigTest.java
+++ b/tests/unit/java/android/net/IpSecConfigTest.java
@@ -36,7 +36,7 @@
 /** Unit tests for {@link IpSecConfig}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecConfigTest {
 
     @Test
diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java
index cda8eb7..335f539 100644
--- a/tests/unit/java/android/net/IpSecManagerTest.java
+++ b/tests/unit/java/android/net/IpSecManagerTest.java
@@ -52,7 +52,7 @@
 /** Unit tests for {@link IpSecManager}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecManagerTest {
 
     private static final int TEST_UDP_ENCAP_PORT = 34567;
diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java
index 81375f1..c1bd719 100644
--- a/tests/unit/java/android/net/IpSecTransformTest.java
+++ b/tests/unit/java/android/net/IpSecTransformTest.java
@@ -32,7 +32,7 @@
 /** Unit tests for {@link IpSecTransform}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecTransformTest {
 
     @Test
diff --git a/tests/unit/java/android/net/NetworkIdentitySetTest.kt b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
new file mode 100644
index 0000000..d61ebf9
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 android.net
+
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.os.Build
+import android.telephony.TelephonyManager
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+
+private const val TEST_IMSI1 = "testimsi1"
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkIdentitySetTest {
+    private val mockContext = mock(Context::class.java)
+
+    private fun buildMobileNetworkStateSnapshot(
+        caps: NetworkCapabilities,
+        subscriberId: String
+    ): NetworkStateSnapshot {
+        return NetworkStateSnapshot(mock(Network::class.java), caps,
+                LinkProperties(), subscriberId, TYPE_MOBILE)
+    }
+
+    @Test
+    fun testCompare() {
+        val ident1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val ident2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+
+        // Verify that the results of comparing two empty sets are equal
+        assertEquals(0, NetworkIdentitySet.compare(NetworkIdentitySet(), NetworkIdentitySet()))
+
+        val identSet1 = NetworkIdentitySet()
+        val identSet2 = NetworkIdentitySet()
+        identSet1.add(ident1)
+        identSet2.add(ident2)
+        assertEquals(-1, NetworkIdentitySet.compare(NetworkIdentitySet(), identSet1))
+        assertEquals(1, NetworkIdentitySet.compare(identSet1, NetworkIdentitySet()))
+        assertEquals(0, NetworkIdentitySet.compare(identSet1, identSet1))
+        assertEquals(-1, NetworkIdentitySet.compare(identSet1, identSet2))
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index bf5568d..d84328c 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -47,7 +47,7 @@
 private const val TEST_SUBID2 = 2
 
 @RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class NetworkIdentityTest {
     private val mockContext = mock(Context::class.java)
 
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java
index 97a93ca..a74056b 100644
--- a/tests/unit/java/android/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -19,6 +19,7 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.when;
 
 import android.Manifest;
@@ -66,6 +67,10 @@
         when(mContext.getSystemServiceName(DevicePolicyManager.class))
                 .thenReturn(Context.DEVICE_POLICY_SERVICE);
         when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDpm);
+        if (mContext.getSystemService(DevicePolicyManager.class) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(DevicePolicyManager.class);
+        }
 
         setHasCarrierPrivileges(false);
         setIsDeviceOwner(false);
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 32c106d..b518a61 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -37,6 +37,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
+import android.annotation.NonNull;
 import android.content.res.Resources;
 import android.net.NetworkStatsCollection.Key;
 import android.os.Process;
@@ -76,6 +77,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Tests for {@link NetworkStatsCollection}.
@@ -534,50 +536,84 @@
         assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
     }
 
+    private static void assertCollectionEntries(
+            @NonNull Map<Key, NetworkStatsHistory> expectedEntries,
+            @NonNull NetworkStatsCollection collection) {
+        final Map<Key, NetworkStatsHistory> actualEntries = collection.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());
+    }
+
     @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);
+    public void testRemoveHistoryBefore() {
+        final NetworkIdentity testIdent = new NetworkIdentity.Builder()
+                .setSubscriberId(TEST_IMSI).build();
+        final Key key1 = new Key(Set.of(testIdent), 0, 0, 0);
+        final Key key2 = new Key(Set.of(testIdent), 1, 0, 0);
         final long bucketDuration = 10;
 
+        // Prepare entries for testing, with different bucket start timestamps.
         final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
                 4, 50, 5, 60);
-        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(20, 10, 3,
                 41, 7, 1, 0);
+        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(30, 10, 1,
+                21, 70, 4, 1);
 
         NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
                 .addEntry(entry1)
                 .addEntry(entry2)
                 .build();
-
-        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
-
-        NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+        NetworkStatsHistory history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        NetworkStatsCollection collection = 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.
+        // Verify nothing is removed if the cutoff time is equal to bucketStart.
+        collection.removeHistoryBefore(10);
+        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
         expectedEntries.put(key1, history1);
+        expectedEntries.put(key2, history2);
+        assertCollectionEntries(expectedEntries, collection);
 
-        final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+        // Verify entry1 will be removed if its bucket start before to cutoff timestamp.
+        collection.removeHistoryBefore(11);
+        history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .build();
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoff1Entries1 = new ArrayMap<>();
+        cutoff1Entries1.put(key1, history1);
+        cutoff1Entries1.put(key2, history2);
+        assertCollectionEntries(cutoff1Entries1, collection);
 
-        assertEquals(expectedEntries.size(), actualEntries.size());
-        for (Key expectedKey : expectedEntries.keySet()) {
-            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+        // Verify entry2 will be removed if its bucket start covers by cutoff timestamp.
+        collection.removeHistoryBefore(22);
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoffEntries2 = new ArrayMap<>();
+        // History1 is not expected since the collection will omit empty entries.
+        cutoffEntries2.put(key2, history2);
+        assertCollectionEntries(cutoffEntries2, collection);
 
-            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
-            assertNotNull(actualHistory);
-
-            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
-
-            actualEntries.remove(expectedKey);
-        }
-        assertEquals(0, actualEntries.size());
+        // Verify all entries will be removed if cutoff timestamp covers all.
+        collection.removeHistoryBefore(Long.MAX_VALUE);
+        assertEquals(0, collection.getEntries().size());
     }
 
     /**
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c170605..43e331b 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,12 +56,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
-import java.util.List;
 import java.util.Random;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsHistoryTest {
     private static final String TAG = "NetworkStatsHistoryTest";
 
@@ -271,7 +270,7 @@
     }
 
     @Test
-    public void testRemove() throws Exception {
+    public void testRemoveStartingBefore() throws Exception {
         stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
 
         // record some data across 24 buckets
@@ -279,28 +278,28 @@
         assertEquals(24, stats.size());
 
         // try removing invalid data; should be no change
-        stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+        stats.removeBucketsStartingBefore(0 - DAY_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing far before buckets; should be no change
-        stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START - YEAR_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing just moments into first bucket; should be no change
-        // since that bucket contains data beyond the cutoff
-        stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS);
+        // since that bucket doesn't contain data starts before the cutoff
+        stats.removeBucketsStartingBefore(TEST_START);
         assertEquals(24, stats.size());
 
         // try removing single bucket
-        stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + HOUR_IN_MILLIS);
         assertEquals(23, stats.size());
 
         // try removing multiple buckets
-        stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+        stats.removeBucketsStartingBefore(TEST_START + (4 * HOUR_IN_MILLIS));
         assertEquals(20, stats.size());
 
         // try removing all buckets
-        stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + YEAR_IN_MILLIS);
         assertEquals(0, stats.size());
     }
 
@@ -350,7 +349,7 @@
                         stats.recordData(start, end, entry);
                     } else {
                         // trim something
-                        stats.removeBucketsBefore(r.nextLong());
+                        stats.removeBucketsStartingBefore(r.nextLong());
                     }
                 }
                 assertConsistent(stats);
@@ -533,40 +532,6 @@
         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/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
new file mode 100644
index 0000000..fad11a3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *i
+ * 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 android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkStats;
+import android.os.DropBoxManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.FileRotator;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public final class NetworkStatsRecorderTest {
+    private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
+
+    private static final String TEST_PREFIX = "test";
+
+    @Mock private DropBoxManager mDropBox;
+    @Mock private NetworkStats.NonMonotonicObserver mObserver;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
+        return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
+                    HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+    }
+
+    @Test
+    public void testWipeOnError() throws Exception {
+        final FileRotator rotator = mock(FileRotator.class);
+        final NetworkStatsRecorder wipeOnErrorRecorder = buildRecorder(rotator, true);
+
+        // Assuming that the rotator gets an exception happened when read data.
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        wipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the files will be deleted.
+        verify(rotator, times(1)).deleteAll();
+        reset(rotator);
+
+        final NetworkStatsRecorder noWipeOnErrorRecorder = buildRecorder(rotator, false);
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        noWipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the rotator won't delete files.
+        verify(rotator, never()).deleteAll();
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index b0cc16c..6d79869 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -61,7 +61,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NetworkStatsTest {
 
     private static final String TEST_IFACE = "test0";
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 453612f..3e9662d 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -29,12 +29,8 @@
 import android.net.NetworkStats.METERED_NO
 import android.net.NetworkStats.METERED_YES
 import android.net.NetworkStats.ROAMING_ALL
-import android.net.NetworkTemplate.MATCH_BLUETOOTH
-import android.net.NetworkTemplate.MATCH_CARRIER
-import android.net.NetworkTemplate.MATCH_ETHERNET
 import android.net.NetworkTemplate.MATCH_MOBILE
 import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
-import android.net.NetworkTemplate.MATCH_PROXY
 import android.net.NetworkTemplate.MATCH_WIFI
 import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
 import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -52,11 +48,9 @@
 import android.net.wifi.WifiInfo
 import android.os.Build
 import android.telephony.TelephonyManager
-import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
 import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SC_V2
 import com.android.testutils.assertParcelSane
 import org.junit.Before
 import org.junit.Test
@@ -65,7 +59,6 @@
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
@@ -77,7 +70,7 @@
 private const val TEST_WIFI_KEY2 = "wifiKey2"
 
 @RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class NetworkTemplateTest {
     private val mockContext = mock(Context::class.java)
     private val mockWifiInfo = mock(WifiInfo::class.java)
@@ -555,140 +548,4 @@
             it.assertMatches(identMobileImsi3)
         }
     }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderMatchRules() {
-        // Verify unknown match rules cannot construct templates.
-        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify hidden match rules cannot construct templates.
-        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify template which matches metered cellular and carrier networks with
-        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
-        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
-                    .setMeteredness(METERED_YES).build().let {
-                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
-                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                        assertEquals(expectedTemplate, it)
-                    }
-        }
-
-        // Verify carrier template cannot be created without IMSI.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_CARRIER).build()
-        }
-
-        // Verify template which matches metered cellular networks,
-        // regardless of IMSI. See buildTemplateMobileWildcard.
-        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches metered cellular networks and ratType.
-        // See NetworkTemplate#buildTemplateMobileWithRatType.
-        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
-                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
-                .build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches all wifi networks,
-        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
-        NetworkTemplate.Builder(MATCH_WIFI).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches wifi networks with the given Wifi Network Key.
-        // See buildTemplateWifi(wifiNetworkKey).
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches all wifi networks with the
-        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
-        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
-                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
-                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches ethernet and bluetooth networks.
-        // See buildTemplateEthernet and buildTemplateBluetooth.
-        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).build().let {
-                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
-                        null /*subscriberIds*/, arrayOf<String>(),
-                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-                assertEquals(expectedTemplate, it)
-            }
-        }
-    }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderWifiNetworkKeys() {
-        // Verify template builder which generates same template with the given different
-        // sequence keys.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
-            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches non-wifi networks with the given key is invalid.
-        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
-                Integer.MAX_VALUE).forEach { matchRule ->
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
-            }
-        }
-
-        // Verify template which matches wifi networks with the given null key is invalid.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
-        }
-
-        // Verify template which matches wifi wildcard with the given empty key set.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-    }
 }
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index 91f2cdd..6820b40 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -16,8 +16,17 @@
 
 package android.net;
 
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.os.Build;
 
@@ -29,6 +38,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
@@ -36,14 +46,14 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class QosSocketFilterTest {
-
+    private static final int TEST_NET_ID = 1777;
+    private final Network mNetwork = new Network(TEST_NET_ID);
     @Test
     public void testPortExactMatch() {
         final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
         final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
         assertTrue(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
-
     }
 
     @Test
@@ -77,5 +87,90 @@
         assertFalse(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
     }
+
+    @Test
+    public void testAddressMatchWithAnyLocalAddresses() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("0.0.0.0");
+        assertTrue(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 10), addressB, 10, 10));
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressB, 10), addressA, 10, 10));
+    }
+
+    @Test
+    public void testProtocolMatch() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 10));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+        socketV6.connect(new InetSocketAddress("::1", socketV6.getLocalPort() + 10));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertTrue(socketFilter.matchesProtocol(IPPROTO_UDP));
+        assertTrue(socketFilter6.matchesProtocol(IPPROTO_UDP));
+        assertFalse(socketFilter.matchesProtocol(IPPROTO_TCP));
+        assertFalse(socketFilter6.matchesProtocol(IPPROTO_TCP));
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidate() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 7));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter6.validate());
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidateUnbind() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_BOUND, socketFilter.validate());
+        socket.close();
+    }
+
+    @Test
+    public void testValidateLocalAddressChanged() throws Exception {
+        DatagramSocket socket = new DatagramSocket(null);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socket6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        socket.bind(new InetSocketAddress("127.0.0.1", 0));
+        socket6.bind(new InetSocketAddress("::1", 0));
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter6.validate());
+        socket.close();
+        socket6.close();
+    }
+
+    @Test
+    public void testValidateRemoteAddressChanged() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 53137));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 11));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        socket.disconnect();
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_CONNECTED, socketFilter.validate());
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 13));
+        assertEquals(EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED, socketFilter.validate());
+        socket.close();
+    }
 }
 
diff --git a/tests/unit/java/android/net/QosSocketInfoTest.java b/tests/unit/java/android/net/QosSocketInfoTest.java
new file mode 100644
index 0000000..749c182
--- /dev/null
+++ b/tests/unit/java/android/net/QosSocketInfoTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 android.net;
+
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class QosSocketInfoTest {
+    @Mock
+    private Network mMockNetwork = mock(Network.class);
+
+    @Test
+    public void testConstructWithSock() throws Exception {
+        ServerSocket server = new ServerSocket();
+        ServerSocket server6 = new ServerSocket();
+
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        server.bind(serverAddr);
+        server6.bind(serverAddr6);
+        Socket socket = new Socket(serverAddr.getAddress(), server.getLocalPort(),
+                clientAddr.getAddress(), clientAddr.getPort());
+        Socket socket6 = new Socket(serverAddr6.getAddress(), server6.getLocalPort(),
+                clientAddr6.getAddress(), clientAddr6.getPort());
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort())));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket6.getLocalAddress(), socket6.getLocalPort())));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo6.getSocketType());
+        socket.close();
+        socket6.close();
+        server.close();
+        server6.close();
+    }
+
+    @Test
+    public void testConstructWithDatagramSock() throws Exception {
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        DatagramSocket socket = new DatagramSocket(null);
+        socket.setReuseAddress(true);
+        socket.bind(clientAddr);
+        socket.connect(serverAddr);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        socket6.setReuseAddress(true);
+        socket6.bind(clientAddr);
+        socket6.connect(serverAddr);
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket.getLocalSocketAddress()));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket6.getLocalSocketAddress()));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo6.getSocketType());
+        socket.close();
+    }
+}
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
index 743d39e..aa5a246 100644
--- a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -61,14 +61,6 @@
         assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
     }
 
-    @Test
-    fun testMaybeReadLegacyUid() {
-        val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
-        NetworkStatsDataMigrationUtils.readLegacyUid(builder,
-                getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
-        assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
-    }
-
     private fun assertValues(
         collection: NetworkStatsCollection,
         expectedSize: Int,
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 30b8fcd..32274bc 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -51,7 +51,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdManagerTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index e5e7ebc..64355ed 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -42,7 +42,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceInfoTest {
 
     public final static InetAddress LOCALHOST;
@@ -125,6 +125,7 @@
         fullInfo.setPort(4242);
         fullInfo.setHost(LOCALHOST);
         fullInfo.setNetwork(new Network(123));
+        fullInfo.setInterfaceIndex(456);
         checkParcelable(fullInfo);
 
         NsdServiceInfo noHostInfo = new NsdServiceInfo();
@@ -175,6 +176,7 @@
         assertEquals(original.getHost(), result.getHost());
         assertTrue(original.getPort() == result.getPort());
         assertEquals(original.getNetwork(), result.getNetwork());
+        assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
 
         // Assert equality of attribute map.
         Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 360390d..0a6d2f2 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -17,7 +17,7 @@
 package com.android.internal.net;
 
 import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
@@ -128,7 +128,7 @@
     private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
         final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
                 false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS));
+                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS));
 
         p.name = "foo";
         p.server = "bar";
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index f07a10d..433b892 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,53 +16,446 @@
 
 package com.android.server;
 
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
 import static android.net.INetd.PERMISSION_INTERNET;
 
+import static com.android.server.BpfNetMaps.DOZABLE_MATCH;
+import static com.android.server.BpfNetMaps.HAPPY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.IIF_MATCH;
+import static com.android.server.BpfNetMaps.LOCKDOWN_VPN_MATCH;
+import static com.android.server.BpfNetMaps.NO_MATCH;
+import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
+import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.mockito.Mockito.verify;
 
 import android.net.INetd;
 import android.os.Build;
+import android.os.ServiceSpecificException;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
+
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public final class BpfNetMapsTest {
     private static final String TAG = "BpfNetMapsTest";
+
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     private static final int TEST_UID = 10086;
     private static final int[] TEST_UIDS = {10002, 10003};
-    private static final String IFNAME = "wlan0";
+    private static final String TEST_IF_NAME = "wlan0";
+    private static final int TEST_IF_INDEX = 7;
+    private static final int NO_IIF = 0;
     private static final String CHAINNAME = "fw_dozable";
+    private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+    private static final List<Integer> FIREWALL_CHAINS = List.of(
+            FIREWALL_CHAIN_DOZABLE,
+            FIREWALL_CHAIN_STANDBY,
+            FIREWALL_CHAIN_POWERSAVE,
+            FIREWALL_CHAIN_RESTRICTED,
+            FIREWALL_CHAIN_LOW_POWER_STANDBY,
+            FIREWALL_CHAIN_OEM_DENY_1,
+            FIREWALL_CHAIN_OEM_DENY_2,
+            FIREWALL_CHAIN_OEM_DENY_3
+    );
+
     private BpfNetMaps mBpfNetMaps;
 
     @Mock INetd mNetd;
+    private final BpfMap<U32, U32> mConfigurationMap = new TestBpfMap<>(U32.class, U32.class);
+    private final BpfMap<U32, UidOwnerValue> mUidOwnerMap =
+            new TestBpfMap<>(U32.class, UidOwnerValue.class);
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
+        BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
         mBpfNetMaps = new BpfNetMaps(mNetd);
     }
 
     @Test
     public void testBpfNetMapsBeforeT() throws Exception {
         assumeFalse(SdkLevel.isAtLeastT());
-        mBpfNetMaps.addUidInterfaceRules(IFNAME, TEST_UIDS);
-        verify(mNetd).firewallAddUidInterfaceRules(IFNAME, TEST_UIDS);
+        mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+        verify(mNetd).firewallAddUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
         mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS);
         verify(mNetd).firewallRemoveUidInterfaceRules(TEST_UIDS);
         mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
         verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
     }
+
+    private void doTestIsChainEnabled(final List<Integer> enableChains) throws Exception {
+        long match = 0;
+        for (final int chain: enableChains) {
+            match |= mBpfNetMaps.getMatchByFirewallChain(chain);
+        }
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(match));
+
+        for (final int chain: FIREWALL_CHAINS) {
+            final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
+            if (enableChains.contains(chain)) {
+                assertTrue("Expected isChainEnabled returns True, " + testCase,
+                        mBpfNetMaps.isChainEnabled(chain));
+            } else {
+                assertFalse("Expected isChainEnabled returns False, " + testCase,
+                        mBpfNetMaps.isChainEnabled(chain));
+            }
+        }
+    }
+
+    private void doTestIsChainEnabled(final int enableChain) throws Exception {
+        doTestIsChainEnabled(List.of(enableChain));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsChainEnabled() throws Exception {
+        doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
+        doTestIsChainEnabled(FIREWALL_CHAIN_STANDBY);
+        doTestIsChainEnabled(FIREWALL_CHAIN_POWERSAVE);
+        doTestIsChainEnabled(FIREWALL_CHAIN_RESTRICTED);
+        doTestIsChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsChainEnabledMultipleChainEnabled() throws Exception {
+        doTestIsChainEnabled(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY));
+        doTestIsChainEnabled(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED));
+        doTestIsChainEnabled(FIREWALL_CHAINS);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsChainEnabledInvalidChain() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(-1 /* childChain */));
+        assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(1000 /* childChain */));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testIsChainEnabledBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.isChainEnabled(FIREWALL_CHAIN_DOZABLE));
+    }
+
+    private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
+        long expectedMatch = 0;
+        for (final int chain: testChains) {
+            expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+        }
+
+        assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+        for (final int chain: testChains) {
+            mBpfNetMaps.setChildChain(chain, true /* enable */);
+        }
+        assertEquals(expectedMatch, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+        for (final int chain: testChains) {
+            mBpfNetMaps.setChildChain(chain, false /* enable */);
+        }
+        assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+    }
+
+    private void doTestSetChildChain(final int testChain) throws Exception {
+        doTestSetChildChain(List.of(testChain));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChain() throws Exception {
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        doTestSetChildChain(FIREWALL_CHAIN_DOZABLE);
+        doTestSetChildChain(FIREWALL_CHAIN_STANDBY);
+        doTestSetChildChain(FIREWALL_CHAIN_POWERSAVE);
+        doTestSetChildChain(FIREWALL_CHAIN_RESTRICTED);
+        doTestSetChildChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainMultipleChain() throws Exception {
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        doTestSetChildChain(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY));
+        doTestSetChildChain(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED));
+        doTestSetChildChain(FIREWALL_CHAINS);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainInvalidChain() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected,
+                () -> mBpfNetMaps.setChildChain(-1 /* childChain */, true /* enable */));
+        assertThrows(expected,
+                () -> mBpfNetMaps.setChildChain(1000 /* childChain */, true /* enable */));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+    }
+
+    private void checkUidOwnerValue(final long uid, final long expectedIif,
+            final long expectedMatch) throws Exception {
+        final UidOwnerValue config = mUidOwnerMap.getValue(new U32(uid));
+        if (expectedMatch == 0) {
+            assertNull(config);
+        } else {
+            assertEquals(expectedIif, config.iif);
+            assertEquals(expectedMatch, config.rule);
+        }
+    }
+
+    private void doTestRemoveNaughtyApp(final long iif, final long match) throws Exception {
+        mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+        mBpfNetMaps.removeNaughtyApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match & ~PENALTY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyApp() throws Exception {
+        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH);
+
+        // PENALTY_BOX_MATCH with other matches
+        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+        // PENALTY_BOX_MATCH with IIF_MATCH
+        doTestRemoveNaughtyApp(TEST_IF_INDEX, PENALTY_BOX_MATCH | IIF_MATCH);
+
+        // PENALTY_BOX_MATCH is not enabled
+        doTestRemoveNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyAppMissingUid() {
+        // UidOwnerMap does not have entry for TEST_UID
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+    }
+
+    private void doTestAddNaughtyApp(final long iif, final long match) throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.addNaughtyApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match | PENALTY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddNaughtyApp() throws Exception {
+        doTestAddNaughtyApp(NO_IIF, NO_MATCH);
+
+        // Other matches are enabled
+        doTestAddNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+        // IIF_MATCH is enabled
+        doTestAddNaughtyApp(TEST_IF_INDEX, IIF_MATCH);
+
+        // PENALTY_BOX_MATCH is already enabled
+        doTestAddNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testAddNaughtyAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.addNaughtyApp(TEST_UID));
+    }
+
+    private void doTestRemoveNiceApp(final long iif, final long match) throws Exception {
+        mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+        mBpfNetMaps.removeNiceApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match & ~HAPPY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceApp() throws Exception {
+        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH);
+
+        // HAPPY_BOX_MATCH with other matches
+        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+        // HAPPY_BOX_MATCH with IIF_MATCH
+        doTestRemoveNiceApp(TEST_IF_INDEX, HAPPY_BOX_MATCH | IIF_MATCH);
+
+        // HAPPY_BOX_MATCH is not enabled
+        doTestRemoveNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceAppMissingUid() {
+        // UidOwnerMap does not have entry for TEST_UID
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+    }
+
+    private void doTestAddNiceApp(final long iif, final long match) throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.addNiceApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match | HAPPY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddNiceApp() throws Exception {
+        doTestAddNiceApp(NO_IIF, NO_MATCH);
+
+        // Other matches are enabled
+        doTestAddNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+        // IIF_MATCH is enabled
+        doTestAddNiceApp(TEST_IF_INDEX, IIF_MATCH);
+
+        // HAPPY_BOX_MATCH is already enabled
+        doTestAddNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testAddNiceAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.addNiceApp(TEST_UID));
+    }
+
+    private void doTestUpdateUidLockdownRule(final long iif, final long match, final boolean add)
+            throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.updateUidLockdownRule(TEST_UID, add);
+
+        final long expectedMatch = add ? match | LOCKDOWN_VPN_MATCH : match & ~LOCKDOWN_VPN_MATCH;
+        checkUidOwnerValue(TEST_UID, iif, expectedMatch);
+    }
+
+    private static final boolean ADD = true;
+    private static final boolean REMOVE = false;
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleAddLockdown() throws Exception {
+        doTestUpdateUidLockdownRule(NO_IIF, NO_MATCH, ADD);
+
+        // Other matches are enabled
+        doTestUpdateUidLockdownRule(
+                NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, ADD);
+
+        // IIF_MATCH is enabled
+        doTestUpdateUidLockdownRule(TEST_IF_INDEX, DOZABLE_MATCH, ADD);
+
+        // LOCKDOWN_VPN_MATCH is already enabled
+        doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH | DOZABLE_MATCH, ADD);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleRemoveLockdown() throws Exception {
+        doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH with other matches
+        doTestUpdateUidLockdownRule(
+                NO_IIF, LOCKDOWN_VPN_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH with IIF_MATCH
+        doTestUpdateUidLockdownRule(TEST_IF_INDEX, LOCKDOWN_VPN_MATCH | IIF_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH is not enabled
+        doTestUpdateUidLockdownRule(NO_IIF, POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.updateUidLockdownRule(TEST_UID, true /* add */));
+    }
 }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 4c76803..0919dfc 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -51,6 +51,17 @@
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -104,6 +115,10 @@
 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.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;
@@ -136,6 +151,8 @@
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_PROFILE;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_VPN;
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -159,8 +176,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -194,6 +211,7 @@
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
 import android.app.usage.NetworkStatsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -338,10 +356,12 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.networkstack.apishim.NetworkAgentConfigShimImpl;
+import com.android.networkstack.apishim.api29.ConstantsShim;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
@@ -464,6 +484,9 @@
     private static final int TEST_APP_ID_2 = 104;
     private static final int TEST_WORK_PROFILE_APP_UID_2 =
             UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+    private static final int TEST_APP_ID_3 = 105;
+    private static final int TEST_APP_ID_4 = 106;
+    private static final int TEST_APP_ID_5 = 107;
 
     private static final String CLAT_PREFIX = "v4-";
     private static final String MOBILE_IFNAME = "test_rmnet_data0";
@@ -539,7 +562,9 @@
     @Mock NetworkPolicyManager mNetworkPolicyManager;
     @Mock VpnProfileStore mVpnProfileStore;
     @Mock SystemConfigManager mSystemConfigManager;
+    @Mock DevicePolicyManager mDevicePolicyManager;
     @Mock Resources mResources;
+    @Mock ClatCoordinator mClatCoordinator;
     @Mock PacProxyManager mPacProxyManager;
     @Mock BpfNetMaps mBpfNetMaps;
     @Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
@@ -642,7 +667,8 @@
         @Override
         public ComponentName startService(Intent service) {
             final String action = service.getAction();
-            if (!VpnConfig.SERVICE_INTERFACE.equals(action)) {
+            if (!VpnConfig.SERVICE_INTERFACE.equals(action)
+                    && !ConstantsShim.ACTION_VPN_MANAGER_EVENT.equals(action)) {
                 fail("Attempt to start unknown service, action=" + action);
             }
             return new ComponentName(service.getPackage(), "com.android.test.Service");
@@ -659,6 +685,7 @@
             if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
             if (Context.ETHERNET_SERVICE.equals(name)) return mEthernetManager;
             if (Context.NETWORK_POLICY_SERVICE.equals(name)) return mNetworkPolicyManager;
+            if (Context.DEVICE_POLICY_SERVICE.equals(name)) return mDevicePolicyManager;
             if (Context.SYSTEM_CONFIG_SERVICE.equals(name)) return mSystemConfigManager;
             if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
             if (Context.BATTERY_STATS_SERVICE.equals(name)) return mBatteryStatsManager;
@@ -687,6 +714,14 @@
             doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier()));
         }
 
+        public void setDeviceOwner(@NonNull final UserHandle userHandle, String value) {
+            // This relies on all contexts for a given user returning the same UM mock
+            final DevicePolicyManager dpmMock = createContextAsUser(userHandle, 0 /* flags */)
+                    .getSystemService(DevicePolicyManager.class);
+            doReturn(value).when(dpmMock).getDeviceOwner();
+            doReturn(value).when(mDevicePolicyManager).getDeviceOwner();
+        }
+
         @Override
         public ContentResolver getContentResolver() {
             return mContentResolver;
@@ -781,6 +816,32 @@
         }
     }
 
+    // This was only added in the T SDK, but this test needs to build against the R+S SDKs, too.
+    private static int toSdkSandboxUid(int appUid) {
+        final int firstSdkSandboxUid = 20000;
+        return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
+    }
+
+    // This function assumes the UID range for user 0 ([1, 99999])
+    private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
+        int start = 1;
+        Arrays.sort(excludedUids);
+        List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+        for (int excludedUid : excludedUids) {
+            if (excludedUid == start) {
+                start++;
+            } else {
+                parcels.add(new UidRangeParcel(start, excludedUid - 1));
+                start = excludedUid + 1;
+            }
+        }
+        if (start <= 99999) {
+            parcels.add(new UidRangeParcel(start, 99999));
+        }
+
+        return parcels.toArray(new UidRangeParcel[0]);
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
         waitForIdle(mCellNetworkAgent, TIMEOUT_MS);
@@ -2004,6 +2065,11 @@
             return mBpfNetMaps;
         }
 
+        @Override
+        public ClatCoordinator getClatCoordinator(INetd netd) {
+            return mClatCoordinator;
+        }
+
         final ArrayTrackRecord<Pair<String, Long>> mRateLimitHistory = new ArrayTrackRecord<>();
         final Map<String, Long> mActiveRateLimit = new HashMap<>();
 
@@ -5851,7 +5917,7 @@
     }
 
     /**
-     * Validate the callback flow CBS request without carrier privilege.
+     * Validate the service throws if request with CBS but without carrier privilege.
      */
     @Test
     public void testCBSRequestWithoutCarrierPrivilege() throws Exception {
@@ -5860,10 +5926,8 @@
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
 
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
-        // Now file the test request and expect it.
-        mCm.requestNetwork(nr, networkCallback);
-        networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
-        mCm.unregisterNetworkCallback(networkCallback);
+        // Now file the test request and expect the service throws.
+        assertThrows(SecurityException.class, () -> mCm.requestNetwork(nr, networkCallback));
     }
 
     private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
@@ -8989,10 +9053,16 @@
                 allowList);
         waitForIdle();
 
-        UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1);
-        UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999);
+        final Set<Integer> excludedUids = new ArraySet<Integer>();
+        excludedUids.add(VPN_UID);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(VPN_UID));
+        }
+        final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
         InOrder inOrder = inOrder(mMockNetd);
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
 
         // Connect a network when lockdown is active, expect to see it blocked.
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -9016,7 +9086,7 @@
         vpnUidCallback.assertNoCallback();
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
-        expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcels);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9033,13 +9103,14 @@
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
 
-        // The following requires that the UID of this test package is greater than VPN_UID. This
-        // is always true in practice because a plain AOSP build with no apps installed has almost
-        // 200 packages installed.
-        final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1);
-        final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1);
-        final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999);
-        expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3);
+        excludedUids.add(uid);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(uid));
+        }
+        final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9065,12 +9136,12 @@
         // Everything should now be blocked.
         mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
         allowList.clear();
         mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
                 allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
         defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
         assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
         vpnUidCallback.assertNoCallback();
@@ -9442,6 +9513,128 @@
         b2.expectBroadcast();
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testLockdownSetFirewallUidRule() throws Exception {
+        final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+        // Enable Lockdown
+        mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
+        waitForIdle();
+
+        // Lockdown rule is set to apps uids
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+
+        reset(mBpfNetMaps);
+
+        // Disable lockdown
+        mCm.setRequireVpnForUids(false /* requireVPN */, lockdownRange);
+        waitForIdle();
+
+        // Lockdown rule is removed from apps uids
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
+
+        // Interface rules are not changed by Lockdown mode enable/disable
+        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+    }
+
+    private void doTestSetUidFirewallRule(final int chain, final int defaultRule) {
+        final int uid = 1001;
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_ALLOW);
+        verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_ALLOW);
+        reset(mBpfNetMaps);
+
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DENY);
+        verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_DENY);
+        reset(mBpfNetMaps);
+
+        mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DEFAULT);
+        verify(mBpfNetMaps).setUidRule(chain, uid, defaultRule);
+        reset(mBpfNetMaps);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testSetUidFirewallRule() throws Exception {
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testSetFirewallChainEnabled() throws Exception {
+        final List<Integer> firewallChains = Arrays.asList(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED,
+                FIREWALL_CHAIN_LOW_POWER_STANDBY,
+                FIREWALL_CHAIN_OEM_DENY_1,
+                FIREWALL_CHAIN_OEM_DENY_2,
+                FIREWALL_CHAIN_OEM_DENY_3);
+        for (final int chain: firewallChains) {
+            mCm.setFirewallChainEnabled(chain, true /* enabled */);
+            verify(mBpfNetMaps).setChildChain(chain, true /* enable */);
+            reset(mBpfNetMaps);
+
+            mCm.setFirewallChainEnabled(chain, false /* enabled */);
+            verify(mBpfNetMaps).setChildChain(chain, false /* enable */);
+            reset(mBpfNetMaps);
+        }
+    }
+
+    private void doTestReplaceFirewallChain(final int chain, final String chainName,
+            final boolean allowList) {
+        final int[] uids = new int[] {1001, 1002};
+        mCm.replaceFirewallChain(chain, uids);
+        verify(mBpfNetMaps).replaceUidChain(chainName, allowList, uids);
+        reset(mBpfNetMaps);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testReplaceFirewallChain() {
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_DOZABLE, "fw_dozable", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_STANDBY, "fw_standby", false);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE, "fw_powersave",  true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED, "fw_restricted", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY, "fw_low_power_standby", true);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1, "fw_oem_deny_1", false);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2, "fw_oem_deny_2", false);
+        doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_3, "fw_oem_deny_3", false);
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testInvalidFirewallChain() throws Exception {
+        final int uid = 1001;
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(-1 /* chain */, uid, FIREWALL_RULE_ALLOW));
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(100 /* chain */, uid, FIREWALL_RULE_ALLOW));
+        assertThrows(expected, () -> mCm.replaceFirewallChain(-1 /* chain */, new int[]{uid}));
+        assertThrows(expected, () -> mCm.replaceFirewallChain(100 /* chain */, new int[]{uid}));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testInvalidFirewallRule() throws Exception {
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+                        1001 /* uid */, -1 /* rule */));
+        assertThrows(expected,
+                () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+                        1001 /* uid */, 100 /* rule */));
+    }
+
     /**
      * Test mutable and requestable network capabilities such as
      * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -9569,6 +9762,59 @@
         return event;
     }
 
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private <T> T verifyNeverWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t, never());
+        } else {
+            return verify(t, never());
+        }
+    }
+
+    private void verifyClatdStart(@Nullable InOrder inOrder, @NonNull String iface, int netId,
+            @NonNull String nat64Prefix) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator)
+                .clatStart(eq(iface), eq(netId), eq(new IpPrefix(nat64Prefix)));
+        } else {
+            verifyWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), eq(nat64Prefix));
+        }
+    }
+
+    private void verifyNeverClatdStart(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyNeverWithOrder(inOrder, mClatCoordinator).clatStart(eq(iface), anyInt(), any());
+        } else {
+            verifyNeverWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), anyString());
+        }
+    }
+
+    private void verifyClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+        }
+    }
+
+    private void verifyNeverClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+            throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyNeverWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyNeverWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+        }
+    }
+
     @Test
     public void testStackedLinkProperties() throws Exception {
         final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -9601,6 +9847,7 @@
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         reset(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Connect with ipv6 link properties. Expect prefix discovery to be started.
         mCellNetworkAgent.connect(true);
@@ -9639,8 +9886,10 @@
                 && ri.iface != null && ri.iface.startsWith("v4-")));
 
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
         reset(mMockDnsResolver);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9661,7 +9910,7 @@
                 CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp();
         assertEquals(0, lpBeforeClat.getStackedLinks().size());
         assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix());
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9693,6 +9942,7 @@
                     new int[] { TRANSPORT_CELLULAR })));
         }
         reset(mMockNetd);
+        reset(mClatCoordinator);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
         // Change the NAT64 prefix without first removing it.
@@ -9701,11 +9951,12 @@
                 cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96));
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 0);
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         assertRoutesRemoved(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId,
+                kOtherNat64Prefix.toString());
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix));
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9714,6 +9965,7 @@
         assertRoutesAdded(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked
         // linkproperties are cleaned up.
@@ -9722,7 +9974,7 @@
         mCellNetworkAgent.sendLinkProperties(cellLp);
         networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
         assertRoutesAdded(cellNetId, ipv4Subnet);
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId);
 
         // As soon as stop is called, the linkproperties lose the stacked interface.
@@ -9739,8 +9991,10 @@
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
+        reset(mClatCoordinator);
         reset(mMockDnsResolver);
         doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
                 .interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9762,7 +10016,7 @@
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
                 cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
         networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
-        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9779,7 +10033,7 @@
         assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault);
 
         // Stop has no effect because clat is already stopped.
-        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 0);
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
@@ -9792,7 +10046,9 @@
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Test disconnecting a network that is running 464xlat.
 
@@ -9809,7 +10065,7 @@
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
 
         // Clatd is started and clat iface comes up. Expect stacked link to be added.
-        verify(mMockNetd).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
         clat = getNat464Xlat(mCellNetworkAgent);
         clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
@@ -9819,16 +10075,18 @@
         // assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
         reset(mMockNetd);
+        reset(mClatCoordinator);
 
         // Disconnect the network. clat is stopped and the network is destroyed.
         mCellNetworkAgent.disconnect();
         networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         networkCallback.assertNoCallback();
-        verify(mMockNetd).clatdStop(MOBILE_IFNAME);
+        verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
         verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mClatCoordinator);
 
         mCm.unregisterNetworkCallback(networkCallback);
     }
@@ -9859,7 +10117,7 @@
         baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
 
         reset(mMockNetd, mMockDnsResolver);
-        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver, mClatCoordinator);
 
         // If a network already has a NAT64 prefix on connect, clatd is started immediately and
         // prefix discovery is never started.
@@ -9870,7 +10128,7 @@
         final Network network = mWiFiNetworkAgent.getNetwork();
         int netId = network.getNetId();
         callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         callback.assertNoCallback();
@@ -9880,7 +10138,7 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
 
@@ -9889,7 +10147,7 @@
         lp.setNat64Prefix(pref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
 
@@ -9898,22 +10156,22 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
 
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
 
         // If an RA advertises the same prefix that was discovered by DNS, nothing happens: prefix
         // discovery is not stopped, and there are no callbacks.
         lp.setNat64Prefix(pref64FromDns);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9922,8 +10180,8 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9932,14 +10190,14 @@
         lp.setNat64Prefix(pref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
 
         // Stopping prefix discovery results in a prefix removed notification.
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_REMOVED, pref64FromDnsStr, 96));
 
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
 
@@ -9947,9 +10205,9 @@
         lp.setNat64Prefix(newPref64FromRa);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
-        inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+        verifyClatdStart(inOrder, iface, netId, newPref64FromRa.toString());
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString());
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
@@ -9959,8 +10217,8 @@
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
         assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9972,20 +10230,20 @@
         lp.setNat64Prefix(null);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
         inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
         expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
-        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+        verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), any());
 
         lp.setNat64Prefix(pref64FromDns);
         mWiFiNetworkAgent.sendLinkProperties(lp);
         callback.assertNoCallback();
-        inOrder.verify(mMockNetd, never()).clatdStop(iface);
-        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        verifyNeverClatdStop(inOrder, iface);
+        verifyNeverClatdStart(inOrder, iface);
         inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9998,7 +10256,7 @@
         callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
         b.expectBroadcast();
 
-        inOrder.verify(mMockNetd).clatdStop(iface);
+        verifyClatdStop(inOrder, iface);
         inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
         inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
 
@@ -10248,7 +10506,7 @@
         verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
         assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
         assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
-        assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
+        assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
 
         mMockVpn.disconnect();
         waitForIdle();
@@ -10256,38 +10514,70 @@
         // Disconnected VPN should have interface rules removed
         verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
         assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
-        assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
+    }
+
+    private void checkInterfaceFilteringRuleWithNullInterface(final LinkProperties lp,
+            final int uid) throws Exception {
+        // The uid range needs to cover the test app so the network is visible to it.
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.establish(lp, uid, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, uid);
+
+        if (SdkLevel.isAtLeastT()) {
+            // On T and above, VPN should have rules for null interface. Null Interface is a
+            // wildcard and this accepts traffic from all the interfaces.
+            // There are two expected invocations, one during the VPN initial
+            // connection, one during the VPN LinkProperties update.
+            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+                    eq(null) /* iface */, uidCaptor.capture());
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            }
+            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+                    vpnRange);
+
+            mMockVpn.disconnect();
+            waitForIdle();
+
+            // Disconnected VPN should have interface rules removed
+            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            }
+            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+        } else {
+            // Before T, rules are not configured for null interface.
+            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        }
     }
 
     @Test
-    public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
-        // The uid range needs to cover the test app so the network is visible to it.
-        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
-        // Legacy VPN should not have interface rules set up
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        // Legacy VPN should have interface filtering with null interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, Process.SYSTEM_UID);
     }
 
     @Test
-    public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
-            throws Exception {
+    public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
-        // The uid range needs to cover the test app so the network is visible to it.
-        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
-        // IPv6 unreachable route should not be misinterpreted as a default route
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        // VPN that does not provide a default route should have interface filtering with null
+        // interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, VPN_UID);
     }
 
     @Test
@@ -10343,19 +10633,6 @@
     }
 
     @Test
-    public void testStartVpnProfileFromDiffPackage() throws Exception {
-        final String notMyVpnPkg = "com.not.my.vpn";
-        assertThrows(
-                SecurityException.class, () -> mVpnManagerService.startVpnProfile(notMyVpnPkg));
-    }
-
-    @Test
-    public void testStopVpnProfileFromDiffPackage() throws Exception {
-        final String notMyVpnPkg = "com.not.my.vpn";
-        assertThrows(SecurityException.class, () -> mVpnManagerService.stopVpnProfile(notMyVpnPkg));
-    }
-
-    @Test
     public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
@@ -11492,6 +11769,12 @@
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
+    private void verifyDump(String[] args) {
+        final StringWriter stringWriter = new StringWriter();
+        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), args);
+        assertFalse(stringWriter.toString().isEmpty());
+    }
+
     @Test
     public void testDumpDoesNotCrash() {
         mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
@@ -11504,11 +11787,26 @@
                 .addTransportType(TRANSPORT_WIFI).build();
         mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
         mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
-        final StringWriter stringWriter = new StringWriter();
 
-        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+        verifyDump(new String[0]);
 
-        assertFalse(stringWriter.toString().isEmpty());
+        // Verify dump with arguments.
+        final String dumpPrio = "--dump-priority";
+        final String[] dumpArgs = {dumpPrio};
+        verifyDump(dumpArgs);
+
+        final String[] highDumpArgs = {dumpPrio, "HIGH"};
+        verifyDump(highDumpArgs);
+
+        final String[] normalDumpArgs = {dumpPrio, "NORMAL"};
+        verifyDump(normalDumpArgs);
+
+        // Invalid args should do dumpNormal w/o exception
+        final String[] unknownDumpArgs = {dumpPrio, "UNKNOWN"};
+        verifyDump(unknownDumpArgs);
+
+        final String[] invalidDumpArgs = {"UNKNOWN"};
+        verifyDump(invalidDumpArgs);
     }
 
     @Test
@@ -11852,16 +12150,14 @@
         mQosCallbackMockHelper.registerQosCallback(
                 mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
 
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 =
-                (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister)
-                        wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbRegister1);
 
         final int registerCallbackId = cbRegister1.mQosCallbackId;
         mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback);
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
-        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
-                wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackUnregister cbUnregister =
+                (OnQosCallbackUnregister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbUnregister);
         assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
         assertNull(wrapper.getCallbackHistory().poll(200, x -> true));
@@ -11940,6 +12236,86 @@
                         && session.getSessionType() == QosSession.TYPE_NR_BEARER));
     }
 
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackAvailableOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper;
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbRegister1);
+        final int registerCallbackId = cbRegister1.mQosCallbackId;
+
+        waitForIdle();
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                1, 2, 3, 4, 5, new ArrayList<>());
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+        waitForIdle();
+
+        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
+        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
+                wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbUnregister);
+        assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackLostOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+        EpsBearerQosSessionAttributes attributes =
+                sendQosSessionEvent(qosCallbackId, sessionId, true);
+        waitForIdle();
+
+        verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes));
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+
+        sendQosSessionEvent(qosCallbackId, sessionId, false);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    private EpsBearerQosSessionAttributes sendQosSessionEvent(
+            int qosCallbackId, int sessionId, boolean available) {
+        if (available) {
+            final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                    1, 2, 3, 4, 5, new ArrayList<>());
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+            return attributes;
+        } else {
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER);
+            return null;
+        }
+
+    }
+
     @Test
     public void testQosCallbackTooManyRequests() throws Exception {
         mQosCallbackMockHelper = new QosCallbackMockHelper();
@@ -14463,7 +14839,7 @@
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
-                NetworkCapabilities.NET_ENTERPRISE_ID_2);
+                NET_ENTERPRISE_ID_2);
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), true,
@@ -14488,6 +14864,393 @@
     }
 
     /**
+     * Make sure per-profile networking preference throws exception when default preference
+     * is set along with enterprise preference.
+     */
+    @Test
+    public void testPreferenceWithInvalidPreferenceDefaultAndEnterpriseTogether()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder1.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder2.build(),
+                                profileNetworkPreferenceBuilder1.build()),
+                        r -> r.run(), listener));
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when two slices with
+     * two different apps within same user profile is configured
+     * Make sure per profile network preferences overrides with latest preference when
+     * same user preference is set twice
+     */
+    @Test
+    public void testSetPreferenceWithOverridingPreference()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        // Set preferences for testHandle to map testWorkProfileAppUid1 to
+        // NET_ENTERPRISE_ID_1 and testWorkProfileAppUid2 to NET_ENTERPRISE_ID_2.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+
+        // Set preferences for testHandle to map testWorkProfileAppUid3 to
+        // to NET_ENTERPRISE_ID_1.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder3.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb3.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb2);
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        workAgent2.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when multiple slices with
+     * multiple different apps within same user profile is configured.
+     */
+    @Test
+    public void testSetPreferenceWithMultiplePreferences()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+        final TestNetworkCallback appCb4 = new TestNetworkCallback();
+        final TestNetworkCallback appCb5 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+        final int testWorkProfileAppUid4 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_4);
+        final int testWorkProfileAppUid5 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_5);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+        registerDefaultNetworkCallbackAsUid(appCb4, testWorkProfileAppUid4);
+        registerDefaultNetworkCallbackAsUid(appCb5, testWorkProfileAppUid5);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        final TestNetworkAgentWrapper workAgent3 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_3);
+        final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
+        final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
+
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+        workAgent3.connect(true);
+        workAgent4.connect(true);
+        workAgent5.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb4.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb5.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent3.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent4.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent5.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_3);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder4 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder4.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder4.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_4);
+        profileNetworkPreferenceBuilder4.setIncludedUids(new int[]{testWorkProfileAppUid4});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder5 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder5.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder5.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_5);
+        profileNetworkPreferenceBuilder5.setIncludedUids(new int[]{testWorkProfileAppUid5});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build(),
+                        profileNetworkPreferenceBuilder3.build(),
+                        profileNetworkPreferenceBuilder4.build(),
+                        profileNetworkPreferenceBuilder5.build()),
+                r -> r.run(), listener);
+
+        listener.expectOnComplete();
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent3.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent4.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent5.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+        appCb3.expectAvailableCallbacksValidated(workAgent3);
+        appCb4.expectAvailableCallbacksValidated(workAgent4);
+        appCb5.expectAvailableCallbacksValidated(workAgent5);
+
+        workAgent1.disconnect();
+        workAgent2.disconnect();
+        workAgent3.disconnect();
+        workAgent4.disconnect();
+        workAgent5.disconnect();
+
+        appCb1.expectCallback(CallbackEntry.LOST, workAgent1);
+        appCb2.expectCallback(CallbackEntry.LOST, workAgent2);
+        appCb3.expectCallback(CallbackEntry.LOST, workAgent3);
+        appCb4.expectCallback(CallbackEntry.LOST, workAgent4);
+        appCb5.expectCallback(CallbackEntry.LOST, workAgent5);
+
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb2.assertNoCallback();
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.assertNoCallback();
+        appCb5.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        mSystemDefaultNetworkCallback.assertNoCallback();
+        mDefaultNetworkCallback.assertNoCallback();
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb3,
+                appCb5);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        mCm.unregisterNetworkCallback(appCb4);
+        mCm.unregisterNetworkCallback(appCb5);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
      * Test that, in a given networking context, calling setPreferenceForUser to set per-profile
      * defaults on then off works as expected.
      */
@@ -14657,12 +15420,42 @@
     public void testProfileNetworkPrefWrongProfile() throws Exception {
         final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
         mServiceContext.setWorkProfile(testHandle, false);
-        assertThrows("Should not be able to set a user pref for a non-work profile",
+        mServiceContext.setDeviceOwner(testHandle, null);
+        assertThrows("Should not be able to set a user pref for a non-work profile "
+                + "and non device owner",
                 IllegalArgumentException.class , () ->
                         mCm.setProfileNetworkPreference(testHandle,
                                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null));
     }
 
+    /**
+     * Make sure requests for per-profile default networking for a device owner is
+     * accepted on T and not accepted on S
+     */
+    @Test
+    public void testProfileNetworkDeviceOwner() throws Exception {
+        final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+        mServiceContext.setWorkProfile(testHandle, false);
+        mServiceContext.setDeviceOwner(testHandle, "deviceOwnerPackage");
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        if (SdkLevel.isAtLeastT()) {
+            mCm.setProfileNetworkPreferences(testHandle,
+                    List.of(profileNetworkPreferenceBuilder.build()),
+                    r -> r.run(), listener);
+        } else {
+            // S should not allow setting preference on device owner
+            assertThrows("Should not be able to set a user pref for a non-work profile on S",
+                    IllegalArgumentException.class , () ->
+                            mCm.setProfileNetworkPreferences(testHandle,
+                                    List.of(profileNetworkPreferenceBuilder.build()),
+                                    r -> r.run(), listener));
+        }
+    }
+
     @Test
     public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
@@ -14822,6 +15615,27 @@
     }
 
     @Test
+    public void testAutomotiveEthernetAllowedUids() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+        mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+
+        // In this test the automotive feature will be enabled.
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+        // Simulate a restricted ethernet network.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_ETHERNET)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mEthernetNetworkAgent, TRANSPORT_ETHERNET, agentNetCaps, true);
+    }
+
+    @Test
     public void testCbsAllowedUids() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
         mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
@@ -14830,6 +15644,24 @@
         doReturn(true).when(mCarrierPrivilegeAuthenticator)
                 .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
 
+        // Simulate a restricted telephony network. The telephony factory is entitled to set
+        // the access UID to the service package on any of its restricted networks.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mCellNetworkAgent, TRANSPORT_CELLULAR, agentNetCaps, false);
+    }
+
+    private void validateAllowedUids(final TestNetworkAgentWrapper testAgent,
+            @NetworkCapabilities.Transport final int transportUnderTest,
+            final NetworkCapabilities.Builder ncb, final boolean forAutomotive) throws Exception {
         final ArraySet<Integer> serviceUidSet = new ArraySet<>();
         serviceUidSet.add(TEST_PACKAGE_UID);
         final ArraySet<Integer> nonServiceUidSet = new ArraySet<>();
@@ -14840,40 +15672,34 @@
 
         final TestNetworkCallback cb = new TestNetworkCallback();
 
-        // Simulate a restricted telephony network. The telephony factory is entitled to set
-        // the access UID to the service package on any of its restricted networks.
-        final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
-
+        /* Test setting UIDs */
         // Cell gets to set the service UID as access UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
+                .addTransportType(transportUnderTest)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
-                new LinkProperties(), ncb.build());
-        mCellNetworkAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        testAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(testAgent);
         ncb.setAllowedUids(serviceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
+        /* Test setting UIDs is rejected when expected */
+        if (forAutomotive) {
+            mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        }
+
         // ...but not to some other UID. Rejection sets UIDs to the empty set
         ncb.setAllowedUids(nonServiceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
@@ -14882,18 +15708,18 @@
 
         // ...and also not to multiple UIDs even including the service UID
         ncb.setAllowedUids(serviceUidSetPlus);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
 
-        mCellNetworkAgent.disconnect();
-        cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        testAgent.disconnect();
+        cb.expectCallback(CallbackEntry.LOST, testAgent);
         mCm.unregisterNetworkCallback(cb);
 
         // Must be unset before touching the transports, because remove and add transport types
         // check the specifier on the builder immediately, contradicting normal builder semantics
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
-        ncb.removeTransportType(TRANSPORT_CELLULAR);
+        ncb.removeTransportType(transportUnderTest);
         ncb.addTransportType(TRANSPORT_WIFI);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 45f3d3c..9401d47 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -89,7 +89,7 @@
 public class IpSecServiceParameterizedTest {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
-            Build.VERSION_CODES.R /* ignoreClassUpTo */);
+            Build.VERSION_CODES.S_V2 /* ignoreClassUpTo */);
 
     private static final int TEST_SPI = 0xD1201D;
 
@@ -783,6 +783,23 @@
     }
 
     @Test
+    public void testSetNetworkForTunnelInterfaceFailsForNullLp() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final Network newFakeNetwork = new Network(1000);
+        final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+
+        try {
+            mIpSecService.setNetworkForTunnelInterface(
+                    tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+            fail(
+                    "Expected an IllegalArgumentException for underlying network with null"
+                            + " LinkProperties");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
     public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception {
         final IpSecTunnelInterfaceResponse createTunnelResp =
                 createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
index 5c7ca6f..8595ab9 100644
--- a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
@@ -54,7 +54,7 @@
 /** Unit tests for {@link IpSecService.RefcountedResource}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecServiceRefcountedResourceTest {
     Context mMockContext;
     IpSecService.Dependencies mMockDeps;
diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java
index 7e6b157..6955620 100644
--- a/tests/unit/java/com/android/server/IpSecServiceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceTest.java
@@ -75,7 +75,7 @@
 /** Unit tests for {@link IpSecService}. */
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpSecServiceTest {
 
     private static final int DROID_SPI = 0xD1201D;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 3c228d0..9365bee 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -19,10 +19,13 @@
 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -36,6 +39,12 @@
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.ResolutionInfo;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
 import android.net.nsd.MDnsManager;
@@ -63,6 +72,7 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -74,7 +84,7 @@
 //  - test NSD_ON ENABLE/DISABLED listening
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
@@ -114,6 +124,10 @@
         doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
                 .getSystemServiceName(MDnsManager.class);
         doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
+        if (mContext.getSystemService(MDnsManager.class) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
+        }
         doReturn(true).when(mMockMDnsM).registerService(
                 anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
         doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
@@ -275,6 +289,105 @@
         verify(mMockMDnsM, never()).stopDaemon();
     }
 
+    @Test
+    public void testDiscoverOnTetheringDownstream() throws Exception {
+        NsdService service = makeService();
+        NsdManager client = connectClient(service);
+
+        final String serviceType = "a_type";
+        final String serviceName = "a_name";
+        final String domainName = "mytestdevice.local";
+        final int interfaceIdx = 123;
+        final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
+        client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
+        waitForIdle();
+
+        final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(IMDnsEventListener.class);
+        verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
+        final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
+                eq(0) /* interfaceIdx */);
+        // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+        // this needs to use a timeout
+        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
+
+        final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+                discIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_FOUND,
+                serviceName,
+                serviceType,
+                domainName,
+                interfaceIdx,
+                INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
+        final IMDnsEventListener eventListener = listenerCaptor.getValue();
+        eventListener.onServiceDiscoveryStatus(discoveryInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
+        final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
+        assertEquals(serviceName, foundInfo.getServiceName());
+        assertEquals(serviceType, foundInfo.getServiceType());
+        assertNull(foundInfo.getHost());
+        assertNull(foundInfo.getNetwork());
+        assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+        // After discovering the service, verify resolving it
+        final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
+        client.resolveService(foundInfo, resolveListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
+                eq("local.") /* domain */, eq(interfaceIdx));
+
+        final int servicePort = 10123;
+        final String serviceFullName = serviceName + "." + serviceType;
+        final ResolutionInfo resolutionInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLVED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                serviceFullName,
+                domainName,
+                servicePort,
+                new byte[0] /* txtRecord */,
+                interfaceIdx);
+
+        doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+        eventListener.onServiceResolutionStatus(resolutionInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
+                eq(interfaceIdx));
+
+        final String serviceAddress = "192.0.2.123";
+        final GetAddressInfo addressInfo = new GetAddressInfo(
+                getAddrIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+                serviceFullName,
+                serviceAddress,
+                interfaceIdx,
+                INetd.LOCAL_NET_ID);
+        eventListener.onGettingServiceAddressStatus(addressInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
+        final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
+        assertEquals(serviceName, resolvedService.getServiceName());
+        assertEquals("." + serviceType, resolvedService.getServiceType());
+        assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+        assertEquals(servicePort, resolvedService.getPort());
+        assertNull(resolvedService.getNetwork());
+        assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
new file mode 100644
index 0000000..c814cc5
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import static com.android.testutils.ContextUtils.mockService;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.UserIdInt;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.connectivity.Vpn;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(R) // VpnManagerService is not available before R
+@SmallTest
+public class VpnManagerServiceTest extends VpnTestBase {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private static final int TIMEOUT_MS = 2_000;
+
+    @Mock Context mContext;
+    @Mock Context mSystemContext;
+    @Mock Context mUserAllContext;
+    private HandlerThread mHandlerThread;
+    @Mock private Vpn mVpn;
+    @Mock private INetworkManagementService mNms;
+    @Mock private ConnectivityManager mCm;
+    @Mock private UserManager mUserManager;
+    @Mock private INetd mNetd;
+    @Mock private PackageManager mPackageManager;
+
+    private VpnManagerServiceDependencies mDeps;
+    private VpnManagerService mService;
+    private BroadcastReceiver mUserPresentReceiver;
+    private BroadcastReceiver mIntentReceiver;
+    private final String mNotMyVpnPkg = "com.not.my.vpn";
+
+    class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
+        @Override
+        public HandlerThread makeHandlerThread() {
+            return mHandlerThread;
+        }
+
+        @Override
+        public INetworkManagementService getINetworkManagementService() {
+            return mNms;
+        }
+
+        @Override
+        public INetd getNetd() {
+            return mNetd;
+        }
+
+        @Override
+        public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
+                INetd netd, @UserIdInt int userId) {
+            return mVpn;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread("TestVpnManagerService");
+        mDeps = new VpnManagerServiceDependencies();
+        doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
+        doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        setMockedPackages(mPackageManager, sPackages);
+
+        mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
+        mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
+        doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
+
+        mService = new VpnManagerService(mContext, mDeps);
+        mService.systemReady();
+
+        final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mSystemContext).registerReceiver(
+                userPresentReceiverCaptor.capture(), any(), any(), any());
+        verify(mUserAllContext, times(2)).registerReceiver(
+                intentReceiverCaptor.capture(), any(), any(), any());
+        mUserPresentReceiver = userPresentReceiverCaptor.getValue();
+        mIntentReceiver = intentReceiverCaptor.getValue();
+
+        // Add user to create vpn in mVpn
+        onUserStarted(SYSTEM_USER_ID);
+        assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
+    }
+
+    @Test
+    public void testUpdateAppExclusionList() {
+        // Start vpn
+        mService.startVpnProfile(TEST_VPN_PKG);
+        verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
+
+        // Remove package due to package replaced.
+        onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+        // Add package due to package replaced.
+        onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+        // Remove package
+        onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+        verify(mVpn).refreshPlatformVpnAppExclusionList();
+
+        // Add the package back
+        onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+        verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
+    }
+
+    @Test
+    public void testStartVpnProfileFromDiffPackage() {
+        assertThrows(
+                SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testStopVpnProfileFromDiffPackage() {
+        assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testGetProvisionedVpnProfileStateFromDiffPackage() {
+        assertThrows(SecurityException.class, () ->
+                mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testGetProvisionedVpnProfileState() {
+        mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
+        verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
+    }
+
+    private Intent buildIntent(String action, String packageName, int userId, int uid,
+            boolean isReplacing) {
+        final Intent intent = new Intent(action);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+        intent.putExtra(Intent.EXTRA_UID, uid);
+        intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
+        if (packageName != null) {
+            intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
+        }
+
+        return intent;
+    }
+
+    private void sendIntent(Intent intent) {
+        final Handler h = mHandlerThread.getThreadHandler();
+
+        // Send in handler thread.
+        h.post(() -> mIntentReceiver.onReceive(mContext, intent));
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    private void onUserStarted(int userId) {
+        sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
+                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
+    }
+
+    private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
+        sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
+    }
+
+    private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
+        onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+    }
+
+    private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
+        sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
+                isReplacing));
+    }
+
+    private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
+        onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+    }
+
+    @Test
+    public void testReceiveIntentFromNonHandlerThread() {
+        assertThrows(IllegalStateException.class, () ->
+                mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
+                        PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
+
+        assertThrows(IllegalStateException.class, () ->
+                mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
+    }
+}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
new file mode 100644
index 0000000..6113872
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnTestBase.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
+public class VpnTestBase {
+    protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    /**
+     * Names and UIDs for some fake packages. Important points:
+     *  - UID is ordered increasing.
+     *  - One pair of packages have consecutive UIDs.
+     */
+    protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+    protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
+    // Mock packages
+    protected static final Map<String, Integer> sPackages = new ArrayMap<>();
+    static {
+        for (int i = 0; i < PKGS.length; i++) {
+            sPackages.put(PKGS[i], PKG_UIDS[i]);
+        }
+        sPackages.put(TEST_VPN_PKG, Process.myUid());
+    }
+
+    // Mock users
+    protected static final int SYSTEM_USER_ID = 0;
+    protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
+    protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
+            FLAG_ADMIN | FLAG_PRIMARY);
+    protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
+    protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
+            FLAG_RESTRICTED);
+    protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
+            FLAG_RESTRICTED);
+    protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
+            FLAG_MANAGED_PROFILE);
+    static {
+        RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
+        RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
+        MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
+    }
+
+    // Populate a fake packageName-to-UID mapping.
+    protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
+        try {
+            doAnswer(invocation -> {
+                final String appName = (String) invocation.getArguments()[0];
+                final int userId = (int) invocation.getArguments()[1];
+
+                final Integer appId = packages.get(appName);
+                if (appId == null) {
+                    throw new PackageManager.NameNotFoundException(appName);
+                }
+
+                return UserHandle.getUid(userId, appId);
+            }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
+        } catch (Exception e) {
+        }
+    }
+
+    protected List<Integer> toList(int[] arr) {
+        return Arrays.stream(arr).boxed().collect(Collectors.toList());
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index c3d64cb..feee293 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -30,12 +30,16 @@
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import android.annotation.NonNull;
 import android.net.INetd;
@@ -46,6 +50,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.bpf.ClatEgress4Key;
 import com.android.net.module.util.bpf.ClatEgress4Value;
@@ -53,6 +58,7 @@
 import com.android.net.module.util.bpf.ClatIngress6Value;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -64,6 +70,7 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.StringWriter;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.util.Objects;
@@ -101,17 +108,17 @@
     private static final int RAW_SOCK_FD = 535;
     private static final int PACKET_SOCK_FD = 536;
     private static final long RAW_SOCK_COOKIE = 27149;
-    private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
-            new FileDescriptor());
-    private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
-            new FileDescriptor());
-    private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
-            new FileDescriptor());
+    private static final ParcelFileDescriptor TUN_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
+    private static final ParcelFileDescriptor RAW_SOCK_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
+    private static final ParcelFileDescriptor PACKET_SOCK_PFD = spy(new ParcelFileDescriptor(
+            new FileDescriptor()));
 
     private static final String EGRESS_PROG_PATH =
-            "/sys/fs/bpf/prog_clatd_schedcls_egress4_clat_rawip";
+            "/sys/fs/bpf/net_shared/prog_clatd_schedcls_egress4_clat_rawip";
     private static final String INGRESS_PROG_PATH =
-            "/sys/fs/bpf/prog_clatd_schedcls_ingress6_clat_ether";
+            "/sys/fs/bpf/net_shared/prog_clatd_schedcls_ingress6_clat_ether";
     private static final ClatEgress4Key EGRESS_KEY = new ClatEgress4Key(STACKED_IFINDEX,
             INET4_LOCAL4);
     private static final ClatEgress4Value EGRESS_VALUE = new ClatEgress4Value(BASE_IFINDEX,
@@ -121,10 +128,13 @@
     private static final ClatIngress6Value INGRESS_VALUE = new ClatIngress6Value(STACKED_IFINDEX,
             INET4_LOCAL4);
 
+    private final TestBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap =
+            spy(new TestBpfMap<>(ClatIngress6Key.class, ClatIngress6Value.class));
+    private final TestBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap =
+            spy(new TestBpfMap<>(ClatEgress4Key.class, ClatEgress4Value.class));
+
     @Mock private INetd mNetd;
     @Spy private TestDependencies mDeps = new TestDependencies();
-    @Mock private IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
-    @Mock private IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
 
     /**
       * The dependency injection class is used to mock the JNI functions and system functions
@@ -505,4 +515,207 @@
         // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
         assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
     }
+
+    @Test
+    public void testDump() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final StringWriter stringWriter = new StringWriter();
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ");
+        coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        coordinator.dump(ipw);
+
+        final String[] dumpStrings = stringWriter.toString().split("\n");
+        assertEquals(6, dumpStrings.length);
+        assertEquals("CLAT tracker: iface: test0 (1000), v4iface: v4-test0 (1001), "
+                + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
+                + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
+        assertEquals("Forwarding rules:", dumpStrings[1].trim());
+        assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+                dumpStrings[2].trim());
+        assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+                dumpStrings[3].trim());
+        assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+                dumpStrings[4].trim());
+        assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+                dumpStrings[5].trim());
+    }
+
+    @Test
+    public void testNotStartClatWithInvalidPrefix() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final IpPrefix invalidPrefix = new IpPrefix("2001:db8::/64");
+        assertThrows(IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, invalidPrefix));
+    }
+
+    private void assertStartClat(final TestDependencies deps) throws Exception {
+        final ClatCoordinator coordinator = new ClatCoordinator(deps);
+        assertNotNull(coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+    }
+
+    private void assertNotStartClat(final TestDependencies deps) {
+        // Expect that the injection function of TestDependencies causes clatStart() failed.
+        final ClatCoordinator coordinator = new ClatCoordinator(deps);
+        assertThrows(IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+    }
+
+    private void checkNotStartClat(final TestDependencies deps, final boolean needToCloseTunFd,
+            final boolean needToClosePacketSockFd, final boolean needToCloseRawSockFd)
+            throws Exception {
+        // [1] Expect that modified TestDependencies can't start clatd.
+        // Use precise check to make sure that there is no unexpected file descriptor closing.
+        clearInvocations(TUN_PFD, RAW_SOCK_PFD, PACKET_SOCK_PFD);
+        assertNotStartClat(deps);
+        if (needToCloseTunFd) {
+            verify(TUN_PFD).close();
+        } else {
+            verify(TUN_PFD, never()).close();
+        }
+        if (needToClosePacketSockFd) {
+            verify(PACKET_SOCK_PFD).close();
+        } else {
+            verify(PACKET_SOCK_PFD, never()).close();
+        }
+        if (needToCloseRawSockFd) {
+            verify(RAW_SOCK_PFD).close();
+        } else {
+            verify(RAW_SOCK_PFD, never()).close();
+        }
+
+        // [2] Expect that unmodified TestDependencies can start clatd.
+        // Used to make sure that the above modified TestDependencies has really broken the
+        // clatd starting.
+        assertStartClat(new TestDependencies());
+    }
+
+    // The following testNotStartClat* tests verifies bunches of code for unwinding the
+    // failure if any.
+    @Test
+    public void testNotStartClatWithNativeFailureSelectIpv4Address() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureGenerateIpv6Address() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                    @NonNull String prefix64) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureCreateTunInterface() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int createTunInterface(@NonNull String tuniface) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureDetectMtu() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureOpenPacketSocket() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int openPacketSocket() throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureOpenRawSocket6() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int openRawSocket6(int mark) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureAddAnycastSetsockopt() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6,
+                    int ifindex) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureTagSocketAsClat() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public long tagSocketAsClat(@NonNull FileDescriptor sock) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureConfigurePacketSocket() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public void configurePacketSocket(@NonNull FileDescriptor sock, String v6,
+                    int ifindex) throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+    }
+
+    @Test
+    public void testNotStartClatWithNativeFailureStartClatd() throws Exception {
+        class FailureDependencies extends TestDependencies {
+            @Override
+            public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                    @NonNull FileDescriptor writesock6, @NonNull String iface,
+                    @NonNull String pfx96, @NonNull String v4, @NonNull String v6)
+                    throws IOException {
+                throw new IOException();
+            }
+        }
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index ec51537..d1bf40e 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -27,6 +27,7 @@
 
 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -184,7 +185,7 @@
                 (int) setting);
     }
 
-    private void testGetMultipathPreference(
+    private void prepareGetMultipathPreferenceTest(
             long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
             long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
 
@@ -286,7 +287,7 @@
 
     @Test
     public void testGetMultipathPreference_SubscriptionQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
                 DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
@@ -301,7 +302,7 @@
 
     @Test
     public void testGetMultipathPreference_UserWarningQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 // Remaining days are 29 days from Apr. 2nd to May 1st.
@@ -320,7 +321,7 @@
 
     @Test
     public void testGetMultipathPreference_SnoozedWarningQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 POLICY_SNOOZED /* policyWarning */,
@@ -339,7 +340,7 @@
 
     @Test
     public void testGetMultipathPreference_SnoozedBothQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 // 29 days from Apr. 2nd to May 1st
@@ -356,7 +357,7 @@
 
     @Test
     public void testGetMultipathPreference_SettingChanged() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 WARNING_DISABLED,
@@ -381,7 +382,7 @@
 
     @Test
     public void testGetMultipathPreference_ResourceChanged() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 WARNING_DISABLED,
@@ -404,4 +405,45 @@
         verify(mStatsManager, times(1)).registerUsageCallback(
                 any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
     }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testOnThresholdReached() {
+        prepareGetMultipathPreferenceTest(
+                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+                DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
+                DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
+                LIMIT_DISABLED,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        final ArgumentCaptor<NetworkStatsManager.UsageCallback> usageCallbackCaptor =
+                ArgumentCaptor.forClass(NetworkStatsManager.UsageCallback.class);
+        final ArgumentCaptor<NetworkTemplate> networkTemplateCaptor =
+                ArgumentCaptor.forClass(NetworkTemplate.class);
+        // Verify the callback is registered with quota - used = 14 - 2 = 12MB.
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                networkTemplateCaptor.capture(), eq(DataUnit.MEGABYTES.toBytes(12)), any(),
+                usageCallbackCaptor.capture());
+
+        // Capture arguments for later use.
+        final NetworkStatsManager.UsageCallback usageCallback = usageCallbackCaptor.getValue();
+        final NetworkTemplate template = networkTemplateCaptor.getValue();
+        assertNotNull(usageCallback);
+        assertNotNull(template);
+
+        // Decrease quota from 14 to 11, and trigger the event.
+        // TODO: Mock daily and monthly used bytes instead of changing subscription to simulate
+        //  remaining quota changed.
+        when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
+                .thenReturn(DataUnit.MEGABYTES.toBytes(11));
+        usageCallback.onThresholdReached(template);
+
+        // Callback must have been re-registered with new remaining quota = 11 - 2 = 9MB.
+        verify(mStatsManager, times(1))
+                .unregisterUsageCallback(eq(usageCallback));
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                eq(template), eq(DataUnit.MEGABYTES.toBytes(9)), any(), eq(usageCallback));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index aa4c4e3..06e0d6d 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -21,6 +21,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
@@ -44,8 +46,11 @@
 import android.os.Handler;
 import android.os.test.TestLooper;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.server.ConnectivityService;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -75,13 +80,20 @@
     @Mock IDnsResolver mDnsResolver;
     @Mock INetd mNetd;
     @Mock NetworkAgentInfo mNai;
+    @Mock ClatCoordinator mClatCoordinator;
 
     TestLooper mLooper;
     Handler mHandler;
     NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
 
     Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
-        return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) {
+        final ConnectivityService.Dependencies deps = new ConnectivityService.Dependencies() {
+            @Override public ClatCoordinator getClatCoordinator(INetd netd) {
+                return mClatCoordinator;
+            }
+        };
+
+        return new Nat464Xlat(mNai, mNetd, mDnsResolver, deps) {
             @Override protected int getNetId() {
                 return NETID;
             }
@@ -208,6 +220,39 @@
         }
     }
 
+    private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+        if (inOrder != null) {
+            return inOrder.verify(t);
+        } else {
+            return verify(t);
+        }
+    }
+
+    private void verifyClatdStart(@Nullable InOrder inOrder) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator)
+                .clatStart(eq(BASE_IFACE), eq(NETID), eq(new IpPrefix(NAT64_PREFIX)));
+        } else {
+            verifyWithOrder(inOrder, mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        }
+    }
+
+    private void verifyNeverClatdStart() throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verify(mClatCoordinator, never()).clatStart(anyString(), anyInt(), any());
+        } else {
+            verify(mNetd, never()).clatdStart(anyString(), anyString());
+        }
+    }
+
+    private void verifyClatdStop(@Nullable InOrder inOrder) throws Exception {
+        if (SdkLevel.isAtLeastT()) {
+            verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+        } else {
+            verifyWithOrder(inOrder, mNetd).clatdStop(eq(BASE_IFACE));
+        }
+    }
+
     private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
         Nat464Xlat nat = makeNat464Xlat(true);
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
@@ -219,7 +264,7 @@
         // Start clat.
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -235,7 +280,7 @@
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
         assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
@@ -262,7 +307,7 @@
     private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
         Nat464Xlat nat = makeNat464Xlat(true);
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
-        InOrder inOrder = inOrder(mNetd, mConnectivity);
+        InOrder inOrder = inOrder(mNetd, mConnectivity, mClatCoordinator);
 
         mNai.linkProperties.addLinkAddress(V6ADDR);
 
@@ -270,7 +315,7 @@
 
         nat.start();
 
-        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(inOrder);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -284,7 +329,7 @@
         // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
         nat.stop();
 
-        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(inOrder);
 
         inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -306,7 +351,7 @@
 
         nat.start();
 
-        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(inOrder);
 
         if (!interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
@@ -328,7 +373,7 @@
         // ConnectivityService stops clat again.
         nat.stop();
 
-        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(inOrder);
 
         inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -357,7 +402,7 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
         nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -373,7 +418,7 @@
         nat.interfaceRemoved(STACKED_IFACE);
         mLooper.dispatchNext();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -395,13 +440,13 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertIdle(nat);
 
@@ -437,13 +482,13 @@
 
         nat.start();
 
-        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+        verifyClatdStart(null /* inOrder */);
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
         makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
-        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verifyClatdStop(null /* inOrder */);
         verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
         assertIdle(nat);
 
@@ -518,7 +563,7 @@
             mNai.linkProperties.setNat64Prefix(nat64Prefix);
             nat.setNat64PrefixFromRa(nat64Prefix);
             nat.update();
-            verify(mNetd, never()).clatdStart(anyString(), anyString());
+            verifyNeverClatdStart();
             assertIdle(nat);
         } else {
             // Prefix discovery is started.
@@ -529,7 +574,7 @@
             mNai.linkProperties.setNat64Prefix(nat64Prefix);
             nat.setNat64PrefixFromRa(nat64Prefix);
             nat.update();
-            verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX);
+            verifyClatdStart(null /* inOrder */);
             assertStarting(nat);
         }
     }
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index fb821c3..354e79a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -695,7 +695,8 @@
         mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1},
                 SYSTEM_APPID1);
 
-        final List<PackageInfo> pkgs = List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
+        final List<PackageInfo> pkgs = List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, SYSTEM_APP_UID21, CHANGE_NETWORK_STATE));
         doReturn(pkgs).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
@@ -761,9 +762,10 @@
                 MOCK_APPID1);
     }
 
-    @Test
-    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+    private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+            throws Exception {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
@@ -771,15 +773,15 @@
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
         mPermissionMonitor.startMonitoring();
-        // Every app on user 0 except MOCK_UID12 are under VPN.
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
         final Set<UidRange> vpnRange1 = Set.of(
                 new UidRange(0, MOCK_UID12 - 1),
                 new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1));
         final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
 
         // When VPN is connected, expect a rule to be set up for user app MOCK_UID11
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
@@ -787,28 +789,40 @@
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
         mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
         // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
         // old UID rules then adds the new ones. Expect netd to be updated
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
 
         reset(mBpfNetMaps);
 
         // When VPN is disconnected, expect rules to be torn down
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
-        assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
     }
 
     @Test
-    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
+            throws Exception {
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+    }
+
+    private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
+            Exception {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
@@ -818,12 +832,12 @@
         mPermissionMonitor.startMonitoring();
         final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
                 UidRange.createForUser(MOCK_USER2));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
 
         // Newly-installed package should have uid rules added
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
 
         // Removed package should have its uid rules removed
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,162 @@
         verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
     }
 
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisable() {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
+        final UidRange[] lockdownRange = {
+                new UidRange(0, MOCK_UID12 - 1),
+                new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
+        };
+
+        // Add Lockdown uid range, expect a rule to be set up for MOCK_UID11 and VPN_UID
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range, expect rules to be torn down
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is subject to the VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] lockdownRange = {range};
+
+        // Add Lockdown uid range at 1st time, expect a rule to be set up
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
+        // already has the rule
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
+        // the range 2 times.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
+        // twice and we removed twice.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is subject to the VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] lockdownRangeDuplicates = {range, range};
+        final UidRange[] lockdownRange = {range};
+
+        // Add Lockdown uid ranges which contains duplicated uid ranges
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRangeDuplicates);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
+        // ranges we added contains duplicated uid ranges.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithInstallAndUnInstall() {
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
+
+        mPermissionMonitor.startMonitoring();
+        final UidRange[] lockdownRange = {
+                UidRange.createForUser(MOCK_USER1),
+                UidRange.createForUser(MOCK_USER2)
+        };
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+
+        reset(mBpfNetMaps);
+
+        // Installing package should add Lockdown rules
+        addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID21, true /* add */);
+
+        reset(mBpfNetMaps);
+
+        // Uninstalling package should remove Lockdown rules
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+    }
 
     // Normal package add/remove operations will trigger multiple intent for uids corresponding to
     // each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
@@ -1153,7 +1323,8 @@
     public void testOnExternalApplicationsAvailable() throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // and have different uids. There has no permission for both uids.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1211,7 +1382,8 @@
             throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // storage and shared on MOCK_UID11. There has no permission for MOCK_UID11.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1237,7 +1409,8 @@
         // Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
         // MOCK_PACKAGE2 is installed on device. These two packages are shared on MOCK_UID11.
         // MOCK_UID11 has NETWORK and INTERNET permissions.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11, CHANGE_NETWORK_STATE, INTERNET)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 33c0868..5c1992d 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -20,28 +20,32 @@
 import static android.Manifest.permission.CONTROL_VPN;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
+import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -54,6 +58,8 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -65,6 +71,7 @@
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -77,26 +84,40 @@
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
+import android.net.IpSecConfig;
 import android.net.IpSecManager;
+import android.net.IpSecTransform;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalSocket;
 import android.net.Network;
+import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo.DetailedState;
 import android.net.RouteInfo;
 import android.net.UidRangeParcel;
 import android.net.VpnManager;
+import android.net.VpnProfileState;
 import android.net.VpnService;
 import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.ChildSessionCallback;
+import android.net.ipsec.ike.ChildSessionConfiguration;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeTrafficSelector;
+import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
+import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.net.ipsec.ike.exceptions.IkeTimeoutException;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
 import android.os.ParcelFileDescriptor;
+import android.os.PowerWhitelistManager;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -105,6 +126,7 @@
 import android.security.Credentials;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.filters.SmallTest;
@@ -113,12 +135,18 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
+import com.android.internal.util.HexDump;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.server.DeviceIdleInternal;
 import com.android.server.IpSecService;
+import com.android.server.VpnTestBase;
+import com.android.server.vcn.util.PersistableBundleUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
@@ -134,7 +162,9 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -142,7 +172,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
@@ -154,50 +188,53 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
-public class VpnTest {
+@IgnoreUpTo(S_V2)
+public class VpnTest extends VpnTestBase {
     private static final String TAG = "VpnTest";
 
-    // Mock users
-    static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
-    static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
-    static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED);
-    static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED);
-    static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE);
-    static {
-        restrictedProfileA.restrictedProfileParentId = primaryUser.id;
-        restrictedProfileB.restrictedProfileParentId = secondaryUser.id;
-        managedProfileA.profileGroupId = primaryUser.id;
-    }
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     static final Network EGRESS_NETWORK = new Network(101);
     static final String EGRESS_IFACE = "wlan0";
-    static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    private static final String TEST_VPN_CLIENT = "2.4.6.8";
     private static final String TEST_VPN_SERVER = "1.2.3.4";
     private static final String TEST_VPN_IDENTITY = "identity";
     private static final byte[] TEST_VPN_PSK = "psk".getBytes();
 
+    private static final int IP4_PREFIX_LEN = 32;
+    private static final int MIN_PORT = 0;
+    private static final int MAX_PORT = 65535;
+
+    private static final InetAddress TEST_VPN_CLIENT_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
+    private static final InetAddress TEST_VPN_SERVER_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
+    private static final InetAddress TEST_VPN_CLIENT_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.200");
+    private static final InetAddress TEST_VPN_SERVER_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.201");
+    private static final InetAddress TEST_VPN_INTERNAL_IP =
+            InetAddresses.parseNumericAddress("198.51.100.10");
+    private static final InetAddress TEST_VPN_INTERNAL_DNS =
+            InetAddresses.parseNumericAddress("8.8.8.8");
+
+    private static final IkeTrafficSelector IN_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
+    private static final IkeTrafficSelector OUT_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT,
+                    InetAddresses.parseNumericAddress("0.0.0.0"),
+                    InetAddresses.parseNumericAddress("255.255.255.255"));
+
     private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
+    private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
-
-    /**
-     * Names and UIDs for some fake packages. Important points:
-     *  - UID is ordered increasing.
-     *  - One pair of packages have consecutive UIDs.
-     */
-    static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
-    static final int[] PKG_UIDS = {66, 77, 78, 400};
-
-    // Mock packages
-    static final Map<String, Integer> mPackages = new ArrayMap<>();
-    static {
-        for (int i = 0; i < PKGS.length; i++) {
-            mPackages.put(PKGS[i], PKG_UIDS[i]);
-        }
-    }
-    private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id);
+    private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
+            "VPNAPPEXCLUDED_27_com.testvpn.vpn";
+    static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
+    private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
 
     @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
     @Mock private UserManager mUserManager;
@@ -207,14 +244,21 @@
     @Mock private AppOpsManager mAppOps;
     @Mock private NotificationManager mNotificationManager;
     @Mock private Vpn.SystemServices mSystemServices;
+    @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
     @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
+    @Mock private NetworkAgent mMockNetworkAgent;
     @Mock private ConnectivityManager mConnectivityManager;
     @Mock private IpSecService mIpSecService;
     @Mock private VpnProfileStore mVpnProfileStore;
+    @Mock private ScheduledThreadPoolExecutor mExecutor;
+    @Mock private ScheduledFuture mScheduledFuture;
+    @Mock DeviceIdleInternal mDeviceIdleInternal;
     private final VpnProfile mVpnProfile;
 
     private IpSecManager mIpSecManager;
 
+    private TestDeps mTestDeps;
+
     public VpnTest() throws Exception {
         // Build an actual VPN profile that is capable of being converted to and from an
         // Ikev2VpnProfile
@@ -229,9 +273,10 @@
         MockitoAnnotations.initMocks(this);
 
         mIpSecManager = new IpSecManager(mContext, mIpSecService);
+        mTestDeps = spy(new TestDeps());
 
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        setMockedPackages(mPackages);
+        setMockedPackages(sPackages);
 
         when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
         when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
@@ -269,6 +314,33 @@
         // itself, so set the default value of Context#checkCallingOrSelfPermission to
         // PERMISSION_DENIED.
         doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
+
+        // Set up mIkev2SessionCreator and mExecutor
+        resetIkev2SessionCreator(mIkeSessionWrapper);
+        resetExecutor(mScheduledFuture);
+    }
+
+    private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
+        reset(mIkev2SessionCreator);
+        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
+                .thenReturn(ikeSession);
+    }
+
+    private void resetExecutor(ScheduledFuture scheduledFuture) {
+        doAnswer(
+                (invocation) -> {
+                    ((Runnable) invocation.getArgument(0)).run();
+                    return null;
+                })
+            .when(mExecutor)
+            .execute(any());
+        when(mExecutor.schedule(
+                any(Runnable.class), anyLong(), any())).thenReturn(mScheduledFuture);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
     }
 
     private <T> void mockService(Class<T> clazz, String name, T service) {
@@ -296,73 +368,99 @@
         return new Range<Integer>(start, stop);
     }
 
+    private static String getPackageByteString(List<String> packages) {
+        try {
+            return HexDump.toHexString(
+                    PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
+                            packages, PersistableBundleUtils.STRING_SERIALIZER)),
+                        true /* upperCase */);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     @Test
     public void testRestrictedProfilesAreAddedToVpn() {
-        setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
+        setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
 
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         // Assume the user can have restricted profiles.
         doReturn(true).when(mUserManager).canHaveRestrictedProfile();
         final Set<Range<Integer>> ranges =
-                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null);
+                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
+                 ranges);
     }
 
     @Test
     public void testManagedProfilesAreNotAddedToVpn() {
-        setMockedUsers(primaryUser, managedProfileA);
+        setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
 
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
-                null, null);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
+                PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
     }
 
     @Test
     public void testAddUserToVpnOnlyAddsOneUser() {
-        setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
+        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
 
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final Set<Range<Integer>> ranges = new ArraySet<>();
-        vpn.addUserToRanges(ranges, primaryUser.id, null, null);
+        vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
     }
 
     @Test
     public void testUidAllowAndDenylist() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
 
         // Allowed list
-        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
                 Arrays.asList(packages), null /* disallowedApplications */);
         assertEquals(rangeSet(
                 uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
-                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])),
+                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
                 allow);
 
         // Denied list
         final Set<Range<Integer>> disallow =
-                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
                         null /* allowedApplications */, Arrays.asList(packages));
         assertEquals(rangeSet(
                 uidRange(userStart, userStart + PKG_UIDS[0] - 1),
                 uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
                 /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
-                uidRange(userStart + PKG_UIDS[2] + 1, userStop)),
+                uidRange(userStart + PKG_UIDS[2] + 1,
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
                 disallow);
     }
 
+    private void verifyPowerSaveTempWhitelistApp(String packageName) {
+        verify(mDeviceIdleInternal).addPowerSaveTempWhitelistApp(anyInt(), eq(packageName),
+                anyLong(), anyInt(), eq(false), eq(PowerWhitelistManager.REASON_VPN),
+                eq("VpnManager event"));
+    }
+
     @Test
     public void testGetAlwaysAndOnGetLockDown() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         // Default state.
         assertFalse(vpn.getAlwaysOn());
@@ -386,8 +484,8 @@
 
     @Test
     public void testLockdownChangingPackage() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         // Set always-on without lockdown.
@@ -397,25 +495,31 @@
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Switch to another app.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
     }
 
     @Test
     public void testLockdownAllowlist() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         // Set always-on with lockdown and allow app PKGS[2] from lockdown.
@@ -423,17 +527,25 @@
                 PKGS[1], true, Collections.singletonList(PKGS[2])));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[]  {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         // Change allowed app list to PKGS[3].
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[1], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
 
         // Change the VPN app.
@@ -441,32 +553,52 @@
                 PKGS[0], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
 
         // Remove the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop),
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
 
         // Add the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[0], true, Collections.singletonList(PKGS[1])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Try allowing a package with a comma, should be rejected.
@@ -479,19 +611,27 @@
                 PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
     }
 
     @Test
     public void testLockdownRuleRepeatability() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())};
+                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
         // Given legacy lockdown is already enabled,
         vpn.setLockdown(true);
         verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
@@ -522,13 +662,16 @@
     @Test
     public void testLockdownRuleReversibility() throws Exception {
         doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final UidRangeParcel[] entireUser = {
-            new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
+            new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
         };
         final UidRangeParcel[] exceptPkg0 = {
             new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
-            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop)
+            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
+                               Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
+            new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
+                               entireUser[0].stop),
         };
 
         final InOrder order = inOrder(mConnectivityManager);
@@ -571,17 +714,17 @@
 
     @Test
     public void testIsAlwaysOnPackageSupported() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         ApplicationInfo appInfo = new ApplicationInfo();
-        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id)))
+        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
                 .thenReturn(appInfo);
 
         ServiceInfo svcInfo = new ServiceInfo();
         ResolveInfo resInfo = new ResolveInfo();
         resInfo.serviceInfo = svcInfo;
         when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
-                eq(primaryUser.id)))
+                eq(PRIMARY_USER.id)))
                 .thenReturn(Collections.singletonList(resInfo));
 
         // null package name should return false
@@ -605,9 +748,9 @@
 
     @Test
     public void testNotificationShownForAlwaysOnApp() throws Exception {
-        final UserHandle userHandle = UserHandle.of(primaryUser.id);
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
         final InOrder order = inOrder(mNotificationManager);
 
@@ -640,15 +783,15 @@
      */
     @Test
     public void testGetProfileNameForPackage() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
-        final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+        final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
         assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
     }
 
     private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception {
-        return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+        return createVpnAndSetupUidChecks(PRIMARY_USER, grantedOps);
     }
 
     private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception {
@@ -696,6 +839,118 @@
         }
     }
 
+    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
+                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+        vpn.mNetworkAgent = mMockNetworkAgent;
+        return vpn;
+    }
+
+    @Test
+    public void testSetAndGetAppExclusionList() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
+        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+        verify(mVpnProfileStore)
+                .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
+                     eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
+        assertEquals(vpn.createUserAndRestrictedProfilesRanges(
+                PRIMARY_USER.id, null, Arrays.asList(PKGS)),
+                vpn.mNetworkCapabilities.getUids());
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
+    @Test
+    public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+        verify(mMockNetworkAgent).sendNetworkCapabilities(any());
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+
+        reset(mMockNetworkAgent);
+
+        // Remove one of the package
+        List<Integer> newExcludedUids = toList(PKG_UIDS);
+        newExcludedUids.remove((Integer) PKG_UIDS[0]);
+        sPackages.remove(PKGS[0]);
+        vpn.refreshPlatformVpnAppExclusionList();
+
+        // List in keystore is not changed, but UID for the removed packages is no longer exempted.
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                vpn.mNetworkCapabilities.getUids());
+        ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                ArgumentCaptor.forClass(NetworkCapabilities.class);
+        verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                ncCaptor.getValue().getUids());
+
+        reset(mMockNetworkAgent);
+
+        // Add the package back
+        newExcludedUids.add(PKG_UIDS[0]);
+        sPackages.put(PKGS[0], PKG_UIDS[0]);
+        vpn.refreshPlatformVpnAppExclusionList();
+
+        // List in keystore is not changed and the uid list should be updated in the net cap.
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                vpn.mNetworkCapabilities.getUids());
+        verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                ncCaptor.getValue().getUids());
+    }
+
+    private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+        final SortedSet<Integer> list = new TreeSet<>();
+
+        final int userBase = userId * UserHandle.PER_USER_RANGE;
+        for (int uid : excludedList) {
+            final int applicationUid = UserHandle.getUid(userId, uid);
+            list.add(applicationUid);
+            list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+        }
+
+        final int minUid = userBase;
+        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+        final Set<Range<Integer>> ranges = new ArraySet<>();
+
+        // Iterate the list to create the ranges between each uid.
+        int start = minUid;
+        for (int uid : list) {
+            if (uid == start) {
+                start++;
+            } else {
+                ranges.add(new Range<>(start, uid - 1));
+                start = uid + 1;
+            }
+        }
+
+        // Create the range between last uid and max uid.
+        if (start <= maxUid) {
+            ranges.add(new Range<>(start, maxUid));
+        }
+
+        return ranges;
+    }
+
+    @Test
+    public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        // Mock it to restricted profile
+        when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
+        // Restricted users cannot configure VPNs
+        assertThrows(SecurityException.class,
+                () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
+        assertThrows(SecurityException.class, () -> vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
     @Test
     public void testProvisionVpnProfilePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -739,7 +994,7 @@
     public void testProvisionVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
@@ -762,7 +1017,7 @@
     public void testDeleteVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.deleteVpnProfile(TEST_VPN_PKG);
@@ -783,6 +1038,30 @@
         verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
     }
 
+    private void verifyPlatformVpnIsActivated(String packageName) {
+        verify(mAppOps).noteOpNoThrow(
+                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+        verify(mAppOps).startOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */,
+                eq(null) /* message */);
+    }
+
+    private void verifyPlatformVpnIsDeactivated(String packageName) {
+        // Add a small delay to double confirm that finishOp is only called once.
+        verify(mAppOps, after(100)).finishOp(
+                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+                eq(Process.myUid()),
+                eq(packageName),
+                eq(null) /* attributionTag */);
+    }
+
     @Test
     public void testStartVpnProfile() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -793,13 +1072,7 @@
         vpn.startVpnProfile(TEST_VPN_PKG);
 
         verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
     }
 
     @Test
@@ -811,7 +1084,7 @@
 
         vpn.startVpnProfile(TEST_VPN_PKG);
 
-        // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown.
+        // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
         verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
                 TEST_VPN_PKG, null /* attributionTag */, null /* message */);
     }
@@ -867,7 +1140,7 @@
     public void testStartVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.startVpnProfile(TEST_VPN_PKG);
@@ -880,7 +1153,7 @@
     public void testStopVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.stopVpnProfile(TEST_VPN_PKG);
@@ -896,18 +1169,7 @@
         when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
                 .thenReturn(mVpnProfile.encode());
         vpn.startVpnProfile(TEST_VPN_PKG);
-        verify(mAppOps).noteOpNoThrow(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        verify(mAppOps).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
         // Add a small delay to make sure that startOp is only called once.
         verify(mAppOps, after(100).times(1)).startOp(
                 eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
@@ -923,12 +1185,7 @@
                 eq(null) /* attributionTag */,
                 eq(null) /* message */);
         vpn.stopVpnProfile(TEST_VPN_PKG);
-        // Add a small delay to double confirm that startOp is only called once.
-        verify(mAppOps, after(100)).finishOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
     }
 
     @Test
@@ -964,6 +1221,169 @@
                 eq(null) /* message */);
     }
 
+    private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
+            int errorCode, VpnProfileState... profileState) {
+        final Context userContext =
+                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
+        final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        final int verifyTimes = (profileState == null) ? 1 : profileState.length;
+        verify(userContext, times(verifyTimes)).startService(intentArgumentCaptor.capture());
+
+        for (int i = 0; i < verifyTimes; i++) {
+            final Intent intent = intentArgumentCaptor.getAllValues().get(i);
+            assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
+            final Set<String> categories = intent.getCategories();
+            assertTrue(categories.contains(category));
+            assertEquals(errorClass,
+                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
+            assertEquals(errorCode,
+                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
+            if (profileState != null) {
+                assertEquals(profileState[i], intent.getParcelableExtra(
+                        VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
+            }
+        }
+        reset(userContext);
+    }
+
+    @Test
+    public void testVpnManagerEventForUserDeactivated() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
+        // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
+        // VPN is replaced by a new one. But only Settings can change to some other packages, and
+        // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
+        // security checks.
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        // Test the case that the user deactivates the vpn in vpn app.
+        final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+        vpn.stopVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+        // errorCode won't be set.
+        verifyVpnManagerEvent(sessionKey1, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+        reset(mAppOps);
+
+        // Test the case that the user chooses another vpn and the original one is replaced.
+        final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+        vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
+        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+        // errorCode won't be set.
+        verifyVpnManagerEvent(sessionKey2, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+    }
+
+    @Test
+    public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastT());
+        // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        // Enable VPN always-on for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN lockdown for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
+
+        // Disable VPN lockdown for PKGS[1].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Disable VPN always-on.
+        assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN always-on for PKGS[1] again.
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[1]);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+        // Enable VPN always-on for PKGS[2].
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
+                null /* lockdownAllowlist */));
+        verifyPowerSaveTempWhitelistApp(PKGS[2]);
+        reset(mDeviceIdleInternal);
+        // PKGS[1] is replaced with PKGS[2].
+        // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
+        // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
+        // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+    }
+
+    @Test
+    public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+
+        // Enable VPN always-on for TEST_VPN_PKG.
+        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
+                null /* lockdownAllowlist */));
+
+        // Reset to verify next startVpnProfile.
+        reset(mAppOps);
+
+        vpn.stopVpnProfile(TEST_VPN_PKG);
+
+        // Reconnect the vpn with different package will cause exception.
+        assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
+
+        // Reconnect the vpn again with the vpn always on package w/o exception.
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+    }
+
     @Test
     public void testSetPackageAuthorizationVpnService() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
@@ -981,7 +1401,7 @@
     public void testSetPackageAuthorizationPlatformVpn() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM));
+        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
         verify(mAppOps)
                 .setMode(
                         eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
@@ -1031,15 +1451,16 @@
                 config -> Arrays.asList(config.flags).contains(flag)));
     }
 
-    @Test
-    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+    private void doTestPlatformVpnWithException(IkeException exception,
+            String category, int errorType, int errorCode) throws Exception {
         final ArgumentCaptor<IkeSessionCallback> captor =
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        when(exception.getErrorType())
-                .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED);
 
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile));
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
         final NetworkCallback cb = triggerOnAvailableAndGetCallback();
 
         verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1048,18 +1469,116 @@
         // state
         verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
                 .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+        reset(mIkev2SessionCreator);
         final IkeSessionCallback ikeCb = captor.getValue();
-        ikeCb.onClosedExceptionally(exception);
+        ikeCb.onClosedWithException(exception);
 
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
-        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
+        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+        reset(mDeviceIdleInternal);
+        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, null /* profileState */);
+        if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
+            verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
+                    .unregisterNetworkCallback(eq(cb));
+        } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE) {
+            int retryIndex = 0;
+            final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+
+            ikeCb2.onClosedWithException(exception);
+            verifyRetryAndGetNewIkeCb(retryIndex++);
+        }
+    }
+
+    private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
+        final ArgumentCaptor<Runnable> runnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+
+        // Verify retry is scheduled
+        final long expectedDelay = mTestDeps.getNextRetryDelaySeconds(retryIndex);
+        verify(mExecutor).schedule(runnableCaptor.capture(), eq(expectedDelay), any());
+
+        // Mock the event of firing the retry task
+        runnableCaptor.getValue().run();
+
+        verify(mIkev2SessionCreator)
+                .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
+
+        // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
+        // for the next retry verification
+        resetIkev2SessionCreator(mIkeSessionWrapper);
+        resetExecutor(mScheduledFuture);
+
+        return ikeCbCaptor.getValue();
+    }
+
+    @Test
+    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+        final IkeProtocolException exception = mock(IkeProtocolException.class);
+        final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
+        when(exception.getErrorType()).thenReturn(errorCode);
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
+        final IkeProtocolException exception = mock(IkeProtocolException.class);
+        final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
+        when(exception.getErrorType()).thenReturn(errorCode);
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final UnknownHostException unknownHostException = new UnknownHostException();
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
+        when(exception.getCause()).thenReturn(unknownHostException);
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final IkeTimeoutException ikeTimeoutException =
+                new IkeTimeoutException("IkeTimeoutException");
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
+        when(exception.getCause()).thenReturn(ikeTimeoutException);
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
+        final IkeNetworkLostException exception = new IkeNetworkLostException(
+                new Network(100));
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                VpnManager.ERROR_CODE_NETWORK_LOST);
+    }
+
+    @Test
+    public void testStartPlatformVpnFailedWithIOException() throws Exception {
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final IOException ioException = new IOException();
+        final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
+        when(exception.getCause()).thenReturn(ioException);
+        doTestPlatformVpnWithException(exception,
+                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+                errorCode);
     }
 
     @Test
     public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
         when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
                 .thenThrow(new IllegalArgumentException());
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
         final NetworkCallback cb = triggerOnAvailableAndGetCallback();
 
         verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1079,18 +1598,18 @@
                 eq(AppOpsManager.MODE_ALLOWED));
 
         verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id));
+                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
         verify(mSystemServices).settingsSecurePutIntForUser(
                 eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
-                eq(primaryUser.id));
+                eq(PRIMARY_USER.id));
         verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id));
+                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
     }
 
     @Test
     public void testSetAndStartAlwaysOnVpn() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
         // UID checks must return a different UID; otherwise it'll be treated as already prepared.
         final int uid = Process.myUid() + 1;
@@ -1107,7 +1626,7 @@
     }
 
     private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
-        setMockedUsers(primaryUser);
+        setMockedUsers(PRIMARY_USER);
 
         // Dummy egress interface
         final LinkProperties lp = new LinkProperties();
@@ -1121,11 +1640,266 @@
         return vpn;
     }
 
+    private IkeSessionConnectionInfo createIkeConnectInfo() {
+        return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
+    }
+
+    private IkeSessionConnectionInfo createIkeConnectInfo_2() {
+        return new IkeSessionConnectionInfo(
+                TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
+    }
+
+    private IkeSessionConfiguration createIkeConfig(
+            IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
+        final IkeSessionConfiguration.Builder builder =
+                new IkeSessionConfiguration.Builder(ikeConnectInfo);
+
+        if (isMobikeEnabled) {
+            builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
+        }
+
+        return builder.build();
+    }
+
+    private ChildSessionConfiguration createChildConfig() {
+        return new ChildSessionConfiguration.Builder(Arrays.asList(IN_TS), Arrays.asList(OUT_TS))
+                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
+                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
+                .build();
+    }
+
+    private IpSecTransform createIpSecTransform() {
+        return new IpSecTransform(mContext, new IpSecConfig());
+    }
+
+    private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
+                anyInt(), anyString());
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
+                anyInt(), anyString());
+    }
+
+    private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
+            throws Exception {
+        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+        final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
+                ArgumentCaptor.forClass(ChildSessionCallback.class);
+
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
+                any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
+
+        return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
+    }
+
+    private static class PlatformVpnSnapshot {
+        public final Vpn vpn;
+        public final NetworkCallback nwCb;
+        public final IkeSessionCallback ikeCb;
+        public final ChildSessionCallback childCb;
+
+        PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
+                IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
+            this.vpn = vpn;
+            this.nwCb = nwCb;
+            this.ikeCb = ikeCb;
+            this.childCb = childCb;
+        }
+    }
+
+    private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
+            throws Exception {
+        doReturn(mMockNetworkAgent).when(mTestDeps)
+                .newNetworkAgent(
+                        any(), any(), anyString(), any(), any(), any(), any(), any());
+
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback();
+
+        // Mock the setup procedure by firing callbacks
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        ikeCb.onOpened(ikeConfig);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Verification VPN setup
+        verifyApplyTunnelModeTransforms(1);
+
+        ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
+        ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                ArgumentCaptor.forClass(NetworkCapabilities.class);
+        verify(mTestDeps).newNetworkAgent(
+                any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
+                any(), any(), any());
+
+        // Check LinkProperties
+        final LinkProperties lp = lpCaptor.getValue();
+        final List<RouteInfo> expectedRoutes = Arrays.asList(
+                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RouteInfo.RTN_UNICAST),
+                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RTN_UNREACHABLE));
+        assertEquals(expectedRoutes, lp.getRoutes());
+
+        // Check internal addresses
+        final List<LinkAddress> expectedAddresses =
+                Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN));
+        assertEquals(expectedAddresses, lp.getLinkAddresses());
+
+        // Check internal DNS
+        assertEquals(Arrays.asList(TEST_VPN_INTERNAL_DNS), lp.getDnsServers());
+
+        // Check NetworkCapabilities
+        assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
+
+        return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
+    }
+
     @Test
     public void testStartPlatformVpn() throws Exception {
-        startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
-        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
-        // a subsequent patch.
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
+    public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+
+        // Mock network loss and verify a cleanup task is scheduled
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        verify(mExecutor).schedule(any(Runnable.class), anyLong(), any());
+
+        // Mock new network comes up and the cleanup task is cancelled
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+        verify(mScheduledFuture).cancel(anyBoolean());
+
+        // Verify MOBIKE is triggered
+        verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2);
+
+        // Mock the MOBIKE procedure
+        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
+        vpnSnapShot.childCb.onIpSecTransformsMigrated(
+                createIpSecTransform(), createIpSecTransform());
+
+        verify(mIpSecService).setNetworkForTunnelInterface(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
+
+        // Expect 2 times: one for initial setup and one for MOBIKE
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
+    public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the first IKE creation to be prepared to capture callbacks of the second
+        // IKE session
+        resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
+
+        // Mock network switch
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+
+        // Verify the old IKE Session is killed
+        verify(mIkeSessionWrapper).kill();
+
+        // Capture callbacks of the new IKE Session
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        // Mock the IKE Session setup
+        ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
+
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Expect 2 times since there have been two Session setups
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    private void verifyHandlingNetworkLoss() throws Exception {
+        final ArgumentCaptor<LinkProperties> lpCaptor =
+                ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mMockNetworkAgent).sendLinkProperties(lpCaptor.capture());
+        final LinkProperties lp = lpCaptor.getValue();
+
+        assertNull(lp.getInterfaceName());
+        final List<RouteInfo> expectedRoutes = Arrays.asList(
+                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+                        null /*iface*/, RTN_UNREACHABLE),
+                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+                        null /*iface*/, RTN_UNREACHABLE));
+        assertEquals(expectedRoutes, lp.getRoutes());
+    }
+
+    @Test
+    public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the #sendLinkProperties during first setup.
+        reset(mMockNetworkAgent);
+
+        final ArgumentCaptor<Runnable> runnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+
+        // Mock network loss
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+        // Mock the grace period expires
+        verify(mExecutor).schedule(runnableCaptor.capture(), anyLong(), any());
+        runnableCaptor.getValue().run();
+
+        verifyHandlingNetworkLoss();
+    }
+
+    @Test
+    public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the #sendLinkProperties during first setup.
+        reset(mMockNetworkAgent);
+
+        // Mock network loss
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+        verifyHandlingNetworkLoss();
     }
 
     @Test
@@ -1138,6 +1912,16 @@
         startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
     }
 
+    @Test
+    public void testStartPptp() throws Exception {
+        startPptp(true /* useMppe */);
+    }
+
+    @Test
+    public void testStartPptp_NoMppe() throws Exception {
+        startPptp(false /* useMppe */);
+    }
+
     private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
         assertNotNull(nc);
         VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
@@ -1145,6 +1929,48 @@
         assertEquals(type, ti.getType());
     }
 
+    private void startPptp(boolean useMppe) throws Exception {
+        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
+        profile.type = VpnProfile.TYPE_PPTP;
+        profile.name = "testProfileName";
+        profile.username = "userName";
+        profile.password = "thePassword";
+        profile.server = "192.0.2.123";
+        profile.mppe = useMppe;
+
+        doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
+        doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
+                any(), any(), any(), any(), anyInt());
+
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
+        final TestDeps deps = (TestDeps) vpn.mDeps;
+
+        testAndCleanup(() -> {
+            final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
+            final String[] argsPrefix = new String[]{
+                    EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
+                    "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
+                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
+            };
+            assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
+            if (useMppe) {
+                assertEquals(argsPrefix.length + 2, mtpdArgs.length);
+                assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
+                assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
+            } else {
+                assertEquals(argsPrefix.length + 1, mtpdArgs.length);
+                assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
+            }
+
+            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
+                    any(), any(), any(), any(), anyInt());
+        }, () -> { // Cleanup
+                vpn.mVpnRunner.exitVpnRunner();
+                deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
+                vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
+            });
+    }
+
     public void startRacoon(final String serverAddr, final String expectedAddr)
             throws Exception {
         final ConditionVariable legacyRunnerReady = new ConditionVariable();
@@ -1167,7 +1993,7 @@
                     legacyRunnerReady.open();
                     return new Network(102);
                 });
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
         final TestDeps deps = (TestDeps) vpn.mDeps;
         try {
             // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
@@ -1208,7 +2034,8 @@
         }
     }
 
-    private static final class TestDeps extends Vpn.Dependencies {
+    // Make it public and un-final so as to spy it
+    public class TestDeps extends Vpn.Dependencies {
         public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
         public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
         public final File mStateFile;
@@ -1337,6 +2164,22 @@
 
         @Override
         public void setBlocking(FileDescriptor fd, boolean blocking) {}
+
+        @Override
+        public DeviceIdleInternal getDeviceIdleInternal() {
+            return mDeviceIdleInternal;
+        }
+
+        @Override
+        public long getNextRetryDelaySeconds(int retryCount) {
+            // Simply return retryCount as the delay seconds for retrying.
+            return retryCount;
+        }
+
+        @Override
+        public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
+            return mExecutor;
+        }
     }
 
     /**
@@ -1348,7 +2191,7 @@
         when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
                 .thenReturn(asUserContext);
         final TestLooper testLooper = new TestLooper();
-        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService,
+        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
                 mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
         verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
                 provider -> provider.getName().contains("VpnNetworkProvider")
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
new file mode 100644
index 0000000..f84e2d8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link ConnectivityMonitor}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class ConnectivityMonitorWithConnectivityManagerTests {
+    @Mock private Context mContext;
+    @Mock private ConnectivityMonitor.Listener mockListener;
+    @Mock private ConnectivityManager mConnectivityManager;
+
+    private ConnectivityMonitorWithConnectivityManager monitor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mConnectivityManager).when(mContext)
+                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
+    }
+
+    @Test
+    public void testInitialState_shouldNotRegisterNetworkCallback() {
+        verifyNetworkCallbackRegistered(0 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStartDiscovery_shouldRegisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStartDiscoveryTwice_shouldRegisterOneNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.startWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStopDiscovery_shouldUnregisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.stopWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(1 /* time */);
+    }
+
+    @Test
+    public void testStopDiscoveryTwice_shouldUnregisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.stopWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(1 /* time */);
+    }
+
+    @Test
+    public void testIntentFired_shouldNotifyListener() {
+        InOrder inOrder = inOrder(mockListener);
+        monitor.startWatchingConnectivityChanges();
+
+        final ArgumentCaptor<NetworkCallback> callbackCaptor =
+                ArgumentCaptor.forClass(NetworkCallback.class);
+        verify(mConnectivityManager, times(1)).registerNetworkCallback(
+                any(NetworkRequest.class), callbackCaptor.capture());
+
+        final NetworkCallback callback = callbackCaptor.getValue();
+        final Network testNetwork = new Network(1 /* netId */);
+
+        // Simulate network available.
+        callback.onAvailable(testNetwork);
+        inOrder.verify(mockListener).onConnectivityChanged();
+
+        // Simulate network lost.
+        callback.onLost(testNetwork);
+        inOrder.verify(mockListener).onConnectivityChanged();
+
+        // Simulate network unavailable.
+        callback.onUnavailable();
+        inOrder.verify(mockListener).onConnectivityChanged();
+    }
+
+    private void verifyNetworkCallbackRegistered(int time) {
+        verify(mConnectivityManager, times(time)).registerNetworkCallback(
+                any(NetworkRequest.class), any(NetworkCallback.class));
+    }
+
+    private void verifyNetworkCallbackUnregistered(int time) {
+        verify(mConnectivityManager, times(time))
+                .unregisterNetworkCallback(any(NetworkCallback.class));
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
new file mode 100644
index 0000000..3e3c3bf
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsDiscoveryManager}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsDiscoveryManagerTests {
+
+    private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
+    private static final String SERVICE_TYPE_2 = "_test._tcp.local";
+
+    @Mock private ExecutorProvider executorProvider;
+    @Mock private MdnsSocketClient socketClient;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+
+    @Mock MdnsServiceBrowserListener mockListenerOne;
+    @Mock MdnsServiceBrowserListener mockListenerTwo;
+    private MdnsDiscoveryManager discoveryManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mockServiceTypeClientOne.getServiceTypeLabels())
+                .thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
+        when(mockServiceTypeClientTwo.getServiceTypeLabels())
+                .thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
+
+        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
+                    @Override
+                    MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+                        if (serviceType.equals(SERVICE_TYPE_1)) {
+                            return mockServiceTypeClientOne;
+                        } else if (serviceType.equals(SERVICE_TYPE_2)) {
+                            return mockServiceTypeClientTwo;
+                        }
+                        return null;
+                    }
+                };
+    }
+
+    @Test
+    public void registerListener_unregisterListener() throws IOException {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        verify(socketClient).startDiscovery();
+        verify(mockServiceTypeClientOne)
+                .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
+        discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
+        verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+        verify(socketClient).stopDiscovery();
+    }
+
+    @Test
+    public void registerMultipleListeners() throws IOException {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        verify(socketClient).startDiscovery();
+        verify(mockServiceTypeClientOne)
+                .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        discoveryManager.registerListener(
+                SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        verify(mockServiceTypeClientTwo)
+                .startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+    }
+
+    @Test
+    public void onResponseReceived() {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        discoveryManager.registerListener(
+                SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse responseForServiceTypeOne = createMockResponse(SERVICE_TYPE_1);
+        discoveryManager.onResponseReceived(responseForServiceTypeOne);
+        verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne);
+
+        MdnsResponse responseForServiceTypeTwo = createMockResponse(SERVICE_TYPE_2);
+        discoveryManager.onResponseReceived(responseForServiceTypeTwo);
+        verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo);
+
+        MdnsResponse responseForSubtype = createMockResponse("subtype._sub._googlecast._tcp.local");
+        discoveryManager.onResponseReceived(responseForSubtype);
+        verify(mockServiceTypeClientOne).processResponse(responseForSubtype);
+    }
+
+    private MdnsResponse createMockResponse(String serviceType) {
+        MdnsPointerRecord mockPointerRecord = mock(MdnsPointerRecord.class);
+        MdnsResponse mockResponse = mock(MdnsResponse.class);
+        when(mockResponse.getPointerRecords())
+                .thenReturn(Collections.singletonList(mockPointerRecord));
+        when(mockPointerRecord.getName()).thenReturn(TextUtils.split(serviceType, "\\."));
+        return mockResponse;
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
new file mode 100644
index 0000000..19d8a00
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Locale;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsPacketReaderTests {
+
+    @Test
+    public void testLimits() throws IOException {
+        byte[] data = new byte[25];
+        DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+
+        // After creating a new reader, confirm that the remaining is equal to the packet length
+        // (or that there is no temporary limit).
+        MdnsPacketReader packetReader = new MdnsPacketReader(datagramPacket);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to 0.
+        packetReader.setLimit(0);
+        assertEquals(0, packetReader.getRemaining());
+
+        // Confirm that we can clear the temporary limit, and restore to the length of the packet.
+        packetReader.clearLimit();
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to the actual length of the packet.
+        // While parsing packets, it is common to set the limit to the length of the packet.
+        packetReader.setLimit(data.length);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we ignore negative limits.
+        packetReader.setLimit(-10);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to something less than the packet length.
+        packetReader.setLimit(data.length / 2);
+        assertEquals(data.length / 2, packetReader.getRemaining());
+
+        // Confirm that we throw an exception if trying to set the temporary limit beyond the
+        // packet length.
+        packetReader.clearLimit();
+        try {
+            packetReader.setLimit(data.length * 2 + 1);
+            fail("Should have thrown an IOException when trying to set the temporary limit beyond "
+                    + "the packet length");
+        } catch (IOException e) {
+            // Expected
+        } catch (Exception e) {
+            fail(String.format(
+                    Locale.ROOT,
+                    "Should not have thrown any other exception except " + "for IOException: %s",
+                    e.getMessage()));
+        }
+        assertEquals(data.length, packetReader.getRemaining());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
new file mode 100644
index 0000000..fdb4d4a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.util.Log;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsRecordTests {
+    private static final String TAG = "MdnsRecordTests";
+    private static final int MAX_PACKET_SIZE = 4096;
+    private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+
+    @Test
+    public void testInet4AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000001" + "0001000011940004" + "0A010203");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        assertEquals("test", name[0]);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_A, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+        Inet4Address addr = record.getInet4Address();
+        assertEquals("/10.1.2.3", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTypeAAAInet6AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400001C"
+                        + "0001000011940010"
+                        + "AABBCCDD11223344"
+                        + "A0B0C0D010203040");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+                reader);
+        assertNull(record.getInet4Address());
+        Inet6Address addr = record.getInet6Address();
+        assertEquals("/aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV6_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTypeAAAInet4AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400001C"
+                        + "0001000011940010"
+                        + "0000000000000000"
+                        + "0000FFFF10203040");
+        assertNotNull(dataIn);
+        HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+                reader);
+        assertNull(record.getInet6Address());
+        Inet4Address addr = record.getInet4Address();
+        assertEquals("/16.32.48.64", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        final byte[] expectedDataIn =
+                HexDump.hexStringToByteArray("047465737400001C000100001194000410203040");
+        assertNotNull(expectedDataIn);
+        String expectedDataInText = HexDump.dumpHexString(expectedDataIn, 0, expectedDataIn.length);
+
+        assertEquals(expectedDataInText, dataOutText);
+    }
+
+    @Test
+    public void testPointerRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400000C"
+                        + "000100001194000E"
+                        + "03666F6F03626172"
+                        + "047175787800");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_PTR, type);
+
+        MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+        String[] pointer = record.getPointer();
+        assertEquals("foo.bar.quxx", MdnsRecord.labelsToString(pointer));
+
+        assertFalse(record.hasSubtype());
+        assertNull(record.getSubtype());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testServiceRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000021"
+                        + "0001000011940014"
+                        + "000100FF1F480366"
+                        + "6F6F036261720471"
+                        + "75787800");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_SRV, type);
+
+        MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+
+        int servicePort = record.getServicePort();
+        assertEquals(8008, servicePort);
+
+        String serviceHost = MdnsRecord.labelsToString(record.getServiceHost());
+        assertEquals("foo.bar.quxx", serviceHost);
+
+        assertEquals(1, record.getServicePriority());
+        assertEquals(255, record.getServiceWeight());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTextRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000010"
+                        + "0001000011940024"
+                        + "0D613D68656C6C6F"
+                        + "2074686572650C62"
+                        + "3D31323334353637"
+                        + "3839300878797A3D"
+                        + "21402324");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_TXT, type);
+
+        MdnsTextRecord record = new MdnsTextRecord(name, reader);
+
+        List<String> strings = record.getStrings();
+        assertNotNull(strings);
+        assertEquals(3, strings.size());
+
+        assertEquals("a=hello there", strings.get(0));
+        assertEquals("b=1234567890", strings.get(1));
+        assertEquals("xyz=!@#$", strings.get(2));
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
new file mode 100644
index 0000000..ea9156c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsResponseDecoder.Clock;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseDecoderTests {
+    private static final byte[] data = HexDump.hexStringToByteArray(
+            "0000840000000004"
+            + "00000003134A6F68"
+            + "6E6E792773204368"
+            + "726F6D6563617374"
+            + "0B5F676F6F676C65"
+            + "63617374045F7463"
+            + "70056C6F63616C00"
+            + "0010800100001194"
+            + "006C2369643D3937"
+            + "3062663534376237"
+            + "3533666336336332"
+            + "6432613336626238"
+            + "3936616261380576"
+            + "653D30320D6D643D"
+            + "4368726F6D656361"
+            + "73741269633D2F73"
+            + "657475702F69636F"
+            + "6E2E706E6716666E"
+            + "3D4A6F686E6E7927"
+            + "73204368726F6D65"
+            + "636173740463613D"
+            + "350473743D30095F"
+            + "7365727669636573"
+            + "075F646E732D7364"
+            + "045F756470C03100"
+            + "0C00010000119400"
+            + "02C020C020000C00"
+            + "01000011940002C0"
+            + "0CC00C0021800100"
+            + "000078001C000000"
+            + "001F49134A6F686E"
+            + "6E79277320436872"
+            + "6F6D6563617374C0"
+            + "31C0F30001800100"
+            + "0000780004C0A864"
+            + "68C0F3002F800100"
+            + "0000780005C0F300"
+            + "0140C00C002F8001"
+            + "000011940009C00C"
+            + "00050000800040");
+
+    private static final byte[] data6 = HexDump.hexStringToByteArray(
+            "0000840000000001000000030B5F676F6F676C656361737404"
+            + "5F746370056C6F63616C00000C000100000078003330476F6F676C"
+            + "652D486F6D652D4D61782D61363836666331323961366638636265"
+            + "31643636353139343065336164353766C00CC02E00108001000011"
+            + "9400C02369643D6136383666633132396136663863626531643636"
+            + "3531393430653361643537662363643D4133304233303032363546"
+            + "36384341313233353532434639344141353742314613726D3D4335"
+            + "35393134383530383841313638330576653D3035126D643D476F6F"
+            + "676C6520486F6D65204D61781269633D2F73657475702F69636F6E"
+            + "2E706E6710666E3D417474696320737065616B65720863613D3130"
+            + "3234340473743D320F62733D464138464341363734453537046E66"
+            + "3D320372733DC02E0021800100000078002D000000001F49246136"
+            + "3836666331322D396136662D386362652D316436362D3531393430"
+            + "65336164353766C01DC13F001C8001000000780010200033330000"
+            + "0000DA6C63FFFE7C74830109018001000000780004C0A801026C6F"
+            + "63616C0000018001000000780004C0A8010A000001800100000078"
+            + "0004C0A8010A00000000000000");
+
+    private static final String DUMMY_CAST_SERVICE_NAME = "_googlecast";
+    private static final String[] DUMMY_CAST_SERVICE_TYPE =
+            new String[] {DUMMY_CAST_SERVICE_NAME, "_tcp", "local"};
+
+    private final List<MdnsResponse> responses = new LinkedList<>();
+
+    private final Clock mClock = mock(Clock.class);
+
+    @Before
+    public void setUp() {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+        assertNotNull(data);
+        DatagramPacket packet = new DatagramPacket(data, data.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+        assertEquals(1, responses.size());
+    }
+
+    @Test
+    public void testDecodeWithNullServiceType() {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+        assertNotNull(data);
+        DatagramPacket packet = new DatagramPacket(data, data.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+        assertEquals(2, responses.size());
+    }
+
+    @Test
+    public void testDecodeMultipleAnswerPacket() throws IOException {
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        MdnsInetAddressRecord inet4AddressRecord = response.getInet4AddressRecord();
+        Inet4Address inet4Addr = inet4AddressRecord.getInet4Address();
+
+        assertNotNull(inet4Addr);
+        assertEquals("/192.168.100.104", inet4Addr.toString());
+
+        MdnsServiceRecord serviceRecord = response.getServiceRecord();
+        String serviceName = serviceRecord.getServiceName();
+        assertEquals(DUMMY_CAST_SERVICE_NAME, serviceName);
+
+        String serviceInstanceName = serviceRecord.getServiceInstanceName();
+        assertEquals("Johnny's Chromecast", serviceInstanceName);
+
+        String serviceHost = MdnsRecord.labelsToString(serviceRecord.getServiceHost());
+        assertEquals("Johnny's Chromecast.local", serviceHost);
+
+        int serviceProto = serviceRecord.getServiceProtocol();
+        assertEquals(MdnsServiceRecord.PROTO_TCP, serviceProto);
+
+        int servicePort = serviceRecord.getServicePort();
+        assertEquals(8009, servicePort);
+
+        int servicePriority = serviceRecord.getServicePriority();
+        assertEquals(0, servicePriority);
+
+        int serviceWeight = serviceRecord.getServiceWeight();
+        assertEquals(0, serviceWeight);
+
+        MdnsTextRecord textRecord = response.getTextRecord();
+        List<String> textStrings = textRecord.getStrings();
+        assertEquals(7, textStrings.size());
+        assertEquals("id=970bf547b753fc63c2d2a36bb896aba8", textStrings.get(0));
+        assertEquals("ve=02", textStrings.get(1));
+        assertEquals("md=Chromecast", textStrings.get(2));
+        assertEquals("ic=/setup/icon.png", textStrings.get(3));
+        assertEquals("fn=Johnny's Chromecast", textStrings.get(4));
+        assertEquals("ca=5", textStrings.get(5));
+        assertEquals("st=0", textStrings.get(6));
+    }
+
+    @Test
+    public void testDecodeIPv6AnswerPacket() throws IOException {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+        assertNotNull(data6);
+        DatagramPacket packet = new DatagramPacket(data6, data6.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
+        assertNotNull(inet6AddressRecord);
+        Inet4Address inet4Addr = inet6AddressRecord.getInet4Address();
+        assertNull(inet4Addr);
+
+        Inet6Address inet6Addr = inet6AddressRecord.getInet6Address();
+        assertNotNull(inet6Addr);
+        assertEquals(inet6Addr.getHostAddress(), "2000:3333::da6c:63ff:fe7c:7483");
+    }
+
+    @Test
+    public void testIsComplete() {
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        response.clearPointerRecords();
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setInet4AddressRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setInet6AddressRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setServiceRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setTextRecord(null);
+        assertFalse(response.isComplete());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
new file mode 100644
index 0000000..ae16f2b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseTests {
+    private static final String TAG = "MdnsResponseTests";
+    // MDNS response packet for name "test" with an IPv4 address of 10.1.2.3
+    private static final byte[] dataIn_ipv4_1 = HexDump.hexStringToByteArray(
+            "0474657374000001" + "0001000011940004" + "0A010203");
+    // MDNS response packet for name "tess" with an IPv4 address of 10.1.2.4
+    private static final byte[] dataIn_ipv4_2 = HexDump.hexStringToByteArray(
+            "0474657373000001" + "0001000011940004" + "0A010204");
+    // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040
+    private static final byte[] dataIn_ipv6_1 = HexDump.hexStringToByteArray(
+            "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203040");
+    // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3030
+    private static final byte[] dataIn_ipv6_2 = HexDump.hexStringToByteArray(
+            "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203030");
+    // MDNS response w/name "test" & PTR to foo.bar.quxx
+    private static final byte[] dataIn_ptr_1 = HexDump.hexStringToByteArray(
+            "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787800");
+    // MDNS response w/name "test" & PTR to foo.bar.quxy
+    private static final byte[] dataIn_ptr_2 = HexDump.hexStringToByteArray(
+            "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787900");
+    // MDNS response w/name "test" & Service for host foo.bar.quxx
+    private static final byte[] dataIn_service_1 = HexDump.hexStringToByteArray(
+            "0474657374000021"
+            + "0001000011940014"
+            + "000100FF1F480366"
+            + "6F6F036261720471"
+            + "75787800");
+    // MDNS response w/name "test" & Service for host test
+    private static final byte[] dataIn_service_2 = HexDump.hexStringToByteArray(
+            "0474657374000021" + "000100001194000B" + "000100FF1F480474" + "657374");
+    // MDNS response w/name "test" & the following text strings:
+    // "a=hello there", "b=1234567890", and "xyz=!$$$"
+    private static final byte[] dataIn_text_1 = HexDump.hexStringToByteArray(
+            "0474657374000010"
+            + "0001000011940024"
+            + "0D613D68656C6C6F"
+            + "2074686572650C62"
+            + "3D31323334353637"
+            + "3839300878797A3D"
+            + "21242424");
+    // MDNS response w/name "test" & the following text strings:
+    // "a=hello there", "b=1234567890", and "xyz=!@#$"
+    private static final byte[] dataIn_text_2 = HexDump.hexStringToByteArray(
+            "0474657374000010"
+            + "0001000011940024"
+            + "0D613D68656C6C6F"
+            + "2074686572650C62"
+            + "3D31323334353637"
+            + "3839300878797A3D"
+            + "21402324");
+
+    // The following helper classes act as wrappers so that IPv4 and IPv6 address records can
+    // be explicitly created by type using same constructor signature as all other records.
+    static class MdnsInet4AddressRecord extends MdnsInetAddressRecord {
+        public MdnsInet4AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+            super(name, MdnsRecord.TYPE_A, reader);
+        }
+    }
+
+    static class MdnsInet6AddressRecord extends MdnsInetAddressRecord {
+        public MdnsInet6AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+            super(name, MdnsRecord.TYPE_AAAA, reader);
+        }
+    }
+
+    // This helper class just wraps the data bytes of a response packet with the contained record
+    // type.
+    // Its only purpose is to make the test code a bit more readable.
+    static class PacketAndRecordClass {
+        public final byte[] packetData;
+        public final Class<?> recordClass;
+
+        public PacketAndRecordClass() {
+            packetData = null;
+            recordClass = null;
+        }
+
+        public PacketAndRecordClass(byte[] data, Class<?> c) {
+            packetData = data;
+            recordClass = c;
+        }
+    }
+
+    // Construct an MdnsResponse with the specified data packets applied.
+    private MdnsResponse makeMdnsResponse(long time, List<PacketAndRecordClass> responseList)
+            throws IOException {
+        MdnsResponse response = new MdnsResponse(time);
+        for (PacketAndRecordClass responseData : responseList) {
+            DatagramPacket packet =
+                    new DatagramPacket(responseData.packetData, responseData.packetData.length);
+            MdnsPacketReader reader = new MdnsPacketReader(packet);
+            String[] name = reader.readLabels();
+            reader.skip(2); // skip record type indication.
+            // Apply the right kind of record to the response.
+            if (responseData.recordClass == MdnsInet4AddressRecord.class) {
+                response.setInet4AddressRecord(new MdnsInet4AddressRecord(name, reader));
+            } else if (responseData.recordClass == MdnsInet6AddressRecord.class) {
+                response.setInet6AddressRecord(new MdnsInet6AddressRecord(name, reader));
+            } else if (responseData.recordClass == MdnsPointerRecord.class) {
+                response.addPointerRecord(new MdnsPointerRecord(name, reader));
+            } else if (responseData.recordClass == MdnsServiceRecord.class) {
+                response.setServiceRecord(new MdnsServiceRecord(name, reader));
+            } else if (responseData.recordClass == MdnsTextRecord.class) {
+                response.setTextRecord(new MdnsTextRecord(name, reader));
+            } else {
+                fail("Unsupported/unexpected MdnsRecord subtype used in test - invalid test!");
+            }
+        }
+        return response;
+    }
+
+    @Test
+    public void getInet4AddressRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ipv4_1, dataIn_ipv4_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasInet4AddressRecord());
+        assertTrue(response.setInet4AddressRecord(record));
+        assertEquals(response.getInet4AddressRecord(), record);
+    }
+
+    @Test
+    public void getInet6AddressRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ipv6_1, dataIn_ipv6_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsInetAddressRecord record =
+                new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasInet6AddressRecord());
+        assertTrue(response.setInet6AddressRecord(record));
+        assertEquals(response.getInet6AddressRecord(), record);
+    }
+
+    @Test
+    public void getPointerRecords_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ptr_1, dataIn_ptr_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasPointerRecords());
+        assertTrue(response.addPointerRecord(record));
+        List<MdnsPointerRecord> recordList = response.getPointerRecords();
+        assertNotNull(recordList);
+        assertEquals(1, recordList.size());
+        assertEquals(record, recordList.get(0));
+    }
+
+    @Test
+    public void getServiceRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_service_1, dataIn_service_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasServiceRecord());
+        assertTrue(response.setServiceRecord(record));
+        assertEquals(response.getServiceRecord(), record);
+    }
+
+    @Test
+    public void getTextRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_text_1, dataIn_text_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsTextRecord record = new MdnsTextRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasTextRecord());
+        assertTrue(response.setTextRecord(record));
+        assertEquals(response.getTextRecord(), record);
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_ipv4_address() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_2, MdnsInet4AddressRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_ipv6_address() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv6_2, MdnsInet6AddressRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_text() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_text_2, MdnsTextRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_service() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_service_1, MdnsServiceRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_pointer() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_ptr_2, MdnsPointerRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void mergeRecordsFrom_indicates_noChange() throws IOException {
+        //MdnsConfigsFlagsImpl.useReducedMergeRecordUpdateEvents.override(true);
+        List<PacketAndRecordClass> recordList =
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class),
+                        new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class),
+                        new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class),
+                        new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class),
+                        new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class));
+        // Create a two identical responses.
+        MdnsResponse response = makeMdnsResponse(0, recordList);
+        MdnsResponse response2 = makeMdnsResponse(100, recordList);
+        // Merging should not indicate any change.
+        assertFalse(response.mergeRecordsFrom(response2));
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
new file mode 100644
index 0000000..5843fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+
+import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link MdnsServiceTypeClient}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsServiceTypeClientTests {
+
+    private static final String SERVICE_TYPE = "_googlecast._tcp.local";
+
+    @Mock
+    private MdnsServiceBrowserListener mockListenerOne;
+    @Mock
+    private MdnsServiceBrowserListener mockListenerTwo;
+    @Mock
+    private MdnsPacketWriter mockPacketWriter;
+    @Mock
+    private MdnsSocketClient mockSocketClient;
+    @Captor
+    private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
+
+    private final byte[] buf = new byte[10];
+
+    private DatagramPacket[] expectedPackets;
+    private ScheduledFuture<?>[] expectedSendFutures;
+    private FakeExecutor currentThreadExecutor = new FakeExecutor();
+
+    private MdnsServiceTypeClient client;
+
+    @Before
+    @SuppressWarnings("DoNotMock")
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+
+        expectedPackets = new DatagramPacket[16];
+        expectedSendFutures = new ScheduledFuture<?>[16];
+
+        for (int i = 0; i < expectedSendFutures.length; ++i) {
+            expectedPackets[i] = new DatagramPacket(buf, 0, 5);
+            expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
+        }
+        when(mockPacketWriter.getPacket(any(SocketAddress.class)))
+                .thenReturn(expectedPackets[0])
+                .thenReturn(expectedPackets[1])
+                .thenReturn(expectedPackets[2])
+                .thenReturn(expectedPackets[3])
+                .thenReturn(expectedPackets[4])
+                .thenReturn(expectedPackets[5])
+                .thenReturn(expectedPackets[6])
+                .thenReturn(expectedPackets[7])
+                .thenReturn(expectedPackets[8])
+                .thenReturn(expectedPackets[9])
+                .thenReturn(expectedPackets[10])
+                .thenReturn(expectedPackets[11])
+                .thenReturn(expectedPackets[12])
+                .thenReturn(expectedPackets[13])
+                .thenReturn(expectedPackets[14])
+                .thenReturn(expectedPackets[15]);
+
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+    }
+
+    @Test
+    public void sendQueries_activeScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, 3 queries.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
+        verifyAndSendQuery(
+                3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
+        verifyAndSendQuery(
+                6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
+        verifyAndSendQuery(
+                9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Fifth burst will be sent after timeBetweenBurstsMs, 3 queries.
+        verifyAndSendQuery(12, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[15]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_reentry_activeScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, first query is sent.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+        // After the first query is sent, change the subtypes, and restart.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(false)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // The previous scheduled task should be canceled.
+        verify(expectedSendFutures[1]).cancel(true);
+
+        // Queries should continue to be sent.
+        verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_passiveScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, 3 query.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Second burst will be sent after timeBetweenBurstsMs, 1 query.
+        verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+        // Third burst will be sent after timeBetweenBurstsMs, 1 query.
+        verifyAndSendQuery(4, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_reentry_passiveScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, first query is sent.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+        // After the first query is sent, change the subtypes, and restart.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(true)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // The previous scheduled task should be canceled.
+        verify(expectedSendFutures[1]).cancel(true);
+
+        // Queries should continue to be sent.
+        verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
+        //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        QueryTaskConfig config =
+                new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+        // This is the first query. We will ask for unicast response.
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, 1);
+
+        // For the rest of queries in this burst, we will NOT ask for unicast response.
+        for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+            int oldTransactionId = config.transactionId;
+            config = config.getConfigForNextRun();
+            assertFalse(config.expectUnicastResponse);
+            assertEquals(config.subtypes, searchOptions.getSubtypes());
+            assertEquals(config.transactionId, oldTransactionId + 1);
+        }
+
+        // This is the first query of a new burst. We will ask for unicast response.
+        int oldTransactionId = config.transactionId;
+        config = config.getConfigForNextRun();
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, oldTransactionId + 1);
+    }
+
+    @Test
+    public void testQueryTaskConfig_askForUnicastInFirstQuery() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        QueryTaskConfig config =
+                new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+        // This is the first query. We will ask for unicast response.
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, 1);
+
+        // For the rest of queries in this burst, we will NOT ask for unicast response.
+        for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+            int oldTransactionId = config.transactionId;
+            config = config.getConfigForNextRun();
+            assertFalse(config.expectUnicastResponse);
+            assertEquals(config.subtypes, searchOptions.getSubtypes());
+            assertEquals(config.transactionId, oldTransactionId + 1);
+        }
+
+        // This is the first query of a new burst. We will NOT ask for unicast response.
+        int oldTransactionId = config.transactionId;
+        config = config.getConfigForNextRun();
+        assertFalse(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, oldTransactionId + 1);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
+        //MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Change the sutypes and start a new session.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(true)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the first mdns task is not successful canceled and it gets
+        // executed anyway.
+        firstMdnsTask.run();
+
+        // Although it gets executes, no more task gets scheduled.
+        assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testIfPreviousTaskIsCanceledWhenSessionStops() {
+        //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // Change the sutypes and start a new session.
+        client.stopSendAndReceive(mockListenerOne);
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the first mdns task is not successful canceled and it gets
+        // executed anyway.
+        currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+        // Although it gets executes, no more task gets scheduled.
+        assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+    }
+
+    @Test
+    public void processResponse_incompleteResponse() {
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse response = mock(MdnsResponse.class);
+        when(response.getServiceInstanceName()).thenReturn("service-instance-1");
+        when(response.isComplete()).thenReturn(false);
+
+        client.processResponse(response);
+
+        verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
+        verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
+    }
+
+    @Test
+    public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
+        final String ipV4Address = "192.168.1.1";
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV4Address,
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        // Process a second response with a different port and updated text attributes.
+        MdnsResponse secondResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV4Address,
+                        5354,
+                        Collections.singletonList("ABCDE"),
+                        Collections.singletonMap("key", "value"));
+        client.processResponse(secondResponse);
+
+        // Verify onServiceFound was called once for the initial response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
+        assertEquals(initialServiceInfo.getPort(), 5353);
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+        // Verify onServiceUpdated was called once for the second response.
+        verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
+        assertEquals(updatedServiceInfo.getPort(), 5354);
+        assertTrue(updatedServiceInfo.hasSubtypes());
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+    }
+
+    @Test
+    public void processIPv6Response_getCorrectServiceInfo() throws Exception {
+        final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV6Address,
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        // Process a second response with a different port and updated text attributes.
+        MdnsResponse secondResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV6Address,
+                        5354,
+                        Collections.singletonList("ABCDE"),
+                        Collections.singletonMap("key", "value"));
+        client.processResponse(secondResponse);
+
+        System.out.println("secondResponses ip"
+                + secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
+
+        // Verify onServiceFound was called once for the initial response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
+        assertEquals(initialServiceInfo.getPort(), 5353);
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+        // Verify onServiceUpdated was called once for the second response.
+        verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
+        assertEquals(updatedServiceInfo.getPort(), 5354);
+        assertTrue(updatedServiceInfo.hasSubtypes());
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+    }
+
+    @Test
+    public void processResponse_goodBye() {
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse response = mock(MdnsResponse.class);
+        when(response.getServiceInstanceName()).thenReturn("goodbye-service-instance-name");
+        when(response.isGoodbye()).thenReturn(true);
+        client.processResponse(response);
+
+        verify(mockListenerOne).onServiceRemoved("goodbye-service-instance-name");
+        verify(mockListenerTwo).onServiceRemoved("goodbye-service-instance-name");
+    }
+
+    @Test
+    public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        "192.168.1.1",
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Verify onServiceFound was called once for the existing response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
+        assertEquals(existingServiceInfo.getPort(), 5353);
+        assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(existingServiceInfo.getAttributeByKey("key"));
+
+        // Process a goodbye message for the existing response.
+        MdnsResponse goodByeResponse = mock(MdnsResponse.class);
+        when(goodByeResponse.getServiceInstanceName()).thenReturn("service-instance-1");
+        when(goodByeResponse.isGoodbye()).thenReturn(true);
+        client.processResponse(goodByeResponse);
+
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        // Verify onServiceFound was not called on the newly registered listener after the existing
+        // response is gone.
+        verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
+    }
+
+    @Test
+    public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
+        final String serviceInstanceName = "service-instance-1";
+        client.startSendAndReceive(
+                mockListenerOne,
+                MdnsSearchOptions.newBuilder().build());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
+            throws Exception {
+        //MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is under TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was called.
+        verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
+            throws Exception {
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void processResponse_removeServiceAfterTtlExpiresEnabled_shouldRemove()
+            throws Exception {
+        //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+    }
+
+    // verifies that the right query was enqueued with the right delay, and send query by executing
+    // the runnable.
+    private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
+        assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        if (expectsUnicastResponse) {
+            verify(mockSocketClient).sendUnicastPacket(expectedPackets[index]);
+        } else {
+            verify(mockSocketClient).sendMulticastPacket(expectedPackets[index]);
+        }
+    }
+
+    // A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
+    // time.
+    private class FakeExecutor extends ScheduledThreadPoolExecutor {
+        private long lastScheduledDelayInMs;
+        private Runnable lastScheduledRunnable;
+        private Runnable lastSubmittedRunnable;
+        private int futureIndex;
+
+        FakeExecutor() {
+            super(1);
+            lastScheduledDelayInMs = -1;
+        }
+
+        @Override
+        public Future<?> submit(Runnable command) {
+            Future<?> future = super.submit(command);
+            lastSubmittedRunnable = command;
+            return future;
+        }
+
+        // Don't call through the real implementation, just track the scheduled Runnable, and
+        // returns a ScheduledFuture.
+        @Override
+        public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+            lastScheduledDelayInMs = delay;
+            lastScheduledRunnable = command;
+            return expectedSendFutures[futureIndex++];
+        }
+
+        // Returns the delay of the last scheduled task, and clear it.
+        long getAndClearLastScheduledDelayInMs() {
+            long val = lastScheduledDelayInMs;
+            lastScheduledDelayInMs = -1;
+            return val;
+        }
+
+        // Returns the last scheduled task, and clear it.
+        Runnable getAndClearLastScheduledRunnable() {
+            Runnable val = lastScheduledRunnable;
+            lastScheduledRunnable = null;
+            return val;
+        }
+
+        Runnable getAndClearSubmittedRunnable() {
+            Runnable val = lastSubmittedRunnable;
+            lastSubmittedRunnable = null;
+            return val;
+        }
+    }
+
+    // Creates a complete mDNS response.
+    private MdnsResponse createResponse(
+            @NonNull String serviceInstanceName,
+            @NonNull String host,
+            int port,
+            @NonNull List<String> subtypes,
+            @NonNull Map<String, String> textAttributes)
+            throws UnknownHostException {
+        String[] hostName = new String[]{"hostname"};
+        MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
+        when(serviceRecord.getServiceHost()).thenReturn(hostName);
+        when(serviceRecord.getServicePort()).thenReturn(port);
+
+        MdnsResponse response = spy(new MdnsResponse(0));
+
+        MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class);
+        if (host.contains(":")) {
+            when(inetAddressRecord.getInet6Address())
+                    .thenReturn((Inet6Address) Inet6Address.getByName(host));
+            response.setInet6AddressRecord(inetAddressRecord);
+        } else {
+            when(inetAddressRecord.getInet4Address())
+                    .thenReturn((Inet4Address) Inet4Address.getByName(host));
+            response.setInet4AddressRecord(inetAddressRecord);
+        }
+
+        MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
+        List<String> textStrings = new ArrayList<>();
+        for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
+            textStrings.add(kv.getKey() + "=" + kv.getValue());
+        }
+        when(textRecord.getStrings()).thenReturn(textStrings);
+
+        response.setServiceRecord(serviceRecord);
+        response.setTextRecord(textRecord);
+
+        doReturn(false).when(response).isGoodbye();
+        doReturn(true).when(response).isComplete();
+        doReturn(serviceInstanceName).when(response).getServiceInstanceName();
+        doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
+        return response;
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
new file mode 100644
index 0000000..21ed7eb
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest.permission;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.text.format.DateUtils;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests for {@link MdnsSocketClient} */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketClientTests {
+    private static final long TIMEOUT = 500;
+    private final byte[] buf = new byte[10];
+    final AtomicBoolean enableMulticastResponse = new AtomicBoolean(true);
+    final AtomicBoolean enableUnicastResponse = new AtomicBoolean(true);
+
+    @Mock private Context mContext;
+    @Mock private WifiManager mockWifiManager;
+    @Mock private MdnsSocket mockMulticastSocket;
+    @Mock private MdnsSocket mockUnicastSocket;
+    @Mock private MulticastLock mockMulticastLock;
+    @Mock private MdnsSocketClient.Callback mockCallback;
+
+    private MdnsSocketClient mdnsClient;
+
+    @Before
+    public void setup() throws RuntimeException, IOException {
+        MockitoAnnotations.initMocks(this);
+
+        when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
+                .thenReturn(mockMulticastLock);
+
+        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+                    @Override
+                    MdnsSocket createMdnsSocket(int port) throws IOException {
+                        if (port == MdnsConstants.MDNS_PORT) {
+                            return mockMulticastSocket;
+                        }
+                        return mockUnicastSocket;
+                    }
+                };
+        mdnsClient.setCallback(mockCallback);
+
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray(
+                            "0000840000000004"
+                            + "00000003134A6F68"
+                            + "6E6E792773204368"
+                            + "726F6D6563617374"
+                            + "0B5F676F6F676C65"
+                            + "63617374045F7463"
+                            + "70056C6F63616C00"
+                            + "0010800100001194"
+                            + "006C2369643D3937"
+                            + "3062663534376237"
+                            + "3533666336336332"
+                            + "6432613336626238"
+                            + "3936616261380576"
+                            + "653D30320D6D643D"
+                            + "4368726F6D656361"
+                            + "73741269633D2F73"
+                            + "657475702F69636F"
+                            + "6E2E706E6716666E"
+                            + "3D4A6F686E6E7927"
+                            + "73204368726F6D65"
+                            + "636173740463613D"
+                            + "350473743D30095F"
+                            + "7365727669636573"
+                            + "075F646E732D7364"
+                            + "045F756470C03100"
+                            + "0C00010000119400"
+                            + "02C020C020000C00"
+                            + "01000011940002C0"
+                            + "0CC00C0021800100"
+                            + "000078001C000000"
+                            + "001F49134A6F686E"
+                            + "6E79277320436872"
+                            + "6F6D6563617374C0"
+                            + "31C0F30001800100"
+                            + "0000780004C0A864"
+                            + "68C0F3002F800100"
+                            + "0000780005C0F300"
+                            + "0140C00C002F8001"
+                            + "000011940009C00C"
+                            + "00050000800040");
+                    if (enableMulticastResponse.get()) {
+                        DatagramPacket packet = invocationOnMock.getArgument(0);
+                        packet.setData(dataIn);
+                    }
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray(
+                            "0000840000000004"
+                            + "00000003134A6F68"
+                            + "6E6E792773204368"
+                            + "726F6D6563617374"
+                            + "0B5F676F6F676C65"
+                            + "63617374045F7463"
+                            + "70056C6F63616C00"
+                            + "0010800100001194"
+                            + "006C2369643D3937"
+                            + "3062663534376237"
+                            + "3533666336336332"
+                            + "6432613336626238"
+                            + "3936616261380576"
+                            + "653D30320D6D643D"
+                            + "4368726F6D656361"
+                            + "73741269633D2F73"
+                            + "657475702F69636F"
+                            + "6E2E706E6716666E"
+                            + "3D4A6F686E6E7927"
+                            + "73204368726F6D65"
+                            + "636173740463613D"
+                            + "350473743D30095F"
+                            + "7365727669636573"
+                            + "075F646E732D7364"
+                            + "045F756470C03100"
+                            + "0C00010000119400"
+                            + "02C020C020000C00"
+                            + "01000011940002C0"
+                            + "0CC00C0021800100"
+                            + "000078001C000000"
+                            + "001F49134A6F686E"
+                            + "6E79277320436872"
+                            + "6F6D6563617374C0"
+                            + "31C0F30001800100"
+                            + "0000780004C0A864"
+                            + "68C0F3002F800100"
+                            + "0000780005C0F300"
+                            + "0140C00C002F8001"
+                            + "000011940009C00C"
+                            + "00050000800040");
+                    if (enableUnicastResponse.get()) {
+                        DatagramPacket packet = invocationOnMock.getArgument(0);
+                        packet.setData(dataIn);
+                    }
+                    return null;
+                })
+                .when(mockUnicastSocket)
+                .receive(any(DatagramPacket.class));
+    }
+
+    @After
+    public void tearDown() {
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testSendPackets_useSeparateSocketForUnicast()
+            throws InterruptedException, IOException {
+        //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+        //        .override(DateUtils.SECOND_IN_MILLIS);
+        mdnsClient.startDiscovery();
+        Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+        Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+        Thread sendThread = mdnsClient.sendThread;
+
+        assertTrue(multicastReceiverThread.isAlive());
+        assertTrue(sendThread.isAlive());
+        assertTrue(unicastReceiverThread.isAlive());
+
+        // Sends a packet.
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendMulticastPacket(packet);
+        // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+        // it may not be called yet. So timeout is added.
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Verify the packet is sent by the unicast socket.
+        mdnsClient.sendUnicastPacket(packet);
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
+
+        // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+        // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+        // for a long time (the foreground thread can fail the test early).
+        final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+        Thread testThread =
+                new Thread(
+                        new Runnable() {
+                            @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+                            @Override
+                            public void run() {
+                                mdnsClient.stopDiscovery();
+                                stopDiscoveryLatch.countDown();
+                            }
+                        });
+        testThread.start();
+        assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+        // We should be able to join in a reasonable amount of time, to prove that the
+        // the MdnsClient exited without sending the large queue of packets.
+        testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+        assertFalse(multicastReceiverThread.isAlive());
+        assertFalse(sendThread.isAlive());
+        assertFalse(unicastReceiverThread.isAlive());
+    }
+
+    @Test
+    public void testSendPackets_useSameSocketForMulticastAndUnicast()
+            throws InterruptedException, IOException {
+        mdnsClient.startDiscovery();
+        Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+        Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+        Thread sendThread = mdnsClient.sendThread;
+
+        assertTrue(multicastReceiverThread.isAlive());
+        assertTrue(sendThread.isAlive());
+        assertNull(unicastReceiverThread);
+
+        // Sends a packet.
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendMulticastPacket(packet);
+        // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+        // it may not be called yet. So timeout is added.
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Verify the packet is sent by the multicast socket as well.
+        mdnsClient.sendUnicastPacket(packet);
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+        // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+        // for a long time (the foreground thread can fail the test early).
+        final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+        Thread testThread =
+                new Thread(
+                        new Runnable() {
+                            @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+                            @Override
+                            public void run() {
+                                mdnsClient.stopDiscovery();
+                                stopDiscoveryLatch.countDown();
+                            }
+                        });
+        testThread.start();
+        assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+        // We should be able to join in a reasonable amount of time, to prove that the
+        // the MdnsClient exited without sending the large queue of packets.
+        testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+        assertFalse(multicastReceiverThread.isAlive());
+        assertFalse(sendThread.isAlive());
+        assertNull(unicastReceiverThread);
+    }
+
+    @Test
+    public void testStartStop() throws IOException {
+        for (int i = 0; i < 5; i++) {
+            mdnsClient.startDiscovery();
+
+            Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+            Thread socketThread = mdnsClient.sendThread;
+
+            assertTrue(multicastReceiverThread.isAlive());
+            assertTrue(socketThread.isAlive());
+
+            mdnsClient.stopDiscovery();
+
+            assertFalse(multicastReceiverThread.isAlive());
+            assertFalse(socketThread.isAlive());
+        }
+    }
+
+    @Test
+    public void testStopDiscovery_queueIsCleared() throws IOException {
+        mdnsClient.startDiscovery();
+        mdnsClient.stopDiscovery();
+        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+        }
+    }
+
+    @Test
+    public void testSendPacket_afterDiscoveryStops() throws IOException {
+        mdnsClient.startDiscovery();
+        mdnsClient.stopDiscovery();
+        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+        }
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testSendPacket_queueReachesSizeLimit() throws IOException {
+        //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
+        mdnsClient.startDiscovery();
+        for (int i = 0; i < 100; i++) {
+            mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+        }
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.size() <= 2);
+        }
+    }
+
+    @Test
+    public void testMulticastResponseReceived_useSeparateSocketForUnicast() throws IOException {
+        mdnsClient.setCallback(mockCallback);
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onResponseReceived(any(MdnsResponse.class));
+    }
+
+    @Test
+    public void testMulticastResponseReceived_useSameSocketForMulticastAndUnicast()
+            throws Exception {
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
+                .onResponseReceived(any(MdnsResponse.class));
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    public void testFailedToParseMdnsResponse_useSeparateSocketForUnicast() throws IOException {
+        mdnsClient.setCallback(mockCallback);
+
+        // Both multicast socket and unicast socket receive malformed responses.
+        byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    // Malformed data.
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    // Malformed data.
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockUnicastSocket)
+                .receive(any(DatagramPacket.class));
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onFailedToParseMdnsResponse(anyInt(), eq(MdnsResponseErrorCode.ERROR_END_OF_FILE));
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    public void testFailedToParseMdnsResponse_useSameSocketForMulticastAndUnicast()
+            throws IOException {
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onFailedToParseMdnsResponse(1, MdnsResponseErrorCode.ERROR_END_OF_FILE);
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testMulticastResponseIsNotReceived() throws IOException, InterruptedException {
+        //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+        //        .override(DateUtils.SECOND_IN_MILLIS);
+        //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+        enableMulticastResponse.set(false);
+        enableUnicastResponse.set(true);
+
+        mdnsClient.startDiscovery();
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendMulticastPacket(packet);
+
+        // Wait for the timer to be triggered.
+        Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertTrue(mdnsClient.receivedUnicastResponse);
+        assertTrue(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        // Allow multicast response and verify the states again.
+        enableMulticastResponse.set(true);
+        Thread.sleep(DateUtils.SECOND_IN_MILLIS);
+
+        // Verify cannotReceiveMulticastResponse is reset to false.
+        assertTrue(mdnsClient.receivedMulticastResponse);
+        assertTrue(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        // Stop the discovery and start a new session. Don't respond the unicsat query either in
+        // this session.
+        enableMulticastResponse.set(false);
+        enableUnicastResponse.set(false);
+        mdnsClient.stopDiscovery();
+        mdnsClient.startDiscovery();
+
+        // Verify the states are reset.
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertFalse(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendMulticastPacket(packet);
+        Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+        // Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
+        // unicast response either. This is expected for users who don't have any cast device.
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertFalse(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
new file mode 100644
index 0000000..9f11a4b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsSocket}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketTests {
+
+    @Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
+    @Mock private MulticastSocket mockMulticastSocket;
+    @Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
+    private SocketAddress socketIPv4Address;
+    private SocketAddress socketIPv6Address;
+
+    private byte[] data = new byte[25];
+    private final DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+    private NetworkInterface networkInterface;
+
+    private MdnsSocket mdnsSocket;
+
+    @Before
+    public void setUp() throws SocketException, UnknownHostException {
+        MockitoAnnotations.initMocks(this);
+
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+        socketIPv4Address = new InetSocketAddress(
+                InetAddress.getByName("224.0.0.251"), MdnsConstants.MDNS_PORT);
+        socketIPv6Address = new InetSocketAddress(
+                InetAddress.getByName("FF02::FB"), MdnsConstants.MDNS_PORT);
+    }
+
+    @Test
+    public void testMdnsSocket() throws IOException {
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+        mdnsSocket.send(datagramPacket);
+        verify(mockMulticastSocket).setNetworkInterface(networkInterface);
+        verify(mockMulticastSocket).send(datagramPacket);
+
+        mdnsSocket.receive(datagramPacket);
+        verify(mockMulticastSocket).receive(datagramPacket);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv4Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv4Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    @Test
+    public void testIPv6OnlyNetwork_IPv6Enabled() throws IOException {
+        // Have mockMulticastNetworkInterfaceProvider send back an IPv6Only networkInterfaceWrapper
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+
+        when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+                Collections.singletonList(mockNetworkInterfaceWrapper)))
+                .thenReturn(true);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    @Test
+    public void testIPv6OnlyNetwork_IPv6Toggle() throws IOException {
+        // Have mockMulticastNetworkInterfaceProvider send back a networkInterfaceWrapper
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+
+        when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+                Collections.singletonList(mockNetworkInterfaceWrapper)))
+                .thenReturn(true);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    private NetworkInterface createEmptyNetworkInterface() {
+        try {
+            Constructor<NetworkInterface> constructor =
+                    NetworkInterface.class.getDeclaredConstructor();
+            constructor.setAccessible(true);
+            return constructor.newInstance();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
new file mode 100644
index 0000000..2268dfe
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MulticastNetworkInterfaceProvider}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MulticastNetworkInterfaceProviderTests {
+
+    @Mock private NetworkInterfaceWrapper loopbackInterface;
+    @Mock private NetworkInterfaceWrapper pointToPointInterface;
+    @Mock private NetworkInterfaceWrapper virtualInterface;
+    @Mock private NetworkInterfaceWrapper inactiveMulticastInterface;
+    @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterface;
+    @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterfaceTwo;
+    @Mock private NetworkInterfaceWrapper nonMulticastInterface;
+    @Mock private NetworkInterfaceWrapper multicastInterfaceOne;
+    @Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
+
+    private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
+    private MulticastNetworkInterfaceProvider provider;
+    private Context context;
+
+    @Before
+    public void setUp() throws SocketException, UnknownHostException {
+        MockitoAnnotations.initMocks(this);
+        context = InstrumentationRegistry.getContext();
+
+        setupNetworkInterface(
+                loopbackInterface,
+                true /* isUp */,
+                true /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                pointToPointInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                true /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                virtualInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                true /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                inactiveMulticastInterface,
+                false /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                nonMulticastInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                false /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                activeIpv6MulticastInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                true /* isIpv6 */);
+
+        setupNetworkInterface(
+                activeIpv6MulticastInterfaceTwo,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                true /* isIpv6 */);
+
+        setupNetworkInterface(
+                multicastInterfaceOne,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                multicastInterfaceTwo,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        provider =
+                new MulticastNetworkInterfaceProvider(context) {
+                    @Override
+                    List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+                        return networkInterfaces;
+                    }
+                };
+    }
+
+    @Test
+    public void testGetMulticastNetworkInterfaces() {
+        // getNetworkInterfaces returns 1 multicast interface and 5 interfaces that can not be used
+        // to send and receive multicast packets.
+        networkInterfaces.add(loopbackInterface);
+        networkInterfaces.add(pointToPointInterface);
+        networkInterfaces.add(virtualInterface);
+        networkInterfaces.add(inactiveMulticastInterface);
+        networkInterfaces.add(nonMulticastInterface);
+        networkInterfaces.add(multicastInterfaceOne);
+
+        assertEquals(Collections.singletonList(multicastInterfaceOne),
+                provider.getMulticastNetworkInterfaces());
+
+        // getNetworkInterfaces returns 2 multicast interfaces after a connectivity change.
+        networkInterfaces.clear();
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(multicastInterfaceTwo);
+
+        provider.connectivityMonitor.notifyConnectivityChange();
+
+        assertEquals(networkInterfaces, provider.getMulticastNetworkInterfaces());
+    }
+
+    @Test
+    public void testStartWatchingConnectivityChanges() {
+        ConnectivityMonitor mockMonitor = mock(ConnectivityMonitor.class);
+        provider.connectivityMonitor = mockMonitor;
+
+        InOrder inOrder = inOrder(mockMonitor);
+
+        provider.startWatchingConnectivityChanges();
+        inOrder.verify(mockMonitor).startWatchingConnectivityChanges();
+
+        provider.stopWatchingConnectivityChanges();
+        inOrder.verify(mockMonitor).stopWatchingConnectivityChanges();
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_EmptyNetwork() {
+        // getNetworkInterfaces returns no network interfaces.
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv4Only() {
+        // getNetworkInterfaces returns two IPv4 network interface.
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(multicastInterfaceTwo);
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_MixedNetwork() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+        networkInterfaces.add(multicastInterfaceTwo);
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv6Only() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+        assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv6Enabled() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+
+        final List<NetworkInterfaceWrapper> interfaces = provider.getMulticastNetworkInterfaces();
+        assertEquals(Collections.singletonList(activeIpv6MulticastInterface), interfaces);
+    }
+
+    private void setupNetworkInterface(
+            @NonNull NetworkInterfaceWrapper networkInterfaceWrapper,
+            boolean isUp,
+            boolean isLoopback,
+            boolean isPointToPoint,
+            boolean isVirtual,
+            boolean supportsMulticast,
+            boolean isIpv6)
+            throws SocketException, UnknownHostException {
+        when(networkInterfaceWrapper.isUp()).thenReturn(isUp);
+        when(networkInterfaceWrapper.isLoopback()).thenReturn(isLoopback);
+        when(networkInterfaceWrapper.isPointToPoint()).thenReturn(isPointToPoint);
+        when(networkInterfaceWrapper.isVirtual()).thenReturn(isVirtual);
+        when(networkInterfaceWrapper.supportsMulticast()).thenReturn(supportsMulticast);
+        if (isIpv6) {
+            InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+            InetAddress ip6Address = Inet6Address.getByName("2001:4860:0:1001::68");
+            when(interfaceAddress.getAddress()).thenReturn(ip6Address);
+            when(networkInterfaceWrapper.getInterfaceAddresses())
+                    .thenReturn(Collections.singletonList(interfaceAddress));
+        } else {
+            Inet4Address ip = (Inet4Address) Inet4Address.getByName("192.168.0.1");
+            InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+            when(interfaceAddress.getAddress()).thenReturn(ip);
+            when(networkInterfaceWrapper.getInterfaceAddresses())
+                    .thenReturn(Collections.singletonList(interfaceAddress));
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
new file mode 100644
index 0000000..a9f80ea
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ethernet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.util.ArrayMap;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class EthernetConfigStoreTest {
+    private static final LinkAddress LINKADDR = new LinkAddress("192.168.1.100/25");
+    private static final InetAddress GATEWAY = InetAddresses.parseNumericAddress("192.168.1.1");
+    private static final InetAddress DNS1 = InetAddresses.parseNumericAddress("8.8.8.8");
+    private static final InetAddress DNS2 = InetAddresses.parseNumericAddress("8.8.4.4");
+    private static final StaticIpConfiguration STATIC_IP_CONFIG =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(LINKADDR)
+                    .setGateway(GATEWAY)
+                    .setDnsServers(new ArrayList<InetAddress>(
+                            List.of(DNS1, DNS2)))
+                    .build();
+    private static final ProxyInfo PROXY_INFO = ProxyInfo.buildDirectProxy("test", 8888);
+    private static final IpConfiguration APEX_IP_CONFIG =
+            new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+    private static final IpConfiguration LEGACY_IP_CONFIG =
+            new IpConfiguration(IpAssignment.STATIC, ProxySettings.STATIC, STATIC_IP_CONFIG,
+                    PROXY_INFO);
+
+    private EthernetConfigStore mEthernetConfigStore;
+    private File mApexTestDir;
+    private File mLegacyTestDir;
+    private File mApexConfigFile;
+    private File mLegacyConfigFile;
+
+    private void createTestDir() {
+        final Context context = InstrumentationRegistry.getContext();
+        final File baseDir = context.getFilesDir();
+        mApexTestDir = new File(baseDir.getPath() + "/apex");
+        mApexTestDir.mkdirs();
+
+        mLegacyTestDir = new File(baseDir.getPath() + "/legacy");
+        mLegacyTestDir.mkdirs();
+    }
+
+    @Before
+    public void setUp() {
+        createTestDir();
+        mEthernetConfigStore = new EthernetConfigStore();
+    }
+
+    @After
+    public void tearDown() {
+        mApexTestDir.delete();
+        mLegacyTestDir.delete();
+    }
+
+    private void assertConfigFileExist(final String filepath) {
+        assertTrue(new File(filepath).exists());
+    }
+
+    /** Wait for the delayed write operation completes. */
+    private void waitForMs(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (final InterruptedException e) {
+            fail("Thread was interrupted");
+        }
+    }
+
+    @Test
+    public void testWriteIpConfigToApexFilePathAndRead() throws Exception {
+        // Write the config file to the apex file path, pretend the config file exits and
+        // check if IP config should be read from apex file path.
+        mApexConfigFile = new File(mApexTestDir.getPath(), "test.txt");
+        mEthernetConfigStore.write("eth0", APEX_IP_CONFIG, mApexConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(APEX_IP_CONFIG, ipConfigurations.get("eth0"));
+
+        mApexConfigFile.delete();
+    }
+
+    @Test
+    public void testWriteIpConfigToLegacyFilePathAndRead() throws Exception {
+        // Write the config file to the legacy file path, pretend the config file exits and
+        // check if IP config should be read from legacy file path.
+        mLegacyConfigFile = new File(mLegacyTestDir, "test.txt");
+        mEthernetConfigStore.write("0", LEGACY_IP_CONFIG, mLegacyConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(LEGACY_IP_CONFIG, ipConfigurations.get("0"));
+
+        // Check the same config file in apex file path is created.
+        assertConfigFileExist(mApexTestDir.getPath() + "/test.txt");
+
+        final File apexConfigFile = new File(mApexTestDir.getPath() + "/test.txt");
+        apexConfigFile.delete();
+        mLegacyConfigFile.delete();
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index dfb4fcc..2178b33 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.ethernet;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
@@ -32,7 +30,6 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,20 +48,22 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
 import android.net.NetworkRequest;
 import android.net.StaticIpConfiguration;
 import android.net.ip.IpClientCallbacks;
 import android.net.ip.IpClientManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.test.TestLooper;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.InterfaceParams;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -79,8 +78,9 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class EthernetNetworkFactoryTest {
     private static final int TIMEOUT_MS = 2_000;
     private static final String TEST_IFACE = "test123";
@@ -92,6 +92,8 @@
     private Handler mHandler;
     private EthernetNetworkFactory mNetFactory = null;
     private IpClientCallbacks mIpClientCallbacks;
+    private NetworkOfferCallback mNetworkOfferCallback;
+    private NetworkRequest mRequestToKeepNetworkUp;
     @Mock private Context mContext;
     @Mock private Resources mResources;
     @Mock private EthernetNetworkFactory.Dependencies mDeps;
@@ -99,6 +101,7 @@
     @Mock private EthernetNetworkAgent mNetworkAgent;
     @Mock private InterfaceParams mInterfaceParams;
     @Mock private Network mMockNetwork;
+    @Mock private NetworkProvider mNetworkProvider;
 
     @Before
     public void setUp() throws Exception {
@@ -112,7 +115,7 @@
     private void initEthernetNetworkFactory() {
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
-        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mNetworkProvider, mDeps);
     }
 
     private void setupNetworkAgentMock() {
@@ -239,9 +242,18 @@
         mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
                 createInterfaceCapsBuilder(transportType).build());
         assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+
+        ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
+                NetworkOfferCallback.class);
+        verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
+        mRequestToKeepNetworkUp = createDefaultRequest();
+        mNetworkOfferCallback = captor.getValue();
+        mNetworkOfferCallback.onNetworkNeeded(mRequestToKeepNetworkUp);
+
         verifyStart(ipConfig);
         clearInvocations(mDeps);
         clearInvocations(mIpClient);
+        clearInvocations(mNetworkProvider);
     }
 
     // creates a provisioned interface
@@ -281,29 +293,15 @@
         // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
         // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
         // then calling onNetworkUnwanted.
-        createAndVerifyProvisionedInterface(iface);
-
-        mNetworkAgent.getCallbacks().onNetworkUnwanted();
-        mLooper.dispatchAll();
-        verifyStop();
+        mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
+                createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
 
         clearInvocations(mIpClient);
         clearInvocations(mNetworkAgent);
     }
 
     @Test
-    public void testAcceptRequest() throws Exception {
-        initEthernetNetworkFactory();
-        createInterfaceUndergoingProvisioning(TEST_IFACE);
-        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
-
-        NetworkRequest wifiRequest = createDefaultRequestBuilder()
-                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
-        assertFalse(mNetFactory.acceptRequest(wifiRequest));
-    }
-
-    @Test
     public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
         initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
@@ -378,36 +376,6 @@
     }
 
     @Test
-    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
-        initEthernetNetworkFactory();
-        createAndVerifyProvisionedInterface(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient, never()).startProvisioning(any());
-    }
-
-    @Test
-    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
-        initEthernetNetworkFactory();
-        createUnprovisionedInterface(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient).startProvisioning(any());
-
-        triggerOnProvisioningSuccess();
-        verifyNetworkAgentRegistersAndConnects();
-    }
-
-    @Test
-    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
-        initEthernetNetworkFactory();
-        createInterfaceUndergoingProvisioning(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient, never()).startProvisioning(any());
-
-        triggerOnProvisioningSuccess();
-        verifyNetworkAgentRegistersAndConnects();
-    }
-
-    @Test
     public void testProvisioningLoss() throws Exception {
         initEthernetNetworkFactory();
         when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
@@ -441,31 +409,6 @@
     }
 
     @Test
-    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
-        initEthernetNetworkFactory();
-        createUnprovisionedInterface(TEST_IFACE);
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
-
-        mNetFactory.needNetworkFor(createDefaultRequest());
-
-        verify(mDeps, never()).makeIpClient(any(), any(), any());
-
-        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
-        // not start an IpClient when the link is down, but fixing this may make matters worse by
-        // tiggering b/197548738.
-        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
-                .build();
-        mNetFactory.needNetworkFor(specificNetRequest);
-        mNetFactory.releaseNetworkFor(specificNetRequest);
-
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
-        // TODO: change to once when b/191854824 is fixed.
-        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
-    }
-
-    @Test
     public void testLinkPropertiesChanged() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
@@ -669,7 +612,6 @@
         assertEquals(listener.expectOnResult(), TEST_IFACE);
     }
 
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     @Test
     public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
         initEthernetNetworkFactory();
@@ -678,7 +620,6 @@
                 () -> mNetFactory.removeInterface(TEST_IFACE));
     }
 
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     @Test
     public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
         initEthernetNetworkFactory();
@@ -687,7 +628,14 @@
                 () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
     }
 
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
+    }
+
     @Test
     public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
         initEthernetNetworkFactory();
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index dd1f1ed..a1d93a0 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -16,16 +16,17 @@
 
 package com.android.server.ethernet;
 
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
-
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -35,29 +36,37 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkSpecifier;
 import android.net.EthernetNetworkUpdateRequest;
+import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.NetworkCapabilities;
+import android.net.StringNetworkSpecifier;
+import android.os.Build;
 import android.os.Handler;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class EthernetServiceImplTest {
     private static final String TEST_IFACE = "test123";
+    private static final NetworkCapabilities DEFAULT_CAPS = new NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_ETHERNET)
+            .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+            .build();
     private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
             new EthernetNetworkUpdateRequest.Builder()
                     .setIpConfiguration(new IpConfiguration())
-                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .setNetworkCapabilities(DEFAULT_CAPS)
                     .build();
     private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
             new EthernetNetworkUpdateRequest.Builder()
@@ -65,18 +74,21 @@
                     .build();
     private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
             new EthernetNetworkUpdateRequest.Builder()
-                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .setNetworkCapabilities(DEFAULT_CAPS)
                     .build();
     private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
     private EthernetServiceImpl mEthernetServiceImpl;
-    @Mock private Context mContext;
-    @Mock private Handler mHandler;
-    @Mock private EthernetTracker mEthernetTracker;
-    @Mock private PackageManager mPackageManager;
+    private Context mContext;
+    private Handler mHandler;
+    private EthernetTracker mEthernetTracker;
+    private PackageManager mPackageManager;
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
+        mContext = mock(Context.class);
+        mHandler = mock(Handler.class);
+        mEthernetTracker = mock(EthernetTracker.class);
+        mPackageManager = mock(PackageManager.class);
         doReturn(mPackageManager).when(mContext).getPackageManager();
         mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
         mEthernetServiceImpl.mStarted.set(true);
@@ -111,18 +123,18 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsWhenEthNotStarted() {
+    public void testEnableInterfaceRejectsWhenEthNotStarted() {
         mEthernetServiceImpl.mStarted.set(false);
         assertThrows(IllegalStateException.class, () -> {
-            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+            mEthernetServiceImpl.enableInterface("" /* iface */, null /* listener */);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+    public void testDisableInterfaceRejectsWhenEthNotStarted() {
         mEthernetServiceImpl.mStarted.set(false);
         assertThrows(IllegalStateException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+            mEthernetServiceImpl.disableInterface("" /* iface */, null /* listener */);
         });
     }
 
@@ -134,16 +146,16 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsNullIface() {
+    public void testEnableInterfaceRejectsNullIface() {
         assertThrows(NullPointerException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(null /* iface */, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsNullIface() {
+    public void testDisableInterfaceRejectsNullIface() {
         assertThrows(NullPointerException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(null /* iface */, NULL_LISTENER);
         });
     }
 
@@ -156,6 +168,41 @@
     }
 
     @Test
+    public void testUpdateConfigurationRejectsWithInvalidSpecifierType() {
+        final StringNetworkSpecifier invalidSpecifierType = new StringNetworkSpecifier("123");
+        final EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest.Builder()
+                        .setNetworkCapabilities(
+                                new NetworkCapabilities.Builder()
+                                        .addTransportType(TRANSPORT_ETHERNET)
+                                        .setNetworkSpecifier(invalidSpecifierType)
+                                        .build()
+                        ).build();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(
+                    "" /* iface */, request, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWithInvalidSpecifierName() {
+        final String ifaceToUpdate = "eth0";
+        final String ifaceOnSpecifier = "wlan0";
+        EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest.Builder()
+                        .setNetworkCapabilities(
+                                new NetworkCapabilities.Builder()
+                                        .addTransportType(TRANSPORT_ETHERNET)
+                                        .setNetworkSpecifier(
+                                                new EthernetNetworkSpecifier(ifaceOnSpecifier))
+                                        .build()
+                        ).build();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(ifaceToUpdate, request, null /* listener */);
+        });
+    }
+
+    @Test
     public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
         toggleAutomotiveFeature(false);
         mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
@@ -165,22 +212,6 @@
                 eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
     }
 
-    @Test
-    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
-        toggleAutomotiveFeature(false);
-        assertThrows(UnsupportedOperationException.class, () -> {
-            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
-        });
-    }
-
-    @Test
-    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
-        toggleAutomotiveFeature(false);
-        assertThrows(UnsupportedOperationException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
-        });
-    }
-
     private void denyManageEthPermission() {
         doThrow(new SecurityException("")).when(mContext)
                 .enforceCallingOrSelfPermission(
@@ -202,18 +233,18 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+    public void testEnableInterfaceRejectsWithoutManageEthPermission() {
         denyManageEthPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+    public void testDisableInterfaceRejectsWithoutManageEthPermission() {
         denyManageEthPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
@@ -231,20 +262,20 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+    public void testEnableInterfaceRejectsTestRequestWithoutTestPermission() {
         enableTestInterface();
         denyManageTestNetworksPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+    public void testDisableInterfaceRejectsTestRequestWithoutTestPermission() {
         enableTestInterface();
         denyManageTestNetworksPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
@@ -258,15 +289,33 @@
     }
 
     @Test
-    public void testConnectNetwork() {
-        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    public void testUpdateConfigurationAddsSpecifierWhenNotSet() {
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_ETHERNET).build();
+        final EthernetNetworkUpdateRequest requestSansSpecifier =
+                new EthernetNetworkUpdateRequest.Builder()
+                        .setNetworkCapabilities(nc)
+                        .build();
+        final NetworkCapabilities ncWithSpecifier = new NetworkCapabilities(nc)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE));
+
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, requestSansSpecifier, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                isNull(),
+                eq(ncWithSpecifier), eq(NULL_LISTENER));
     }
 
     @Test
-    public void testDisconnectNetwork() {
-        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    public void testEnableInterface() {
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisableInterface() {
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     @Test
@@ -324,23 +373,23 @@
     }
 
     @Test
-    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+    public void testEnableInterfaceForTestRequestDoesNotRequireNetPermission() {
         enableTestInterface();
         toggleAutomotiveFeature(false);
         denyManageEthPermission();
 
-        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     @Test
-    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+    public void testDisableInterfaceForTestRequestDoesNotRequireAutoOrNetPermission() {
         enableTestInterface();
         toggleAutomotiveFeature(false);
         denyManageEthPermission();
 
-        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     private void denyPermissions(String... permissions) {
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index b1831c4..38094ae 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -28,47 +28,53 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.EthernetManager;
-import android.net.InetAddresses;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
-import android.net.InterfaceConfigurationParcel;
 import android.net.LinkAddress;
 import android.net.NetworkCapabilities;
 import android.net.StaticIpConfiguration;
+import android.os.Build;
 import android.os.HandlerThread;
 import android.os.RemoteException;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
-import com.android.connectivity.resources.R;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
 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;
 
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class EthernetTrackerTest {
     private static final String TEST_IFACE = "test123";
     private static final int TIMEOUT_MS = 1_000;
@@ -351,8 +357,8 @@
     }
 
     @Test
-    public void testConnectNetworkCorrectlyCallsFactory() {
-        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+    public void testEnableInterfaceCorrectlyCallsFactory() {
+        tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
         waitForIdle();
 
         verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
@@ -360,8 +366,8 @@
     }
 
     @Test
-    public void testDisconnectNetworkCorrectlyCallsFactory() {
-        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+    public void testDisableInterfaceCorrectlyCallsFactory() {
+        tracker.disableInterface(TEST_IFACE, NULL_LISTENER);
         waitForIdle();
 
         verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
@@ -410,22 +416,41 @@
                 IpConfiguration configuration) { }
     }
 
+    private InterfaceConfigurationParcel createMockedIfaceParcel(final String ifname,
+            final String hwAddr) {
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = ifname;
+        ifaceParcel.hwAddr = hwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+        return ifaceParcel;
+    }
+
     @Test
     public void testListenEthernetStateChange() throws Exception {
-        final String testIface = "testtap123";
-        final String testHwAddr = "11:22:33:44:55:66";
-        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
-        ifaceParcel.ifName = testIface;
-        ifaceParcel.hwAddr = testHwAddr;
-        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
-
         tracker.setIncludeTestInterfaces(true);
         waitForIdle();
 
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = createMockedIfaceParcel(testIface,
+                testHwAddr);
         when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
         when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
         doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
-        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+        final AtomicBoolean ifaceUp = new AtomicBoolean(true);
+        doAnswer(inv -> ifaceUp.get()).when(mFactory).hasInterface(testIface);
+        doAnswer(inv ->
+                ifaceUp.get() ? EthernetManager.STATE_LINK_UP : EthernetManager.STATE_ABSENT)
+                .when(mFactory).getInterfaceState(testIface);
+        doAnswer(inv -> {
+            ifaceUp.set(true);
+            return null;
+        }).when(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        doAnswer(inv -> {
+            ifaceUp.set(false);
+            return null;
+        }).when(mFactory).removeInterface(testIface);
 
         final EthernetStateListener listener = spy(new EthernetStateListener());
         tracker.addListener(listener, true /* canUseRestrictedNetworks */);
@@ -436,7 +461,6 @@
         verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
         reset(listener);
 
-        doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
         tracker.setEthernetEnabled(false);
         waitForIdle();
         verify(mFactory).removeInterface(eq(testIface));
@@ -445,7 +469,6 @@
                 anyInt(), any());
         reset(listener);
 
-        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
         tracker.setEthernetEnabled(true);
         waitForIdle();
         verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
@@ -453,4 +476,43 @@
         verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
                 anyInt(), any());
     }
+
+    @Test
+    public void testListenEthernetStateChange_unsolicitedEventListener() throws Exception {
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {});
+        doReturn(new String[] {}).when(mFactory).getAvailableInterfaces(anyBoolean());
+
+        tracker.setIncludeTestInterfaces(true);
+        tracker.start();
+
+        final ArgumentCaptor<EthernetTracker.InterfaceObserver> captor =
+                ArgumentCaptor.forClass(EthernetTracker.InterfaceObserver.class);
+        verify(mNetd, timeout(TIMEOUT_MS)).registerUnsolicitedEventListener(captor.capture());
+        final EthernetTracker.InterfaceObserver observer = captor.getValue();
+
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        reset(mFactory);
+        reset(mNetd);
+
+        final String testIface = "testtap1";
+        observer.onInterfaceAdded(testIface);
+        verify(mFactory, never()).addInterface(eq(testIface), anyString(), any(), any());
+        observer.onInterfaceRemoved(testIface);
+        verify(mFactory, never()).removeInterface(eq(testIface));
+
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel testIfaceParce =
+                createMockedIfaceParcel(testIface, testHwAddr);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(testIfaceParce);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        reset(mFactory);
+
+        final String testIface2 = "testtap2";
+        observer.onInterfaceRemoved(testIface2);
+        verify(mFactory, timeout(TIMEOUT_MS)).removeInterface(eq(testIface2));
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
index 987b7b7..c6852d1 100644
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -24,16 +24,18 @@
 import android.content.Context;
 import android.net.INetd;
 import android.net.MacAddress;
+import android.os.Build;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -42,8 +44,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public final class BpfInterfaceMapUpdaterTest {
     private static final int TEST_INDEX = 1;
     private static final int TEST_INDEX2 = 2;
diff --git a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
index e9a5309..4adc999 100644
--- a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
+++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
+import android.content.Context;
 import android.net.InetAddresses;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
@@ -27,9 +28,15 @@
 import android.net.LinkAddress;
 import android.net.ProxyInfo;
 import android.net.StaticIpConfiguration;
+import android.os.Build;
+import android.os.HandlerThread;
 import android.util.ArrayMap;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,17 +44,21 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /**
  * Unit tests for {@link IpConfigStore}
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class IpConfigStoreTest {
+    private static final int TIMEOUT_MS = 2_000;
     private static final int KEY_CONFIG = 17;
     private static final String IFACE_1 = "eth0";
     private static final String IFACE_2 = "eth1";
@@ -56,6 +67,22 @@
     private static final String DNS_IP_ADDR_1 = "1.2.3.4";
     private static final String DNS_IP_ADDR_2 = "5.6.7.8";
 
+    private static final ArrayList<InetAddress> DNS_SERVERS = new ArrayList<>(List.of(
+            InetAddresses.parseNumericAddress(DNS_IP_ADDR_1),
+            InetAddresses.parseNumericAddress(DNS_IP_ADDR_2)));
+    private static final StaticIpConfiguration STATIC_IP_CONFIG_1 =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(new LinkAddress(IP_ADDR_1))
+                    .setDnsServers(DNS_SERVERS)
+                    .build();
+    private static final StaticIpConfiguration STATIC_IP_CONFIG_2 =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(new LinkAddress(IP_ADDR_2))
+                    .setDnsServers(DNS_SERVERS)
+                    .build();
+    private static final ProxyInfo PROXY_INFO =
+            ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
+
     @Test
     public void backwardCompatibility2to3() throws IOException {
         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
@@ -79,40 +106,73 @@
 
     @Test
     public void staticIpMultiNetworks() throws Exception {
-        final ArrayList<InetAddress> dnsServers = new ArrayList<>();
-        dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_1));
-        dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_2));
-        final StaticIpConfiguration staticIpConfiguration1 = new StaticIpConfiguration.Builder()
-                .setIpAddress(new LinkAddress(IP_ADDR_1))
-                .setDnsServers(dnsServers).build();
-        final StaticIpConfiguration staticIpConfiguration2 = new StaticIpConfiguration.Builder()
-                .setIpAddress(new LinkAddress(IP_ADDR_2))
-                .setDnsServers(dnsServers).build();
+        final IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+        final IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_2, PROXY_INFO);
 
-        ProxyInfo proxyInfo =
-                ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
-
-        IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
-                ProxySettings.STATIC, staticIpConfiguration1, proxyInfo);
-        IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
-                ProxySettings.STATIC, staticIpConfiguration2, proxyInfo);
-
-        ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
         expectedNetworks.put(IFACE_1, expectedConfig1);
         expectedNetworks.put(IFACE_2, expectedConfig2);
 
-        MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
-        IpConfigStore store = new IpConfigStore(writer);
+        final MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+        final IpConfigStore store = new IpConfigStore(writer);
         store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
 
-        InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
-        ArrayMap<String, IpConfiguration> actualNetworks = IpConfigStore.readIpConfigurations(in);
+        final InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
+        final ArrayMap<String, IpConfiguration> actualNetworks =
+                IpConfigStore.readIpConfigurations(in);
         assertNotNull(actualNetworks);
         assertEquals(2, actualNetworks.size());
         assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
         assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2));
     }
 
+    @Test
+    public void readIpConfigurationFromFilePath() throws Exception {
+        final HandlerThread testHandlerThread = new HandlerThread("IpConfigStoreTest");
+        final DelayedDiskWrite.Dependencies dependencies =
+                new DelayedDiskWrite.Dependencies() {
+                    @Override
+                    public HandlerThread makeHandlerThread() {
+                        return testHandlerThread;
+                    }
+                    @Override
+                    public void quitHandlerThread(HandlerThread handlerThread) {
+                        testHandlerThread.quitSafely();
+                    }
+        };
+
+        final IpConfiguration ipConfig = newIpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+        final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        expectedNetworks.put(IFACE_1, ipConfig);
+
+        // Write IP config to specific file path and read it later.
+        final Context context = InstrumentationRegistry.getContext();
+        final File configFile = new File(context.getFilesDir().getPath(),
+                "IpConfigStoreTest-ipconfig.txt");
+        final DelayedDiskWrite writer = new DelayedDiskWrite(dependencies);
+        final IpConfigStore store = new IpConfigStore(writer);
+        store.writeIpConfigurations(configFile.getPath(), expectedNetworks);
+        HandlerUtils.waitForIdle(testHandlerThread, TIMEOUT_MS);
+
+        // Read IP config from the file path.
+        final ArrayMap<String, IpConfiguration> actualNetworks =
+                IpConfigStore.readIpConfigurations(configFile.getPath());
+        assertNotNull(actualNetworks);
+        assertEquals(1, actualNetworks.size());
+        assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+
+        // Return an empty array when reading IP configuration from an non-exist config file.
+        final ArrayMap<String, IpConfiguration> emptyNetworks =
+                IpConfigStore.readIpConfigurations("/dev/null");
+        assertNotNull(emptyNetworks);
+        assertEquals(0, emptyNetworks.size());
+
+        configFile.delete();
+    }
+
     private IpConfiguration newIpConfiguration(IpAssignment ipAssignment,
             ProxySettings proxySettings, StaticIpConfiguration staticIpConfig, ProxyInfo info) {
         final IpConfiguration config = new IpConfiguration();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 79744b1..f6fb45c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -29,6 +29,7 @@
 import static android.net.NetworkStats.UID_ALL;
 
 import static com.android.server.net.NetworkStatsFactory.kernelToTag;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
@@ -38,12 +39,12 @@
 import android.net.NetworkStats;
 import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
-import android.os.Build;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
 import com.android.frameworks.tests.net.R;
+import com.android.server.BpfNetMaps;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -67,13 +68,14 @@
 /** Tests for {@link NetworkStatsFactory}. */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
     private static final String CLAT_PREFIX = "v4-";
 
     private File mTestProc;
     private NetworkStatsFactory mFactory;
     @Mock private Context mContext;
+    @Mock private BpfNetMaps mBpfNetMaps;
 
     @Before
     public void setUp() throws Exception {
@@ -84,7 +86,7 @@
         // 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(mContext, mTestProc, false);
+        mFactory = new NetworkStatsFactory(mContext, mTestProc, false, mBpfNetMaps);
         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 5f9d1ff..5747e10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -32,6 +32,7 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -64,6 +65,7 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -86,11 +88,19 @@
     private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
     private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
 
+    private static final int PID_SYSTEM = 1234;
+    private static final int PID_RED = 1235;
+    private static final int PID_BLUE = 1236;
+
     private static final int UID_RED = UserHandle.PER_USER_RANGE + 1;
     private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2;
     private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3;
     private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4;
 
+    private static final String PACKAGE_SYSTEM = "android";
+    private static final String PACKAGE_RED = "RED";
+    private static final String PACKAGE_BLUE = "BLUE";
+
     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;
@@ -131,14 +141,15 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
         final DataUsageRequest requestByApp = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_RED , UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestByApp.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
         assertEquals(thresholdTooLowBytes, requestByApp.thresholdInBytes);
 
         // Verify the threshold requested by system uid won't be overridden.
         final DataUsageRequest requestBySystem = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM,
+                NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestBySystem.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
         assertEquals(1, requestBySystem.thresholdInBytes);
@@ -151,7 +162,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         assertEquals(highThresholdBytes, request.thresholdInBytes);
@@ -163,19 +174,64 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
         DataUsageRequest request1 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
         DataUsageRequest request2 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
         assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes);
     }
 
     @Test
+    public void testRegister_limit() throws Exception {
+        final DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
+
+        // Register maximum requests for red.
+        final ArrayList<DataUsageRequest> redRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
+            redRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Verify request exceeds the limit throws.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                    PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Verify another uid is not affected.
+        final ArrayList<DataUsageRequest> blueRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEVICE);
+            blueRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Again, verify request exceeds the limit throws for the 2nd uid.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                        PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Unregister all registered requests. Note that exceptions cannot be tested since
+        // unregister is handled in the handler thread.
+        for (final DataUsageRequest request : redRequests) {
+            mStatsObservers.unregister(request, UID_RED);
+        }
+        for (final DataUsageRequest request : blueRequests) {
+            mStatsObservers.unregister(request, UID_BLUE);
+        }
+    }
+
+    @Test
     public void testUnregister_unknownRequest_noop() throws Exception {
         DataUsageRequest unknownRequest = new DataUsageRequest(
                 123456 /* id */, sTemplateWifi, THRESHOLD_BYTES);
@@ -189,7 +245,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -209,7 +265,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEVICE);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -218,8 +274,12 @@
 
         mStatsObservers.unregister(request, UID_BLUE);
         waitForObserverToIdle();
-
         Mockito.verifyZeroInteractions(mUsageCallbackBinder);
+
+        // Verify that system uid can unregister for other uids.
+        mStatsObservers.unregister(request, Process.SYSTEM_UID);
+        waitForObserverToIdle();
+        mUsageCallback.expectOnCallbackReleased(request);
     }
 
     private NetworkIdentitySet makeTestIdentSet() {
@@ -237,7 +297,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -261,7 +321,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -291,7 +351,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -322,7 +382,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEFAULT);
+                PID_RED, UID_RED, PACKAGE_SYSTEM , NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -355,7 +415,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -387,7 +447,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.USER);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -420,7 +480,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.USER);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ceeb997..f9cbb10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.app.usage.NetworkStatsManager.PREFIX_DEV;
 import static android.content.Intent.ACTION_UID_REMOVED;
 import static android.content.Intent.EXTRA_UID;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -56,6 +57,9 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -63,6 +67,9 @@
 
 import static com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
@@ -77,6 +84,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -87,15 +95,19 @@
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsSession;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
 import android.net.NetworkStateSnapshot;
 import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TelephonyNetworkSpecifier;
@@ -104,6 +116,7 @@
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
+import android.os.DropBoxManager;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -112,11 +125,14 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
 
 import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.connectivity.resources.R;
+import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -143,9 +159,17 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.Clock;
+import java.time.ZoneId;
 import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -187,6 +211,7 @@
     private long mElapsedRealtime;
 
     private File mStatsDir;
+    private File mLegacyStatsDir;
     private MockContext mServiceContext;
     private @Mock TelephonyManager mTelephonyManager;
     private static @Mock WifiInfo sWifiInfo;
@@ -220,6 +245,15 @@
     private ContentObserver mContentObserver;
     private Handler mHandler;
     private TetheringManager.TetheringEventCallback mTetheringEventCallback;
+    private Map<String, NetworkStatsCollection> mPlatformNetworkStatsCollection =
+            new ArrayMap<String, NetworkStatsCollection>();
+    private boolean mStoreFilesInApexData = false;
+    private int mImportLegacyTargetAttempts = 0;
+    private @Mock PersistentInt mImportLegacyAttemptsCounter;
+    private @Mock PersistentInt mImportLegacySuccessesCounter;
+    private @Mock PersistentInt mImportLegacyFallbacksCounter;
+    private @Mock Resources mResources;
+    private Boolean mIsDebuggable;
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -280,12 +314,20 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        // Setup mock resources.
+        final Context mockResContext = mock(Context.class);
+        doReturn(mResources).when(mockResContext).getResources();
+        ConnectivityResources.setResourcesContextForTest(mockResContext);
+
         final Context context = InstrumentationRegistry.getContext();
         mServiceContext = new MockContext(context);
         when(mLocationPermissionChecker.checkCallersLocationPermission(
                 any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
         when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
         mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+        mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
+                getClass().getSimpleName() + "-legacy");
 
         PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
                 Context.POWER_SERVICE);
@@ -295,8 +337,7 @@
         mHandlerThread = new HandlerThread("HandlerThread");
         final NetworkStatsService.Dependencies deps = makeDependencies();
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
-                getBaseDir(mStatsDir), deps);
+                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
 
         mElapsedRealtime = 0L;
 
@@ -339,6 +380,47 @@
     private NetworkStatsService.Dependencies makeDependencies() {
         return new NetworkStatsService.Dependencies() {
             @Override
+            public File getLegacyStatsDir() {
+                return mLegacyStatsDir;
+            }
+
+            @Override
+            public File getOrCreateStatsDir() {
+                return mStatsDir;
+            }
+
+            @Override
+            public boolean getStoreFilesInApexData() {
+                return mStoreFilesInApexData;
+            }
+
+            @Override
+            public int getImportLegacyTargetAttempts() {
+                return mImportLegacyTargetAttempts;
+            }
+
+            @Override
+            public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
+                    @androidx.annotation.NonNull String name) throws IOException {
+                switch (name) {
+                    case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+                        return mImportLegacyAttemptsCounter;
+                    case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+                        return mImportLegacySuccessesCounter;
+                    case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+                        return mImportLegacyFallbacksCounter;
+                    default:
+                        throw new IllegalArgumentException("Unknown counter name: " + name);
+                }
+            }
+
+            @Override
+            public NetworkStatsCollection readPlatformCollection(
+                    @NonNull String prefix, long bucketDuration) {
+                return mPlatformNetworkStatsCollection.get(prefix);
+            }
+
+            @Override
             public HandlerThread makeHandlerThread() {
                 return mHandlerThread;
             }
@@ -393,6 +475,11 @@
             public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
                 return mAppUidStatsMap;
             }
+
+            @Override
+            public boolean isDebuggable() {
+                return mIsDebuggable == Boolean.TRUE;
+            }
         };
     }
 
@@ -1047,6 +1134,40 @@
     }
 
     @Test
+    public void testGetLatestSummary() throws Exception {
+        // Pretend that network comes online.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Increase arbitrary time which does not align to the bucket edge, create some traffic.
+        incrementCurrentTime(1751000L);
+        NetworkStats.Entry entry = new NetworkStats.Entry(
+                TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 50L, 5L, 51L, 1L, 3L);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1).insertEntry(entry));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // Verify the mocked stats is returned by querying with the range of the latest bucket.
+        final ZonedDateTime end =
+                ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
+        final ZonedDateTime start = end.truncatedTo(ChronoUnit.HOURS);
+        NetworkStats stats = mSession.getSummaryForNetwork(buildTemplateWifi(TEST_WIFI_NETWORK_KEY),
+                start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 50L, 5L, 51L, 1L, 3L);
+
+        // For getHistoryIntervalForNetwork, only includes buckets that atomically occur in
+        // the inclusive time range, instead of including the latest bucket. This behavior is
+        // already documented publicly, refer to {@link NetworkStatsManager#queryDetails}.
+    }
+
+    @Test
     public void testUidStatsForTransport() throws Exception {
         // pretend that network comes online
         expectDefaultSettings();
@@ -1077,9 +1198,12 @@
 
         assertEquals(3, stats.size());
         entry1.operations = 1;
+        entry1.iface = null;
         assertEquals(entry1, stats.getValues(0, null));
         entry2.operations = 1;
+        entry2.iface = null;
         assertEquals(entry2, stats.getValues(1, null));
+        entry3.iface = null;
         assertEquals(entry3, stats.getValues(2, null));
     }
 
@@ -1704,10 +1828,216 @@
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
     }
 
-    private static File getBaseDir(File statsDir) {
-        File baseDir = new File(statsDir, "netstats");
-        baseDir.mkdirs();
-        return baseDir;
+    /**
+     * Verify the service will perform data migration process can be controlled by the device flag.
+     */
+    @Test
+    public void testDataMigration() throws Exception {
+        assertStatsFilesExist(false);
+        expectDefaultSettings();
+
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        // expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+
+        mService.noteUidForeground(UID_RED, false);
+        verify(mUidCounterSetMap, never()).deleteEntry(any());
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+        mService.noteUidForeground(UID_RED, true);
+        verify(mUidCounterSetMap).updateEntry(
+                eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
+
+        forcePollAndWaitForIdle();
+        // Simulate shutdown to force persisting data
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // Move the files to the legacy directory to simulate an import from old data
+        for (File f : mStatsDir.listFiles()) {
+            Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+        }
+        assertStatsFilesExist(false);
+
+        // Fetch the stats from the legacy files and set platform stats collection to be identical
+        mPlatformNetworkStatsCollection.put(PREFIX_DEV,
+                getLegacyCollection(PREFIX_DEV, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_XT,
+                getLegacyCollection(PREFIX_XT, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_UID,
+                getLegacyCollection(PREFIX_UID, false /* includeTags */));
+        mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+                getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+        // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        assertStatsFilesExist(false);
+
+        // Set the flag and reboot, verify the imported data is not there until next boot.
+        mStoreFilesInApexData = true;
+        mImportLegacyTargetAttempts = 3;
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(false);
+
+        // Boot through systemReady() again.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+
+        // After systemReady(), the service should have historical stats loaded again.
+        // Thus, verify
+        //  1. The stats are absorbed by the recorder.
+        //  2. The imported data are persisted.
+        //  3. The attempts count is set to target attempts count to indicate a successful
+        //     migration.
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertStatsFilesExist(true);
+        verify(mImportLegacyAttemptsCounter).set(3);
+        verify(mImportLegacySuccessesCounter).set(1);
+
+        // TODO: Verify upgrading with Exception won't damege original data and
+        //  will decrease the retry counter by 1.
+    }
+
+    @Test
+    public void testDataMigration_differentFromFallback() throws Exception {
+        assertStatsFilesExist(false);
+        expectDefaultSettings();
+
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+        forcePollAndWaitForIdle();
+        // Simulate shutdown to force persisting data
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // Move the files to the legacy directory to simulate an import from old data
+        for (File f : mStatsDir.listFiles()) {
+            Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+        }
+        assertStatsFilesExist(false);
+
+        // Prepare some unexpected data.
+        final NetworkIdentity testWifiIdent = new NetworkIdentity.Builder().setType(TYPE_WIFI)
+                .setWifiNetworkKey(TEST_WIFI_NETWORK_KEY).build();
+        final NetworkStatsCollection.Key unexpectedUidAllkey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_ALL, SET_DEFAULT, 0);
+        final NetworkStatsCollection.Key unexpectedUidBluekey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_BLUE, SET_DEFAULT, 0);
+        final NetworkStatsHistory unexpectedHistory = new NetworkStatsHistory
+                .Builder(965L /* bucketDuration */, 1)
+                .addEntry(new NetworkStatsHistory.Entry(TEST_START, 3L, 55L, 4L, 31L, 10L, 5L))
+                .build();
+
+        // Simulate the platform stats collection somehow is different from what is read from
+        // the fallback method. The service should read them as is. This usually happens when an
+        // OEM has changed the implementation of NetworkStatsDataMigrationUtils inside the platform.
+        final NetworkStatsCollection summaryCollection =
+                getLegacyCollection(PREFIX_XT, false /* includeTags */);
+        summaryCollection.recordHistory(unexpectedUidAllkey, unexpectedHistory);
+        final NetworkStatsCollection uidCollection =
+                getLegacyCollection(PREFIX_UID, false /* includeTags */);
+        uidCollection.recordHistory(unexpectedUidBluekey, unexpectedHistory);
+        mPlatformNetworkStatsCollection.put(PREFIX_DEV, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_XT, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID, uidCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+                getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+        // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        assertStatsFilesExist(false);
+
+        // Set the flag and reboot, verify the imported data is not there until next boot.
+        mStoreFilesInApexData = true;
+        mImportLegacyTargetAttempts = 3;
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(false);
+
+        // Boot through systemReady() again.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+
+        // Verify the result read from public API matches the result returned from the importer.
+        assertNetworkTotal(sTemplateWifi, 1024L + 55L, 8L + 4L, 2048L + 31L, 16L + 10L, 0 + 5);
+        assertUidTotal(sTemplateWifi, UID_BLUE,
+                128L + 55L, 1L + 4L, 128L + 31L, 1L + 10L, 0 + 5);
+        assertStatsFilesExist(true);
+        verify(mImportLegacyAttemptsCounter).set(3);
+        verify(mImportLegacySuccessesCounter).set(1);
+    }
+
+    @Test
+    public void testShouldRunComparison() {
+        for (Boolean isDebuggable : Set.of(Boolean.TRUE, Boolean.FALSE)) {
+            mIsDebuggable = isDebuggable;
+            // Verify return false regardless of the device is debuggable.
+            doReturn(0).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertShouldRunComparison(false, isDebuggable);
+            // Verify return true regardless of the device is debuggable.
+            doReturn(1).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertShouldRunComparison(true, isDebuggable);
+            // Verify return true iff the device is debuggable.
+            for (int testValue : Set.of(-1, 2)) {
+                doReturn(testValue).when(mResources)
+                        .getInteger(R.integer.config_netstats_validate_import);
+                assertShouldRunComparison(isDebuggable, isDebuggable);
+            }
+        }
+    }
+
+    private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
+        assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
+                expected, mService.shouldRunComparison());
+    }
+
+    private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
+            boolean includeTags, boolean wipeOnError) {
+        final NetworkStats.NonMonotonicObserver observer =
+                mock(NetworkStats.NonMonotonicObserver.class);
+        final DropBoxManager dropBox = mock(DropBoxManager.class);
+        return new NetworkStatsRecorder(new FileRotator(
+                directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+                observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+    }
+
+    private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
+        final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, prefix,
+                mSettings.getDevConfig(), includeTags, false);
+        return recorder.getOrLoadCompleteLocked();
     }
 
     private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
@@ -1816,11 +2146,10 @@
     }
 
     private void assertStatsFilesExist(boolean exist) {
-        final File basePath = new File(mStatsDir, "netstats");
         if (exist) {
-            assertTrue(basePath.list().length > 0);
+            assertTrue(mStatsDir.list().length > 0);
         } else {
-            assertTrue(basePath.list().length == 0);
+            assertTrue(mStatsDir.list().length == 0);
         }
     }
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index 0d34609..622f2be 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -19,6 +19,8 @@
 import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE;
 import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
@@ -37,7 +39,6 @@
 import android.annotation.Nullable;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.os.Build;
 import android.os.Looper;
 import android.os.Parcel;
 import android.telephony.SubscriptionManager;
@@ -63,7 +64,7 @@
 import java.util.concurrent.Executors;
 
 @RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public final class NetworkStatsSubscriptionsMonitorTest {
     private static final int TEST_SUBID1 = 3;
     private static final int TEST_SUBID2 = 5;
diff --git a/tests/unit/java/com/android/server/net/PersistentIntTest.kt b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
new file mode 100644
index 0000000..9268352
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.util.SystemConfigFileCommitEventLogger
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import com.android.testutils.assertThrows
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermission
+import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
+import java.nio.file.attribute.PosixFilePermission.OWNER_READ
+import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
+import java.util.Random
+import kotlin.test.assertEquals
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(SC_V2)
+class PersistentIntTest {
+    val tempFilesCreated = mutableSetOf<Path>()
+    lateinit var tempDir: Path
+
+    @Before
+    fun setUp() {
+        tempDir = Files.createTempDirectory("tmp.PersistentIntTest.")
+    }
+
+    @After
+    fun tearDown() {
+        var permissions = setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
+        Files.setPosixFilePermissions(tempDir, permissions)
+
+        for (file in tempFilesCreated) {
+            Files.deleteIfExists(file)
+        }
+        Files.delete(tempDir)
+    }
+
+    @Test
+    fun testNormalReadWrite() {
+        // New, initialized to 0.
+        val pi = createPersistentInt()
+        assertEquals(0, pi.get())
+        pi.set(12345)
+        assertEquals(12345, pi.get())
+
+        // Existing.
+        val pi2 = createPersistentInt(pathOf(pi))
+        assertEquals(12345, pi2.get())
+    }
+
+    @Test
+    fun testReadOrWriteFailsInCreate() {
+        setWritable(tempDir, false)
+        assertThrows(IOException::class.java) {
+            createPersistentInt()
+        }
+    }
+
+    @Test
+    fun testReadOrWriteFailsAfterCreate() {
+        val pi = createPersistentInt()
+        pi.set(42)
+        assertEquals(42, pi.get())
+
+        val path = pathOf(pi)
+        setReadable(path, false)
+        assertThrows(IOException::class.java) { pi.get() }
+        pi.set(77)
+
+        setReadable(path, true)
+        setWritable(path, false)
+        setWritable(tempDir, false) // Writing creates a new file+renames, make this fail.
+        assertThrows(IOException::class.java) { pi.set(99) }
+        assertEquals(77, pi.get())
+    }
+
+    fun addOrRemovePermission(p: Path, permission: PosixFilePermission, add: Boolean) {
+        val permissions = Files.getPosixFilePermissions(p)
+        if (add) {
+            permissions.add(permission)
+        } else {
+            permissions.remove(permission)
+        }
+        Files.setPosixFilePermissions(p, permissions)
+    }
+
+    fun setReadable(p: Path, readable: Boolean) {
+        addOrRemovePermission(p, OWNER_READ, readable)
+    }
+
+    fun setWritable(p: Path, writable: Boolean) {
+        addOrRemovePermission(p, OWNER_WRITE, writable)
+    }
+
+    fun pathOf(pi: PersistentInt): Path {
+        return File(pi.path).toPath()
+    }
+
+    fun createPersistentInt(path: Path = randomTempPath()): PersistentInt {
+        tempFilesCreated.add(path)
+        return PersistentInt(path.toString(),
+                SystemConfigFileCommitEventLogger("PersistentIntTest"))
+    }
+
+    fun randomTempPath(): Path {
+        return tempDir.resolve(Integer.toHexString(Random().nextInt())).also {
+            tempFilesCreated.add(it)
+        }
+    }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
new file mode 100644
index 0000000..1fa93bb
--- /dev/null
+++ b/tools/Android.bp
@@ -0,0 +1,91 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build tool used to generate jarjar rules for all classes in a jar, except those that are
+// API, UnsupportedAppUsage or otherwise excluded.
+python_binary_host {
+    name: "jarjar-rules-generator",
+    srcs: [
+        "gen_jarjar.py",
+    ],
+    main: "gen_jarjar.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+genrule_defaults {
+    name: "jarjar-rules-combine-defaults",
+    // Concat files with a line break in the middle
+    cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+    defaults_visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_library {
+    name: "jarjar-rules-generator-testjavalib",
+    srcs: ["testdata/java/**/*.java"],
+    visibility: ["//visibility:private"],
+}
+
+// TODO(b/233723405) - Remove this workaround.
+// Temporary work around of b/233723405. Using the module_lib stub directly
+// in the test causes it to sometimes get the dex jar and sometimes get the
+// classes jar due to b/233111644. Statically including it here instead
+// ensures that it will always get the classes jar.
+java_library {
+    name: "framework-connectivity.stubs.module_lib-for-test",
+    visibility: ["//visibility:private"],
+    static_libs: [
+        "framework-connectivity.stubs.module_lib",
+    ],
+    // Not strictly necessary but specified as this MUST not have generate
+    // a dex jar as that will break the tests.
+    compile_dex: false,
+}
+
+python_test_host {
+    name: "jarjar-rules-generator-test",
+    srcs: [
+        "gen_jarjar.py",
+        "gen_jarjar_test.py",
+    ],
+    data: [
+        "testdata/test-jarjar-excludes.txt",
+        "testdata/test-unsupportedappusage.txt",
+        ":framework-connectivity.stubs.module_lib-for-test",
+        ":jarjar-rules-generator-testjavalib",
+    ],
+    main: "gen_jarjar_test.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+}
diff --git a/tools/gen_jarjar.py b/tools/gen_jarjar.py
new file mode 100755
index 0000000..4c2cf54
--- /dev/null
+++ b/tools/gen_jarjar.py
@@ -0,0 +1,133 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" This script generates jarjar rule files to add a jarjar prefix to all classes, except those
+that are API, unsupported API or otherwise excluded."""
+
+import argparse
+import io
+import re
+import subprocess
+from xml import sax
+from xml.sax.handler import ContentHandler
+from zipfile import ZipFile
+
+
+def parse_arguments(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        '--jars', nargs='+',
+        help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.')
+    parser.add_argument(
+        '--prefix', required=True,
+        help='Package prefix to use for jarjared classes, '
+             'for example "com.android.connectivity" (does not end with a dot).')
+    parser.add_argument(
+        '--output', required=True, help='Path to output jarjar rules file.')
+    parser.add_argument(
+        '--apistubs', nargs='*', default=[],
+        help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--unsupportedapi', nargs='*', default=[],
+        help='Path to UnsupportedAppUsage hidden API .txt lists. '
+             'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--excludes', nargs='*', default=[],
+        help='Path to files listing classes that should not be jarjared. Can be followed by '
+             'multiple space-separated paths. '
+             'Each file should contain one full-match regex per line. Empty lines or lines '
+             'starting with "#" are ignored.')
+    return parser.parse_args(argv)
+
+
+def _list_toplevel_jar_classes(jar):
+    """List all classes in a .class .jar file that are not inner classes."""
+    return {_get_toplevel_class(c) for c in _list_jar_classes(jar)}
+
+def _list_jar_classes(jar):
+    with ZipFile(jar, 'r') as zip:
+        files = zip.namelist()
+        assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
+                                           'expected an intermediate zip of .class files'
+        class_len = len('.class')
+        return [f.replace('/', '.')[:-class_len] for f in files
+                if f.endswith('.class') and not f.endswith('/package-info.class')]
+
+
+def _list_hiddenapi_classes(txt_file):
+    out = set()
+    with open(txt_file, 'r') as f:
+        for line in f:
+            if not line.strip():
+                continue
+            assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
+            clazz = line.replace('/', '.').split(';')[0][1:]
+            out.add(_get_toplevel_class(clazz))
+    return out
+
+
+def _get_toplevel_class(clazz):
+    """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
+    if '$' not in clazz:
+        return clazz
+    return clazz.split('$')[0]
+
+
+def _get_excludes(path):
+    out = []
+    with open(path, 'r') as f:
+        for line in f:
+            stripped = line.strip()
+            if not stripped or stripped.startswith('#'):
+                continue
+            out.append(re.compile(stripped))
+    return out
+
+
+def make_jarjar_rules(args):
+    excluded_classes = set()
+    for apistubs_file in args.apistubs:
+        excluded_classes.update(_list_toplevel_jar_classes(apistubs_file))
+
+    for unsupportedapi_file in args.unsupportedapi:
+        excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
+
+    exclude_regexes = []
+    for exclude_file in args.excludes:
+        exclude_regexes.extend(_get_excludes(exclude_file))
+
+    with open(args.output, 'w') as outfile:
+        for jar in args.jars:
+            jar_classes = _list_jar_classes(jar)
+            jar_classes.sort()
+            for clazz in jar_classes:
+                if (_get_toplevel_class(clazz) not in excluded_classes and
+                        not any(r.fullmatch(clazz) for r in exclude_regexes)):
+                    outfile.write(f'rule {clazz} {args.prefix}.@0\n')
+                    # Also include jarjar rules for unit tests of the class, so the package matches
+                    outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
+                    outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
+
+
+def _main():
+    # Pass in None to use argv
+    args = parse_arguments(None)
+    make_jarjar_rules(args)
+
+
+if __name__ == '__main__':
+    _main()
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
new file mode 100644
index 0000000..8d8e82b
--- /dev/null
+++ b/tools/gen_jarjar_test.py
@@ -0,0 +1,57 @@
+#  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.
+#
+#  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.
+
+import gen_jarjar
+import unittest
+
+
+class TestGenJarjar(unittest.TestCase):
+    def test_gen_rules(self):
+        args = gen_jarjar.parse_arguments([
+            "--jars", "jarjar-rules-generator-testjavalib.jar",
+            "--prefix", "jarjar.prefix",
+            "--output", "test-output-rules.txt",
+            "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+            "--unsupportedapi", "testdata/test-unsupportedappusage.txt",
+            "--excludes", "testdata/test-jarjar-excludes.txt",
+        ])
+        gen_jarjar.make_jarjar_rules(args)
+
+        with open(args.output) as out:
+            lines = out.readlines()
+
+        self.assertListEqual([
+            'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest$* jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
+
+if __name__ == '__main__':
+    # Need verbosity=2 for the test results parser to find results
+    unittest.main(verbosity=2)
diff --git a/tools/testdata/java/android/net/LinkProperties.java b/tools/testdata/java/android/net/LinkProperties.java
new file mode 100644
index 0000000..bdca377
--- /dev/null
+++ b/tools/testdata/java/android/net/LinkProperties.java
@@ -0,0 +1,23 @@
+/*
+ * 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 android.net;
+
+/**
+ * Test class with a name matching a public API.
+ */
+public class LinkProperties {
+}
diff --git a/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
new file mode 100644
index 0000000..7e3bee1
--- /dev/null
+++ b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
@@ -0,0 +1,23 @@
+/*
+ * 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 test.jarjarexcluded;
+
+/**
+ * Test class that is excluded from jarjar.
+ */
+public class JarjarExcludedClass {
+}
diff --git a/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
new file mode 100644
index 0000000..9d32296
--- /dev/null
+++ b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
@@ -0,0 +1,21 @@
+/*
+ * 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 test.unsupportedappusage;
+
+public class TestUnsupportedAppUsageClass {
+    public void testMethod() {}
+}
diff --git a/tools/testdata/java/test/utils/TestUtilClass.java b/tools/testdata/java/test/utils/TestUtilClass.java
new file mode 100644
index 0000000..2162e45
--- /dev/null
+++ b/tools/testdata/java/test/utils/TestUtilClass.java
@@ -0,0 +1,24 @@
+/*
+ * 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 test.utils;
+
+/**
+ * Sample class to test jarjar rules.
+ */
+public class TestUtilClass {
+    public static class TestInnerClass {}
+}
diff --git a/tools/testdata/test-jarjar-excludes.txt b/tools/testdata/test-jarjar-excludes.txt
new file mode 100644
index 0000000..35d97a2
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes.txt
@@ -0,0 +1,3 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
diff --git a/tools/testdata/test-unsupportedappusage.txt b/tools/testdata/test-unsupportedappusage.txt
new file mode 100644
index 0000000..331eff9
--- /dev/null
+++ b/tools/testdata/test-unsupportedappusage.txt
@@ -0,0 +1 @@
+Ltest/unsupportedappusage/TestUnsupportedAppUsageClass;->testMethod()V
\ No newline at end of file