Merge "Add test to verify null ProxyInfo in Ikev2VpnProfile"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index f3d6aee..3ab1ec2 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -26,6 +26,21 @@
 }
 
 java_defaults {
+    name: "TetheringExternalLibs",
+    // Libraries not including Tethering's own framework-tethering (different flavors of that one
+    // are needed depending on the build rule)
+    libs: [
+        "framework-connectivity.stubs.module_lib",
+        "framework-connectivity-t.stubs.module_lib",
+        "framework-statsd.stubs.module_lib",
+        "framework-wifi",
+        "framework-bluetooth",
+        "unsupportedappusage",
+    ],
+    defaults_visibility: ["//visibility:private"],
+}
+
+java_defaults {
     name: "TetheringAndroidLibraryDefaults",
     srcs: [
         "apishim/**/*.java",
@@ -51,14 +66,9 @@
         "netd-client",
         "tetheringstatsprotos",
     ],
+    defaults: ["TetheringExternalLibs"],
     libs: [
-        "framework-connectivity",
-        "framework-connectivity-t.stubs.module_lib",
-        "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
-        "framework-wifi",
-        "framework-bluetooth",
-        "unsupportedappusage",
     ],
     plugins: ["java_api_finder"],
     manifest: "AndroidManifestBase.xml",
@@ -148,9 +158,17 @@
     resource_dirs: [
         "res",
     ],
+    // Libs are not actually needed to build here since build rules using these defaults are just
+    // packaging the TetheringApiXLibs in APKs, but they are necessary so that R8 has the right
+    // references to optimize the code. Without these, there will be missing class warnings and code
+    // may be wrongly optimized.
+    // R8 runs after jarjar, so the framework-X libraries need to be the post-jarjar artifacts
+    // (framework-tethering.impl), if they are not just stubs, so that the name of jarjared
+    // classes match.
+    // TODO(b/229727645): ensure R8 fails the build fully if libraries are missing
+    defaults: ["TetheringExternalLibs"],
     libs: [
-        "framework-tethering",
-        "framework-wifi",
+        "framework-tethering.impl",
     ],
     jarjar_rules: "jarjar-rules.txt",
     optimize: {
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 776832f..3cad1c6 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
@@ -28,7 +28,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
@@ -66,31 +66,31 @@
 
     // BPF map for downstream IPv4 forwarding.
     @Nullable
-    private final BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+    private final IBpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
 
     // BPF map for upstream IPv4 forwarding.
     @Nullable
-    private final BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+    private final IBpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
 
     // BPF map for downstream IPv6 forwarding.
     @Nullable
-    private final BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+    private final IBpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
 
     // BPF map for upstream IPv6 forwarding.
     @Nullable
-    private final BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+    private final IBpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
 
     // BPF map of tethering statistics of the upstream interface since tethering startup.
     @Nullable
-    private final BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+    private final IBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
 
     // BPF map of per-interface quota for tethering offload.
     @Nullable
-    private final BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+    private final IBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
 
     // BPF map of interface index mapping for XDP.
     @Nullable
-    private final BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+    private final IBpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
 
     // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for
     // reducing the BPF map iteration query. The count is increased or decreased when the rule is
@@ -482,7 +482,7 @@
         return true;
     }
 
-    private String mapStatus(BpfMap m, String name) {
+    private String mapStatus(IBpfMap m, String name) {
         return name + "{" + (m != null ? "OK" : "ERROR") + "}";
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 05a2884..142a0b9 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -60,6 +60,7 @@
 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.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.SharedLog;
@@ -320,7 +321,7 @@
         }
 
         /** Get downstream4 BPF map. */
-        @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+        @Nullable public IBpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
@@ -332,7 +333,7 @@
         }
 
         /** Get upstream4 BPF map. */
-        @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+        @Nullable public IBpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
@@ -344,7 +345,7 @@
         }
 
         /** Get downstream6 BPF map. */
-        @Nullable public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+        @Nullable public IBpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
@@ -356,7 +357,7 @@
         }
 
         /** Get upstream6 BPF map. */
-        @Nullable public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+        @Nullable public IBpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
@@ -368,7 +369,7 @@
         }
 
         /** Get stats BPF map. */
-        @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+        @Nullable public IBpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_STATS_MAP_PATH,
@@ -380,7 +381,7 @@
         }
 
         /** Get limit BPF map. */
-        @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+        @Nullable public IBpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
@@ -392,7 +393,7 @@
         }
 
         /** Get dev BPF map. */
-        @Nullable public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+        @Nullable public IBpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
             if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DEV_MAP_PATH,
@@ -1047,7 +1048,7 @@
         }
     }
     private void dumpBpfStats(@NonNull IndentingPrintWriter pw) {
-        try (BpfMap<TetherStatsKey, TetherStatsValue> map = mDeps.getBpfStatsMap()) {
+        try (IBpfMap<TetherStatsKey, TetherStatsValue> map = mDeps.getBpfStatsMap()) {
             if (map == null) {
                 pw.println("No BPF stats map");
                 return;
@@ -1102,7 +1103,7 @@
     }
 
     private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
-        try (BpfMap<TetherUpstream6Key, Tether6Value> map = mDeps.getBpfUpstream6Map()) {
+        try (IBpfMap<TetherUpstream6Key, Tether6Value> map = mDeps.getBpfUpstream6Map()) {
             if (map == null) {
                 pw.println("No IPv6 upstream");
                 return;
@@ -1130,7 +1131,7 @@
     }
 
     private void dumpIpv6DownstreamRules(IndentingPrintWriter pw) {
-        try (BpfMap<TetherDownstream6Key, Tether6Value> map = mDeps.getBpfDownstream6Map()) {
+        try (IBpfMap<TetherDownstream6Key, Tether6Value> map = mDeps.getBpfDownstream6Map()) {
             if (map == null) {
                 pw.println("No IPv6 downstream");
                 return;
@@ -1161,7 +1162,7 @@
         pw.decreaseIndent();
     }
 
-    private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+    private <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
             IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
             pw.println("No BPF support");
@@ -1192,7 +1193,7 @@
         // expected argument order.
         // TODO: dump downstream4 map.
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
-            try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+            try (IBpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
                 dumpRawMap(statsMap, pw);
             } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping stats map: " + e);
@@ -1200,7 +1201,7 @@
             return;
         }
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
-            try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
+            try (IBpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
                 dumpRawMap(upstreamMap, pw);
             } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
@@ -1245,7 +1246,7 @@
     }
 
     private void dumpIpv4ForwardingRuleMap(long now, boolean downstream,
-            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+            IBpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
             pw.println("No IPv4 support");
             return;
@@ -1260,8 +1261,8 @@
     private void dumpBpfForwardingRulesIpv4(IndentingPrintWriter pw) {
         final long now = SystemClock.elapsedRealtimeNanos();
 
-        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map();
-                BpfMap<Tether4Key, Tether4Value> downstreamMap = mDeps.getBpfDownstream4Map()) {
+        try (IBpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map();
+                IBpfMap<Tether4Key, Tether4Value> downstreamMap = mDeps.getBpfDownstream4Map()) {
             pw.println("IPv4 Upstream: proto [inDstMac] iif(iface) src -> nat -> "
                     + "dst [outDstMac] age");
             pw.increaseIndent();
@@ -1283,7 +1284,7 @@
             pw.println("No counter support");
             return;
         }
-        try (BpfMap<S32, S32> map = new BpfMap<>(TETHER_ERROR_MAP_PATH, BpfMap.BPF_F_RDONLY,
+        try (IBpfMap<S32, S32> map = new BpfMap<>(TETHER_ERROR_MAP_PATH, BpfMap.BPF_F_RDONLY,
                 S32.class, S32.class)) {
 
             map.forEach((k, v) -> {
@@ -1304,7 +1305,7 @@
     }
 
     private void dumpDevmap(@NonNull IndentingPrintWriter pw) {
-        try (BpfMap<TetherDevKey, TetherDevValue> map = mDeps.getBpfDevMap()) {
+        try (IBpfMap<TetherDevKey, TetherDevValue> map = mDeps.getBpfDevMap()) {
             if (map == null) {
                 pw.println("No devmap support");
                 return;
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 6014722..880a285 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -24,7 +24,9 @@
 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.TestDnsPacket;
 import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpDnsPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
 import static android.system.OsConstants.IPPROTO_IP;
 import static android.system.OsConstants.IPPROTO_IPV6;
@@ -81,7 +83,9 @@
 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.structs.Ipv4Header;
 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.IgnoreUpTo;
 import com.android.testutils.DeviceInfoUtils;
@@ -156,6 +160,8 @@
     private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
+    private static final short DNS_PORT = 53;
+
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
     private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -165,6 +171,66 @@
     private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
     private static final short HOP_LIMIT = 0x40;
 
+    // TODO: use class DnsPacket to build DNS query and reply message once DnsPacket supports
+    // building packet for given arguments.
+    private static final ByteBuffer DNS_QUERY = ByteBuffer.wrap(new byte[] {
+            // scapy.DNS(
+            //   id=0xbeef,
+            //   qr=0,
+            //   qd=scapy.DNSQR(qname="hello.example.com"))
+            //
+            /* Header */
+            (byte) 0xbe, (byte) 0xef, /* Transaction ID: 0xbeef */
+            (byte) 0x01, (byte) 0x00, /* Flags: rd */
+            (byte) 0x00, (byte) 0x01, /* Questions: 1 */
+            (byte) 0x00, (byte) 0x00, /* Answer RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
+            /* Queries */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00, /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,              /* Type: A */
+            (byte) 0x00, (byte) 0x01               /* Class: IN */
+    });
+
+    private static final byte[] DNS_REPLY = new byte[] {
+            // scapy.DNS(
+            //   id=0,
+            //   qr=1,
+            //   qd=scapy.DNSQR(qname="hello.example.com"),
+            //   an=scapy.DNSRR(rrname="hello.example.com", rdata='1.2.3.4'))
+            //
+            /* Header */
+            (byte) 0x00, (byte) 0x00, /* Transaction ID: 0x0, must be updated by dns query id */
+            (byte) 0x81, (byte) 0x00, /* Flags: qr rd */
+            (byte) 0x00, (byte) 0x01, /* Questions: 1 */
+            (byte) 0x00, (byte) 0x01, /* Answer RRs: 1 */
+            (byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
+            (byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
+            /* Queries */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00,              /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,                           /* Type: A */
+            (byte) 0x00, (byte) 0x01,                           /* Class: IN */
+            /* Answers */
+            (byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
+            (byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
+            (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
+            (byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x00,              /* Name: hello.example.com */
+            (byte) 0x00, (byte) 0x01,                           /* Type: A */
+            (byte) 0x00, (byte) 0x01,                           /* Class: IN */
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* Time to live: 0 */
+            (byte) 0x00, (byte) 0x04,                           /* Data length: 4 */
+            (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04  /* Address: 1.2.3.4 */
+    };
+
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
     private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -212,26 +278,26 @@
             mUpstreamReader = null;
         }
 
-        runAsShell(TETHER_PRIVILEGED, () -> {
-            mTm.stopTethering(TETHERING_ETHERNET);
-            // Binder call is an async call. Need to hold the shell permission until tethering
-            // stopped. This helps to avoid the test become flaky.
-            if (mTetheringEventCallback != null) {
-                mTetheringEventCallback.awaitInterfaceUntethered();
-                mTetheringEventCallback.unregister();
-                mTetheringEventCallback = null;
-            }
-        });
         if (mDownstreamReader != null) {
             TapPacketReader reader = mDownstreamReader;
             mHandler.post(() -> reader.stop());
             mDownstreamReader = null;
         }
-        runAsShell(NETWORK_SETTINGS, () -> {
-            mTetheredInterfaceRequester.release();
-        });
-        setIncludeTestInterfaces(false);
+
+        // To avoid flaky which caused by the next test started but the previous interface is not
+        // untracked from EthernetTracker yet. Just delete the test interface without explicitly
+        // calling TetheringManager#stopTethering could let EthernetTracker untrack the test
+        // interface from server mode before tethering stopped. Thus, awaitInterfaceUntethered
+        // could not only make sure tethering is stopped but also guarantee the test interface is
+        // untracked from EthernetTracker.
         maybeDeleteTestInterface();
+        if (mTetheringEventCallback != null) {
+            mTetheringEventCallback.awaitInterfaceUntethered();
+            mTetheringEventCallback.unregister();
+            mTetheringEventCallback = null;
+        }
+        runAsShell(NETWORK_SETTINGS, () -> mTetheredInterfaceRequester.release());
+        setIncludeTestInterfaces(false);
     }
 
     @After
@@ -1371,6 +1437,94 @@
         runClatUdpTest();
     }
 
+    @NonNull
+    private ByteBuffer buildDnsReplyMessageById(short id) {
+        byte[] replyMessage = Arrays.copyOf(DNS_REPLY, DNS_REPLY.length);
+        // Assign transaction id of reply message pattern with a given DNS transaction id.
+        replyMessage[0] = (byte) ((id >> 8) & 0xff);
+        replyMessage[1] = (byte) (id & 0xff);
+        Log.d(TAG, "Built DNS reply: " + dumpHexString(replyMessage));
+
+        return ByteBuffer.wrap(replyMessage);
+    }
+
+    @NonNull
+    private void sendDownloadPacketDnsV4(@NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, short srcPort, short dstPort, short dnsId,
+            @NonNull final TetheringTester tester) throws Exception {
+        // DNS response transaction id must be copied from DNS query. Used by the requester
+        // to match up replies to outstanding queries. See RFC 1035 section 4.1.1.
+        final ByteBuffer dnsReplyMessage = buildDnsReplyMessageById(dnsId);
+        final ByteBuffer testPacket = buildUdpPacket((InetAddress) srcIp,
+                (InetAddress) dstIp, srcPort, dstPort, dnsReplyMessage);
+
+        tester.verifyDownload(testPacket, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedUdpDnsPacket(p, true /* hasEther */, true /* isIpv4 */,
+                    dnsReplyMessage);
+        });
+    }
+
+    // Send IPv4 UDP DNS packet and return the forwarded DNS packet on upstream.
+    @NonNull
+    private byte[] sendUploadPacketDnsV4(@NonNull final MacAddress srcMac,
+            @NonNull final MacAddress dstMac, @NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+            @NonNull final TetheringTester tester) throws Exception {
+        final ByteBuffer testPacket = buildUdpPacket(srcMac, dstMac, srcIp, dstIp,
+                srcPort, dstPort, DNS_QUERY);
+
+        return tester.verifyUpload(testPacket, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+            return isExpectedUdpDnsPacket(p, false /* hasEther */, true /* isIpv4 */,
+                    DNS_QUERY);
+        });
+    }
+
+    @Test
+    public void testTetherUdpV4Dns() throws Exception {
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+                toList(TEST_IP4_DNS));
+        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+        // TODO: remove the connectivity verification for upstream connected notification race.
+        // See the same reason in runUdp4Test().
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
+
+        // [1] Send DNS query.
+        // tethered device --> downstream --> dnsmasq forwarding --> upstream --> DNS server
+        //
+        // Need to extract DNS transaction id and source port from dnsmasq forwarded DNS query
+        // packet. dnsmasq forwarding creats new query which means UDP source port and DNS
+        // transaction id are changed from original sent DNS query. See forward_query() in
+        // external/dnsmasq/src/forward.c. Note that #TetheringTester.isExpectedUdpDnsPacket
+        // guarantees that |forwardedQueryPacket| is a valid DNS packet. So we can parse it as DNS
+        // packet.
+        final MacAddress srcMac = tethered.macAddr;
+        final MacAddress dstMac = tethered.routerMacAddr;
+        final Inet4Address clientIp = tethered.ipv4Addr;
+        final Inet4Address gatewayIp = tethered.ipv4Gatway;
+        final byte[] forwardedQueryPacket = sendUploadPacketDnsV4(srcMac, dstMac, clientIp,
+                gatewayIp, LOCAL_PORT, DNS_PORT, tester);
+        final ByteBuffer buf = ByteBuffer.wrap(forwardedQueryPacket);
+        Struct.parse(Ipv4Header.class, buf);
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
+        final TestDnsPacket dnsQuery = TestDnsPacket.getTestDnsPacket(buf);
+        assertNotNull(dnsQuery);
+        Log.d(TAG, "Forwarded UDP source port: " + udpHeader.srcPort + ", DNS query id: "
+                + dnsQuery.getHeader().id);
+
+        // [2] Send DNS reply.
+        // DNS server --> upstream --> dnsmasq forwarding --> downstream --> tethered device
+        //
+        // DNS reply transaction id must be copied from DNS query. Used by the requester to match
+        // up replies to outstanding queries. See RFC 1035 section 4.1.1.
+        final Inet4Address remoteIp = (Inet4Address) TEST_IP4_DNS;
+        final Inet4Address tetheringUpstreamIp = (Inet4Address) TEST_IP4_ADDR.getAddress();
+        sendDownloadPacketDnsV4(remoteIp, tetheringUpstreamIp, DNS_PORT,
+                (short) udpHeader.srcPort, (short) dnsQuery.getHeader().id, tester);
+    }
+
     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 4d90d39..9cc2e49 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -20,6 +20,11 @@
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.DnsPacket.ANSECTION;
+import static com.android.net.module.util.DnsPacket.ARSECTION;
+import static com.android.net.module.util.DnsPacket.NSSECTION;
+import static com.android.net.module.util.DnsPacket.QDSECTION;
+import static com.android.net.module.util.HexDump.dumpHexString;
 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;
@@ -41,12 +46,14 @@
 import android.net.dhcp.DhcpAckPacket;
 import android.net.dhcp.DhcpOfferPacket;
 import android.net.dhcp.DhcpPacket;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.net.module.util.DnsPacket;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
@@ -79,7 +86,7 @@
  */
 public final class TetheringTester {
     private static final String TAG = TetheringTester.class.getSimpleName();
-    private static final int PACKET_READ_TIMEOUT_MS = 100;
+    private static final int PACKET_READ_TIMEOUT_MS = 500;
     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[] {
@@ -124,12 +131,14 @@
         public final MacAddress macAddr;
         public final MacAddress routerMacAddr;
         public final Inet4Address ipv4Addr;
+        public final Inet4Address ipv4Gatway;
         public final Inet6Address ipv6Addr;
 
         private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
             macAddr = mac;
             DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
             ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
+            ipv4Gatway = (Inet4Address) dhcpResults.gateway;
             routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
                     dhcpResults.serverAddress);
             ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
@@ -386,8 +395,8 @@
         }
     }
 
-    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
-            boolean isIpv4, @NonNull final ByteBuffer payload) {
+    private static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, Predicate<ByteBuffer> payloadVerifier) {
         final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
         try {
             if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
@@ -395,15 +404,178 @@
             if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
 
             if (Struct.parse(UdpHeader.class, buf) == null) return false;
+
+            if (!payloadVerifier.test(buf)) return false;
         } catch (Exception e) {
             // Parsing packet fail means it is not udp packet.
             return false;
         }
+        return true;
+    }
 
-        if (buf.remaining() != payload.limit()) return false;
+    // Returns remaining bytes in the ByteBuffer in a new byte array of the right size. The
+    // ByteBuffer will be empty upon return. Used to avoid lint warning.
+    // See https://errorprone.info/bugpattern/ByteBufferBackingArray
+    private static byte[] getRemaining(final ByteBuffer buf) {
+        final byte[] bytes = new byte[buf.remaining()];
+        buf.get(bytes);
+        Log.d(TAG, "Get remaining bytes: " + dumpHexString(bytes));
+        return bytes;
+    }
 
-        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
-                payload.array());
+    // |expectedPayload| is copied as read-only because the caller may reuse it.
+    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
+        return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
+            if (p.remaining() != expectedPayload.limit()) return false;
+
+            return Arrays.equals(getRemaining(p), getRemaining(
+                    expectedPayload.asReadOnlyBuffer()));
+        });
+    }
+
+    // |expectedPayload| is copied as read-only because the caller may reuse it.
+    // See hasExpectedDnsMessage.
+    public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
+        return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
+            return hasExpectedDnsMessage(p, expectedPayload);
+        });
+    }
+
+    public static class TestDnsPacket extends DnsPacket {
+        TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
+            super(data);
+        }
+
+        @Nullable
+        public static TestDnsPacket getTestDnsPacket(final ByteBuffer buf) {
+            try {
+                // The ByteBuffer will be empty upon return.
+                return new TestDnsPacket(getRemaining(buf));
+            } catch (DnsPacket.ParseException e) {
+                return null;
+            }
+        }
+
+        public DnsHeader getHeader() {
+            return mHeader;
+        }
+
+        public List<DnsRecord> getRecordList(int secType) {
+            return mRecords[secType];
+        }
+
+        public int getANCount() {
+            return mHeader.getRecordCount(ANSECTION);
+        }
+
+        public int getQDCount() {
+            return mHeader.getRecordCount(QDSECTION);
+        }
+
+        public int getNSCount() {
+            return mHeader.getRecordCount(NSSECTION);
+        }
+
+        public int getARCount() {
+            return mHeader.getRecordCount(ARSECTION);
+        }
+
+        private boolean isRecordsEquals(int type, @NonNull final TestDnsPacket other) {
+            List<DnsRecord> records = getRecordList(type);
+            List<DnsRecord> otherRecords = other.getRecordList(type);
+
+            if (records.size() != otherRecords.size()) return false;
+
+            // Expect that two compared resource records are in the same order. For current tests
+            // in EthernetTetheringTest, it is okay because dnsmasq doesn't reorder the forwarded
+            // resource records.
+            // TODO: consider allowing that compare records out of order.
+            for (int i = 0; i < records.size(); i++) {
+                // TODO: use DnsRecord.equals once aosp/1387135 is merged.
+                if (!TextUtils.equals(records.get(i).dName, otherRecords.get(i).dName)
+                        || records.get(i).nsType != otherRecords.get(i).nsType
+                        || records.get(i).nsClass != otherRecords.get(i).nsClass
+                        || records.get(i).ttl != otherRecords.get(i).ttl
+                        || !Arrays.equals(records.get(i).getRR(), otherRecords.get(i).getRR())) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public boolean isQDRecordsEquals(@NonNull final TestDnsPacket other) {
+            return isRecordsEquals(QDSECTION, other);
+        }
+
+        public boolean isANRecordsEquals(@NonNull final TestDnsPacket other) {
+            return isRecordsEquals(ANSECTION, other);
+        }
+    }
+
+    // The ByteBuffer |actual| will be empty upon return. The ByteBuffer |excepted| will be copied
+    // as read-only because the caller may reuse it.
+    private static boolean hasExpectedDnsMessage(@NonNull final ByteBuffer actual,
+            @NonNull final ByteBuffer excepted) {
+        // Forwarded DNS message is extracted from remaining received packet buffer which has
+        // already parsed ethernet header, if any, IP header and UDP header.
+        final TestDnsPacket forwardedDns = TestDnsPacket.getTestDnsPacket(actual);
+        if (forwardedDns == null) return false;
+
+        // Original DNS message is the payload of the sending test UDP packet. It is used to check
+        // that the forwarded DNS query and reply have corresponding contents.
+        final TestDnsPacket originalDns = TestDnsPacket.getTestDnsPacket(
+                excepted.asReadOnlyBuffer());
+        assertNotNull(originalDns);
+
+        // Compare original DNS message which is sent to dnsmasq and forwarded DNS message which
+        // is forwarded by dnsmasq. The original message and forwarded message may be not identical
+        // because dnsmasq may change the header flags or even recreate the DNS query message and
+        // so on. We only simple check on forwarded packet and monitor if test will be broken by
+        // vendor dnsmasq customization. See forward_query() in external/dnsmasq/src/forward.c.
+        //
+        // DNS message format. See rfc1035 section 4.1.
+        // +---------------------+
+        // |        Header       |
+        // +---------------------+
+        // |       Question      | the question for the name server
+        // +---------------------+
+        // |        Answer       | RRs answering the question
+        // +---------------------+
+        // |      Authority      | RRs pointing toward an authority
+        // +---------------------+
+        // |      Additional     | RRs holding additional information
+        // +---------------------+
+
+        // [1] Header section. See rfc1035 section 4.1.1.
+        // Verify QR flag bit, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT.
+        if (originalDns.getHeader().isResponse() != forwardedDns.getHeader().isResponse()) {
+            return false;
+        }
+        if (originalDns.getQDCount() != forwardedDns.getQDCount()) return false;
+        if (originalDns.getANCount() != forwardedDns.getANCount()) return false;
+        if (originalDns.getNSCount() != forwardedDns.getNSCount()) return false;
+        if (originalDns.getARCount() != forwardedDns.getARCount()) return false;
+
+        // [2] Question section. See rfc1035 section 4.1.2.
+        // Question section has at least one entry either DNS query or DNS reply.
+        if (forwardedDns.getRecordList(QDSECTION).isEmpty()) return false;
+        // Expect that original and forwarded message have the same question records (usually 1).
+        if (!originalDns.isQDRecordsEquals(forwardedDns)) return false;
+
+        // [3] Answer section. See rfc1035 section 4.1.3.
+        if (forwardedDns.getHeader().isResponse()) {
+            // DNS reply has at least have one answer in our tests.
+            // See EthernetTetheringTest#testTetherUdpV4Dns.
+            if (forwardedDns.getRecordList(ANSECTION).isEmpty()) return false;
+            // Expect that original and forwarded message have the same answer records.
+            if (!originalDns.isANRecordsEquals(forwardedDns)) return false;
+        }
+
+        // Ignore checking {Authority, Additional} sections because they are not tested
+        // in EthernetTetheringTest.
+        return true;
     }
 
     private void sendUploadPacket(ByteBuffer packet) throws Exception {
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 b100f58..758b533 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -94,8 +94,8 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
-import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.SharedLog;
@@ -362,9 +362,9 @@
     @Mock private IpServer mIpServer2;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
-    @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
-    @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
-    @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+    @Mock private IBpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+    @Mock private IBpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+    @Mock private IBpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -379,13 +379,13 @@
     private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
             ArgumentCaptor.forClass(ArrayList.class);
     private final TestLooper mTestLooper = new TestLooper();
-    private final BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map =
+    private final IBpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map =
             spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
-    private final BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map =
+    private final IBpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map =
             spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
-    private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
+    private final IBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
             spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class));
-    private final TestBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap =
+    private final IBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap =
             spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class));
     private BpfCoordinator.Dependencies mDeps =
             spy(new BpfCoordinator.Dependencies() {
@@ -424,37 +424,37 @@
                     }
 
                     @Nullable
-                    public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+                    public IBpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
                         return mBpfDownstream4Map;
                     }
 
                     @Nullable
-                    public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+                    public IBpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
                         return mBpfUpstream4Map;
                     }
 
                     @Nullable
-                    public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+                    public IBpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
                         return mBpfDownstream6Map;
                     }
 
                     @Nullable
-                    public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+                    public IBpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
                         return mBpfUpstream6Map;
                     }
 
                     @Nullable
-                    public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+                    public IBpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
                         return mBpfStatsMap;
                     }
 
                     @Nullable
-                    public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+                    public IBpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
                         return mBpfLimitMap;
                     }
 
                     @Nullable
-                    public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+                    public IBpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
                         return mBpfDevMap;
                     }
             });
diff --git a/framework-t/src/android/net/NetworkIdentity.java b/framework-t/src/android/net/NetworkIdentity.java
index da5f88d..350ed86 100644
--- a/framework-t/src/android/net/NetworkIdentity.java
+++ b/framework-t/src/android/net/NetworkIdentity.java
@@ -85,6 +85,12 @@
 
     private static final long SUPPORTED_OEM_MANAGED_TYPES = OEM_PAID | OEM_PRIVATE;
 
+    // Need to be synchronized with ConnectivityManager.
+    // TODO: Use {@code ConnectivityManager#*} when visible.
+    static final int TYPE_TEST = 18;
+    private static final int MAX_NETWORK_TYPE = TYPE_TEST;
+    private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;
+
     final int mType;
     final int mRatType;
     final int mSubId;
@@ -346,11 +352,6 @@
      * Builder class for {@link NetworkIdentity}.
      */
     public static final class Builder {
-        // Need to be synchronized with ConnectivityManager.
-        // TODO: Use {@link ConnectivityManager#MAX_NETWORK_TYPE} when this file is in the module.
-        private static final int MAX_NETWORK_TYPE = 18; // TYPE_TEST
-        private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;
-
         private int mType;
         private int mRatType;
         private String mSubscriberId;
@@ -413,6 +414,12 @@
                     final WifiInfo info = (WifiInfo) transportInfo;
                     setWifiNetworkKey(info.getNetworkKey());
                 }
+            } else if (mType == TYPE_TEST) {
+                final NetworkSpecifier ns = snapshot.getNetworkCapabilities().getNetworkSpecifier();
+                if (ns instanceof TestNetworkSpecifier) {
+                    // Reuse the wifi network key field to identify individual test networks.
+                    setWifiNetworkKey(((TestNetworkSpecifier) ns).getInterfaceName());
+                }
             }
             return this;
         }
@@ -574,7 +581,7 @@
             }
 
             // Assert non-wifi network cannot have a wifi network key.
-            if (mType != TYPE_WIFI && mWifiNetworkKey != null) {
+            if (mType != TYPE_WIFI && mType != TYPE_TEST && mWifiNetworkKey != null) {
                 throw new IllegalArgumentException("Invalid wifi network key for type " + mType);
             }
         }
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index b82a126..b6bd1a5 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -50,6 +50,7 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkIdentityUtils;
 import com.android.net.module.util.NetworkStatsUtils;
@@ -114,6 +115,14 @@
      * may offer non-cellular networks like WiFi, which will be matched by this rule.
      */
     public static final int MATCH_CARRIER = 10;
+    /**
+     * Match rule to match networks with {@link ConnectivityManager#TYPE_TEST} as the legacy
+     * network type.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static final int MATCH_TEST = 11;
 
     // TODO: Remove this and replace all callers with WIFI_NETWORK_KEY_ALL.
     /** @hide */
@@ -176,6 +185,7 @@
             case MATCH_BLUETOOTH:
             case MATCH_PROXY:
             case MATCH_CARRIER:
+            case MATCH_TEST:
                 return true;
 
             default:
@@ -666,6 +676,8 @@
                 return matchesProxy(ident);
             case MATCH_CARRIER:
                 return matchesCarrier(ident);
+            case MATCH_TEST:
+                return matchesTest(ident);
             default:
                 // We have no idea what kind of network template we are, so we
                 // just claim not to match anything.
@@ -776,6 +788,17 @@
                 && CollectionUtils.contains(mMatchSubscriberIds, ident.mSubscriberId);
     }
 
+    /**
+     * Check if matches test network. If the wifiNetworkKeys in the template is specified, Then it
+     * will only match a network containing any of the specified the wifi network key. Otherwise,
+     * all test networks would be matched.
+     */
+    private boolean matchesTest(NetworkIdentity ident) {
+        return ident.mType == NetworkIdentity.TYPE_TEST
+                && ((CollectionUtils.isEmpty(mMatchWifiNetworkKeys)
+                || CollectionUtils.contains(mMatchWifiNetworkKeys, ident.mWifiNetworkKey)));
+    }
+
     private boolean matchesMobileWildcard(NetworkIdentity ident) {
         if (ident.mType == TYPE_WIMAX) {
             return true;
@@ -829,6 +852,8 @@
                 return "PROXY";
             case MATCH_CARRIER:
                 return "CARRIER";
+            case MATCH_TEST:
+                return "TEST";
             default:
                 return "UNKNOWN(" + matchRule + ")";
         }
@@ -1079,7 +1104,9 @@
         }
 
         private void validateWifiNetworkKeys() {
-            if (mMatchRule != MATCH_WIFI && !mMatchWifiNetworkKeys.isEmpty()) {
+            // Also allow querying test networks which use wifi network key as identifier.
+            if (mMatchRule != MATCH_WIFI && mMatchRule != MATCH_TEST
+                    && !mMatchWifiNetworkKeys.isEmpty()) {
                 throw new IllegalArgumentException("Trying to build non wifi match rule: "
                         + mMatchRule + " with wifi network keys");
             }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1fbbd25..547b4ba 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1167,6 +1167,8 @@
                 return "PROXY";
             case TYPE_VPN:
                 return "VPN";
+            case TYPE_TEST:
+                return "TEST";
             default:
                 return Integer.toString(type);
         }
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index b6f3314..da12a0a 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -188,7 +188,8 @@
      * Set to true if the PRIVATE_DNS_BROKEN notification has shown for this network.
      * Reset this bit when private DNS mode is changed from strict mode to opportunistic/off mode.
      *
-     * This is not parceled, because it would not make sense.
+     * This is not parceled, because it would not make sense. It's also ignored by the
+     * equals() and hashcode() methods.
      *
      * @hide
      */
@@ -503,8 +504,10 @@
                 && provisioningNotificationDisabled == that.provisioningNotificationDisabled
                 && skip464xlat == that.skip464xlat
                 && legacyType == that.legacyType
+                && legacySubType == that.legacySubType
                 && Objects.equals(subscriberId, that.subscriberId)
                 && Objects.equals(legacyTypeName, that.legacyTypeName)
+                && Objects.equals(legacySubTypeName, that.legacySubTypeName)
                 && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo)
                 && excludeLocalRouteVpn == that.excludeLocalRouteVpn
                 && mVpnRequiresValidation == that.mVpnRequiresValidation;
@@ -514,8 +517,8 @@
     public int hashCode() {
         return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
                 acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
-                skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo, excludeLocalRouteVpn,
-                mVpnRequiresValidation);
+                skip464xlat, legacyType, legacySubType, legacyTypeName, legacySubTypeName,
+                mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation);
     }
 
     @Override
@@ -529,8 +532,10 @@
                 + ", subscriberId = '" + subscriberId + '\''
                 + ", skip464xlat = " + skip464xlat
                 + ", legacyType = " + legacyType
+                + ", legacySubType = " + legacySubType
                 + ", hasShownBroken = " + hasShownBroken
                 + ", legacyTypeName = '" + legacyTypeName + '\''
+                + ", legacySubTypeName = '" + legacySubTypeName + '\''
                 + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
                 + ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\''
                 + ", vpnRequiresValidation = '" + mVpnRequiresValidation + '\''
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 994db1d..3f7ed2a 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -110,12 +110,12 @@
 }
 
 Status BpfHandler::initMaps() {
-    std::lock_guard guard(mMutex);
-    RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
     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(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    // initialized last so mCookieTagMap.isValid() implies everything else is valid too
+    RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
     ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
@@ -133,7 +133,6 @@
 }
 
 int BpfHandler::tagSocket(int sockFd, uint32_t tag, uid_t chargeUid, uid_t realUid) {
-    std::lock_guard guard(mMutex);
     if (!mCookieTagMap.isValid()) return -EPERM;
 
     if (chargeUid != realUid && !hasUpdateDeviceStatsPermission(realUid)) return -EPERM;
@@ -185,9 +184,9 @@
     uint32_t perUidEntryCount = 0;
     // Now we go through the stats map and count how many entries are associated
     // with chargeUid. If the uid entry hit the limit for each chargeUid, we block
-    // the request to prevent the map from overflow. It is safe here to iterate
-    // over the map since when mMutex is hold, system server cannot toggle
-    // the live stats map and clean it. So nobody can delete entries from the map.
+    // the request to prevent the map from overflow. Note though that it isn't really
+    // safe here to iterate over the map since it might be modified by the system server,
+    // which might toggle the live stats map and clean it.
     const auto countUidStatsEntries = [chargeUid, &totalEntryCount, &perUidEntryCount](
                                               const StatsKey& key,
                                               const BpfMap<StatsKey, StatsValue>&) {
@@ -227,9 +226,9 @@
     }
     // Update the tag information of a socket to the cookieUidMap. Use BPF_ANY
     // flag so it will insert a new entry to the map if that value doesn't exist
-    // yet. And update the tag if there is already a tag stored. Since the eBPF
+    // yet and update the tag if there is already a tag stored. Since the eBPF
     // program in kernel only read this map, and is protected by rcu read lock. It
-    // should be fine to cocurrently update the map while eBPF program is running.
+    // should be fine to concurrently update the map while eBPF program is running.
     res = mCookieTagMap.writeValue(sock_cookie, newKey, BPF_ANY);
     if (!res.ok()) {
         ALOGE("Failed to tag the socket: %s, fd: %d", strerror(res.error().code()),
@@ -240,8 +239,6 @@
 }
 
 int BpfHandler::untagSocket(int sockFd) {
-    std::lock_guard guard(mMutex);
-
     uint64_t sock_cookie = getSocketCookie(sockFd);
     if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
 
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 5ee04d1..925a725 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -16,8 +16,6 @@
 
 #pragma once
 
-#include <mutex>
-
 #include <netdutils/Status.h>
 #include "bpf/BpfMap.h"
 #include "bpf_shared.h"
@@ -66,8 +64,6 @@
     BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
-    std::mutex mMutex;
-
     // The limit on the number of stats entries a uid can have in the per uid stats map. BpfHandler
     // will block that specific uid from tagging new sockets after the limit is reached.
     const uint32_t mPerUidStatsEntriesLimit;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index 99160da..f5c9a68 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -53,7 +53,6 @@
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
-        std::lock_guard guard(mBh.mMutex);
         ASSERT_EQ(0, setrlimitForTest());
 
         mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 6605428..28de881 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,10 +40,6 @@
 
 using base::Result;
 
-// The target map for stats reading should be the inactive map, which is opposite
-// from the config value.
-static constexpr char const* STATS_MAP_PATH[] = {STATS_MAP_B_PATH, STATS_MAP_A_PATH};
-
 int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
                            const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
     auto statsEntry = appUidStatsMap.readValue(uid);
@@ -171,30 +167,42 @@
                                int limitUid) {
     static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
+    static BpfMap<StatsKey, StatsValue> statsMapA(STATS_MAP_A_PATH);
+    static BpfMap<StatsKey, StatsValue> statsMapB(STATS_MAP_B_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();
     }
-    if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+    // The target map for stats reading should be the inactive map, which is opposite
+    // from the config value.
+    BpfMap<StatsKey, StatsValue> *inactiveStatsMap;
+    switch (configuration.value()) {
+      case SELECT_MAP_A:
+        inactiveStatsMap = &statsMapB;
+        break;
+      case SELECT_MAP_B:
+        inactiveStatsMap = &statsMapA;
+        break;
+      default:
         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.
-    int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid, statsMap,
-                                                 ifaceIndexNameMap);
+    // TODO: the above comment feels like it may be obsolete / out of date,
+    // since we no longer swap the map via netd binder rpc - though we do
+    // still swap it.
+    int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid,
+                                                 *inactiveStatsMap, ifaceIndexNameMap);
     if (ret) {
         ALOGE("parse detail network stats failed: %s", strerror(errno));
         return ret;
     }
 
-    Result<void> res = statsMap.clear();
+    Result<void> res = inactiveStatsMap->clear();
     if (!res.ok()) {
         ALOGE("Clean up current stats map failed: %s", strerror(res.error().code()));
         return -res.error().code();
diff --git a/service-t/src/com/android/server/ethernet/EthernetCallback.java b/service-t/src/com/android/server/ethernet/EthernetCallback.java
new file mode 100644
index 0000000..5461156
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetCallback.java
@@ -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.
+ */
+
+package com.android.server.ethernet;
+
+import android.net.EthernetNetworkManagementException;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/** Convenience wrapper for INetworkInterfaceOutcomeReceiver */
+@VisibleForTesting
+public class EthernetCallback {
+    private static final String TAG = EthernetCallback.class.getSimpleName();
+    private final INetworkInterfaceOutcomeReceiver mReceiver;
+
+    public EthernetCallback(INetworkInterfaceOutcomeReceiver receiver) {
+        mReceiver = receiver;
+    }
+
+    /** Calls INetworkInterfaceOutcomeReceiver#onResult */
+    public void onResult(String ifname) {
+        try {
+            if (mReceiver != null) {
+                mReceiver.onResult(ifname);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to report error to OutcomeReceiver", e);
+        }
+    }
+
+    /** Calls INetworkInterfaceOutcomeReceiver#onError */
+    public void onError(String msg) {
+        try {
+            if (mReceiver != null) {
+                mReceiver.onError(new EthernetNetworkManagementException(msg));
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to report error to OutcomeReceiver", e);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index e5bddf6..56c21eb 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -22,9 +22,7 @@
 import android.net.ConnectivityManager;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
-import android.net.EthernetNetworkManagementException;
 import android.net.EthernetNetworkSpecifier;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
@@ -42,7 +40,6 @@
 import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
 import android.util.ArraySet;
@@ -190,22 +187,19 @@
      *                     {@code null} is passed, then the network's current
      *                     {@link NetworkCapabilities} will be used in support of existing APIs as
      *                     the public API does not allow this.
-     * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of
-     *                 completion.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected void updateInterface(@NonNull final String ifaceName,
             @Nullable final IpConfiguration ipConfig,
-            @Nullable final NetworkCapabilities capabilities,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final NetworkCapabilities capabilities) {
         if (!hasInterface(ifaceName)) {
-            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
             return;
         }
 
         final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
-        iface.updateInterface(ipConfig, capabilities, listener);
+        iface.updateInterface(ipConfig, capabilities);
         mTrackingInterfaces.put(ifaceName, iface);
+        return;
     }
 
     private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
@@ -238,10 +232,8 @@
 
     /** Returns true if state has been modified */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up) {
         if (!hasInterface(ifaceName)) {
-            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
             return false;
         }
 
@@ -250,14 +242,7 @@
         }
 
         NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
-        return iface.updateLinkState(up, listener);
-    }
-
-    private void maybeSendNetworkManagementCallbackForUntracked(
-            String ifaceName, INetworkInterfaceOutcomeReceiver listener) {
-        maybeSendNetworkManagementCallback(listener, null,
-                new EthernetNetworkManagementException(
-                        ifaceName + " can't be updated as it is not available."));
+        return iface.updateLinkState(up);
     }
 
     @VisibleForTesting
@@ -265,25 +250,6 @@
         return mTrackingInterfaces.containsKey(ifaceName);
     }
 
-    private static void maybeSendNetworkManagementCallback(
-            @Nullable final INetworkInterfaceOutcomeReceiver listener,
-            @Nullable final String iface,
-            @Nullable final EthernetNetworkManagementException e) {
-        if (null == listener) {
-            return;
-        }
-
-        try {
-            if (iface != null) {
-                listener.onResult(iface);
-            } else {
-                listener.onError(e);
-            }
-        } catch (RemoteException re) {
-            Log.e(TAG, "Can't send onComplete for network management callback", re);
-        }
-    }
-
     @VisibleForTesting
     static class NetworkInterfaceState {
         final String name;
@@ -332,11 +298,6 @@
         private class EthernetIpClientCallback extends IpClientCallbacks {
             private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
             private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
-            @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener;
-
-            EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
-                mNetworkManagementListener = listener;
-            }
 
             @Override
             public void onIpClientCreated(IIpClient ipClient) {
@@ -372,14 +333,14 @@
 
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
-                handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
+                handleIpEvent(() -> onIpLayerStarted(newLp));
             }
 
             @Override
             public void onProvisioningFailure(LinkProperties newLp) {
                 // This cannot happen due to provisioning timeout, because our timeout is 0. It can
                 // happen due to errors while provisioning or on provisioning loss.
-                handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener));
+                handleIpEvent(() -> onIpLayerStopped());
             }
 
             @Override
@@ -491,13 +452,11 @@
         }
 
         void updateInterface(@Nullable final IpConfiguration ipConfig,
-                @Nullable final NetworkCapabilities capabilities,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+                @Nullable final NetworkCapabilities capabilities) {
             if (DBG) {
                 Log.d(TAG, "updateInterface, iface: " + name
                         + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
                         + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
-                        + ", listener: " + listener
                 );
             }
 
@@ -510,7 +469,7 @@
             // 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.
-            restart(listener);
+            restart();
         }
 
         boolean isRestricted() {
@@ -518,10 +477,6 @@
         }
 
         private void start() {
-            start(null);
-        }
-
-        private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
             if (mIpClient != null) {
                 if (DBG) Log.d(TAG, "IpClient already started");
                 return;
@@ -530,7 +485,7 @@
                 Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
             }
 
-            mIpClientCallback = new EthernetIpClientCallback(listener);
+            mIpClientCallback = new EthernetIpClientCallback();
             mDeps.makeIpClient(mContext, name, mIpClientCallback);
             mIpClientCallback.awaitIpClientStart();
 
@@ -540,8 +495,7 @@
             provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
         }
 
-        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties) {
             if (mNetworkAgent != null) {
                 Log.e(TAG, "Already have a NetworkAgent - aborting new request");
                 stop();
@@ -573,40 +527,18 @@
                     });
             mNetworkAgent.register();
             mNetworkAgent.markConnected();
-            realizeNetworkManagementCallback(name, null);
         }
 
-        void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        void onIpLayerStopped() {
             // There is no point in continuing if the interface is gone as stop() will be triggered
             // by removeInterface() when processed on the handler thread and start() won't
             // work for a non-existent interface.
             if (null == mDeps.getNetworkInterfaceByName(name)) {
                 if (DBG) Log.d(TAG, name + " is no longer available.");
                 // Send a callback in case a provisioning request was in progress.
-                maybeSendNetworkManagementCallbackForAbort();
                 return;
             }
-            restart(listener);
-        }
-
-        private void maybeSendNetworkManagementCallbackForAbort() {
-            realizeNetworkManagementCallback(null,
-                    new EthernetNetworkManagementException(
-                            "The IP provisioning request has been aborted."));
-        }
-
-        // Must be called on the handler thread
-        private void realizeNetworkManagementCallback(@Nullable final String iface,
-                @Nullable final EthernetNetworkManagementException e) {
-            ensureRunningOnEthernetHandlerThread();
-            if (null == mIpClientCallback) {
-                return;
-            }
-
-            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
-                    mIpClientCallback.mNetworkManagementListener, iface, e);
-            // Only send a single callback per listener.
-            mIpClientCallback.mNetworkManagementListener = null;
+            restart();
         }
 
         private void ensureRunningOnEthernetHandlerThread() {
@@ -636,12 +568,8 @@
         }
 
         /** Returns true if state has been modified */
-        boolean updateLinkState(final boolean up,
-                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        boolean updateLinkState(final boolean up) {
             if (mLinkUp == up)  {
-                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null,
-                        new EthernetNetworkManagementException(
-                                "No changes with requested link state " + up + " for " + name));
                 return false;
             }
             mLinkUp = up;
@@ -654,7 +582,6 @@
                 registerNetworkOffer();
             }
 
-            EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
             return true;
         }
 
@@ -665,8 +592,7 @@
                 mIpClientCallback.awaitIpClientShutdown();
                 mIpClient = null;
             }
-            // Send an abort callback if an updateInterface request was in progress.
-            maybeSendNetworkManagementCallbackForAbort();
+
             mIpClientCallback = null;
 
             if (mNetworkAgent != null) {
@@ -723,13 +649,9 @@
         }
 
         void restart() {
-            restart(null);
-        }
-
-        void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
             if (DBG) Log.d(TAG, "reconnecting Ethernet");
             stop();
-            start(listener);
+            start();
         }
 
         @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index dae3d2a..edf04b2 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -260,7 +260,7 @@
     @Override
     public void updateConfiguration(@NonNull final String iface,
             @NonNull final EthernetNetworkUpdateRequest request,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
         Objects.requireNonNull(iface);
         Objects.requireNonNull(request);
         throwIfEthernetNotStarted();
@@ -277,31 +277,31 @@
         }
 
         mTracker.updateConfiguration(
-                iface, request.getIpConfiguration(), nc, listener);
+                iface, request.getIpConfiguration(), nc, new EthernetCallback(cb));
     }
 
     @Override
     public void enableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
+        Log.i(TAG, "enableInterface called with: iface=" + iface + ", cb=" + cb);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
         enforceAdminPermission(iface, false, "enableInterface()");
 
-        mTracker.enableInterface(iface, listener);
+        mTracker.enableInterface(iface, new EthernetCallback(cb));
     }
 
     @Override
     public void disableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
+            @Nullable final INetworkInterfaceOutcomeReceiver cb) {
+        Log.i(TAG, "disableInterface called with: iface=" + iface + ", cb=" + cb);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
         enforceAdminPermission(iface, false, "disableInterface()");
 
-        mTracker.disableInterface(iface, listener);
+        mTracker.disableInterface(iface, new EthernetCallback(cb));
     }
 
     @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index ba367cf..00dff5b 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -29,7 +29,6 @@
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.ITetheredInterfaceCallback;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
@@ -271,7 +270,7 @@
         }
         writeIpConfiguration(iface, ipConfiguration);
         mHandler.post(() -> {
-            mFactory.updateInterface(iface, ipConfiguration, null, null);
+            mFactory.updateInterface(iface, ipConfiguration, null);
             broadcastInterfaceStateChange(iface);
         });
     }
@@ -335,7 +334,7 @@
     protected void updateConfiguration(@NonNull final String iface,
             @Nullable final IpConfiguration ipConfig,
             @Nullable final NetworkCapabilities capabilities,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final EthernetCallback cb) {
         if (DBG) {
             Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
                     + ", ipConfig: " + ipConfig);
@@ -353,21 +352,29 @@
             mNetworkCapabilities.put(iface, capabilities);
         }
         mHandler.post(() -> {
-            mFactory.updateInterface(iface, localIpConfig, capabilities, listener);
-            broadcastInterfaceStateChange(iface);
+            mFactory.updateInterface(iface, localIpConfig, capabilities);
+
+            // only broadcast state change when the ip configuration is updated.
+            if (ipConfig != null) {
+                broadcastInterfaceStateChange(iface);
+            }
+            // Always return success. Even if the interface does not currently exist, the
+            // IpConfiguration and NetworkCapabilities were saved and will be applied if an
+            // interface with the given name is ever added.
+            cb.onResult(iface);
         });
     }
 
     @VisibleForTesting(visibility = PACKAGE)
     protected void enableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+            @Nullable final EthernetCallback cb) {
+        mHandler.post(() -> updateInterfaceState(iface, true, cb));
     }
 
     @VisibleForTesting(visibility = PACKAGE)
     protected void disableInterface(@NonNull final String iface,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        mHandler.post(() -> updateInterfaceState(iface, false, listener));
+            @Nullable final EthernetCallback cb) {
+        mHandler.post(() -> updateInterfaceState(iface, false, cb));
     }
 
     IpConfiguration getIpConfiguration(String iface) {
@@ -571,8 +578,14 @@
         // Bring up the interface so we get link status indications.
         try {
             PermissionUtils.enforceNetworkStackPermission(mContext);
-            NetdUtils.setInterfaceUp(mNetd, iface);
+            // Read the flags before attempting to bring up the interface. If the interface is
+            // already running an UP event is created after adding the interface.
             config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
+            if (NetdUtils.hasFlag(config, INetd.IF_STATE_DOWN)) {
+                // As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
+                // address and readds it which *could* lead to unexpected behavior in the future.
+                NetdUtils.setInterfaceUp(mNetd, iface);
+            }
         } catch (IllegalStateException e) {
             // Either the system is crashing or the interface has disappeared. Just ignore the
             // error; we haven't modified any state because we only do that if our calls succeed.
@@ -608,23 +621,31 @@
         // Note: if the interface already has link (e.g., if we crashed and got
         // restarted while it was running), we need to fake a link up notification so we
         // start configuring it.
-        if (NetdUtils.hasFlag(config, "running")) {
+        if (NetdUtils.hasFlag(config, INetd.IF_FLAG_RUNNING)) {
             updateInterfaceState(iface, true);
         }
     }
 
     private void updateInterfaceState(String iface, boolean up) {
-        updateInterfaceState(iface, up, null /* listener */);
+        // TODO: pull EthernetCallbacks out of EthernetNetworkFactory.
+        updateInterfaceState(iface, up, new EthernetCallback(null /* cb */));
     }
 
     private void updateInterfaceState(@NonNull final String iface, final boolean up,
-            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            @Nullable final EthernetCallback cb) {
         final int mode = getInterfaceMode(iface);
         final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT)
-                && mFactory.updateInterfaceLinkState(iface, up, listener);
+                && mFactory.updateInterfaceLinkState(iface, up);
 
         if (factoryLinkStateUpdated) {
             broadcastInterfaceStateChange(iface);
+            cb.onResult(iface);
+        } else {
+            // The interface may already be in the correct state. While usually this should not be
+            // an error, since updateInterfaceState is used in setInterfaceEnabled() /
+            // setInterfaceDisabled() it has to be reported as such.
+            // It is also possible that the interface disappeared or is in server mode.
+            cb.onError("Failed to set link state " + (up ? "up" : "down") + " for " + iface);
         }
     }
 
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index c9d1718..8161f50 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -296,6 +296,16 @@
         return mTunAnd464xlatAdjustedStats.clone();
     }
 
+    /**
+     * Remove stats from {@code mPersistSnapshot} and {@code mTunAnd464xlatAdjustedStats} for the
+     * given uids.
+     */
+    public void removeUidsLocked(int[] uids) {
+        synchronized (mPersistentDataLock) {
+            mPersistSnapshot.removeUids(uids);
+            mTunAnd464xlatAdjustedStats.removeUids(uids);
+        }
+    }
 
     public void assertEquals(NetworkStats expected, NetworkStats actual) {
         if (expected.size() != actual.size()) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index c4ffdec..b08879e 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -2469,13 +2469,13 @@
         mUidRecorder.removeUidsLocked(uids);
         mUidTagRecorder.removeUidsLocked(uids);
 
+        mStatsFactory.removeUidsLocked(uids);
         // Clear kernel stats associated with UID
         for (int uid : uids) {
             deleteKernelTagData(uid);
         }
-
-       // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
-       // mOpenSessionCallsPerCaller
+        // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+        // mOpenSessionCallsPerCaller
     }
 
     /**
@@ -2819,24 +2819,10 @@
         if (mCookieTagMap == null) {
             return;
         }
-        pw.println("mCookieTagMap:");
-        pw.increaseIndent();
-        try {
-            mCookieTagMap.forEach((key, value) -> {
-                // value could be null if there is a concurrent entry deletion.
-                // http://b/220084230.
-                if (value != null) {
-                    pw.println("cookie=" + key.socketCookie
-                            + " tag=0x" + Long.toHexString(value.tag)
-                            + " uid=" + value.uid);
-                } else {
-                    pw.println("Entry is deleted while dumping, iterating from first entry");
-                }
-            });
-        } catch (ErrnoException e) {
-            pw.println("mCookieTagMap dump end with error: " + Os.strerror(e.errno));
-        }
-        pw.decreaseIndent();
+        BpfDump.dumpMap(mCookieTagMap, pw, "mCookieTagMap",
+                (key, value) -> "cookie=" + key.socketCookie
+                        + " tag=0x" + Long.toHexString(value.tag)
+                        + " uid=" + value.uid);
     }
 
     @GuardedBy("mStatsLock")
@@ -2844,22 +2830,8 @@
         if (mUidCounterSetMap == null) {
             return;
         }
-        pw.println("mUidCounterSetMap:");
-        pw.increaseIndent();
-        try {
-            mUidCounterSetMap.forEach((uid, set) -> {
-                // set could be null if there is a concurrent entry deletion.
-                // http://b/220084230.
-                if (set != null) {
-                    pw.println("uid=" + uid.val + " set=" + set.val);
-                } else {
-                    pw.println("Entry is deleted while dumping, iterating from first entry");
-                }
-            });
-        } catch (ErrnoException e) {
-            pw.println("mUidCounterSetMap dump end with error: " + Os.strerror(e.errno));
-        }
-        pw.decreaseIndent();
+        BpfDump.dumpMap(mUidCounterSetMap, pw, "mUidCounterSetMap",
+                (uid, set) -> "uid=" + uid.val + " set=" + set.val);
     }
 
     @GuardedBy("mStatsLock")
@@ -2867,27 +2839,13 @@
         if (mAppUidStatsMap == null) {
             return;
         }
-        pw.println("mAppUidStatsMap:");
-        pw.increaseIndent();
-        pw.println("uid rxBytes rxPackets txBytes txPackets");
-        try {
-            mAppUidStatsMap.forEach((key, value) -> {
-                // value could be null if there is a concurrent entry deletion.
-                // http://b/220084230.
-                if (value != null) {
-                    pw.println(key.uid + " "
-                            + value.rxBytes + " "
-                            + value.rxPackets + " "
-                            + value.txBytes + " "
-                            + value.txPackets);
-                } else {
-                    pw.println("Entry is deleted while dumping, iterating from first entry");
-                }
-            });
-        } catch (ErrnoException e) {
-            pw.println("mAppUidStatsMap dump end with error: " + Os.strerror(e.errno));
-        }
-        pw.decreaseIndent();
+        BpfDump.dumpMap(mAppUidStatsMap, pw, "mAppUidStatsMap",
+                "uid rxBytes rxPackets txBytes txPackets",
+                (key, value) -> key.uid + " "
+                        + value.rxBytes + " "
+                        + value.rxPackets + " "
+                        + value.txBytes + " "
+                        + value.txPackets);
     }
 
     private NetworkStats readNetworkStatsSummaryDev() {
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 7387483..dc5c4c7 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -40,21 +40,20 @@
 import android.provider.DeviceConfig;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.util.ArraySet;
 import android.util.Log;
 
 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.DeviceConfigUtils;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * BpfNetMaps is responsible for providing traffic controller relevant functionality.
@@ -101,10 +100,10 @@
     private static final long STATS_SELECT_MAP_A = 0;
     private static final long STATS_SELECT_MAP_B = 1;
 
-    private static BpfMap<U32, U32> sConfigurationMap = null;
+    private static IBpfMap<U32, U32> sConfigurationMap = null;
     // BpfMap for UID_OWNER_MAP_PATH. This map is not accessed by others.
-    private static BpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
-    private static BpfMap<U32, U8> sUidPermissionMap = null;
+    private static IBpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
+    private static IBpfMap<U32, U8> sUidPermissionMap = null;
 
     // LINT.IfChange(match_type)
     @VisibleForTesting public static final long NO_MATCH = 0;
@@ -134,7 +133,7 @@
      * Set configurationMap for test.
      */
     @VisibleForTesting
-    public static void setConfigurationMapForTest(BpfMap<U32, U32> configurationMap) {
+    public static void setConfigurationMapForTest(IBpfMap<U32, U32> configurationMap) {
         sConfigurationMap = configurationMap;
     }
 
@@ -142,7 +141,7 @@
      * Set uidOwnerMap for test.
      */
     @VisibleForTesting
-    public static void setUidOwnerMapForTest(BpfMap<U32, UidOwnerValue> uidOwnerMap) {
+    public static void setUidOwnerMapForTest(IBpfMap<U32, UidOwnerValue> uidOwnerMap) {
         sUidOwnerMap = uidOwnerMap;
     }
 
@@ -150,11 +149,11 @@
      * Set uidPermissionMap for test.
      */
     @VisibleForTesting
-    public static void setUidPermissionMapForTest(BpfMap<U32, U8> uidPermissionMap) {
+    public static void setUidPermissionMapForTest(IBpfMap<U32, U8> uidPermissionMap) {
         sUidPermissionMap = uidPermissionMap;
     }
 
-    private static BpfMap<U32, U32> getConfigurationMap() {
+    private static IBpfMap<U32, U32> getConfigurationMap() {
         try {
             return new BpfMap<>(
                     CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
@@ -163,7 +162,7 @@
         }
     }
 
-    private static BpfMap<U32, UidOwnerValue> getUidOwnerMap() {
+    private static IBpfMap<U32, UidOwnerValue> getUidOwnerMap() {
         try {
             return new BpfMap<>(
                     UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, UidOwnerValue.class);
@@ -172,7 +171,7 @@
         }
     }
 
-    private static BpfMap<U32, U8> getUidPermissionMap() {
+    private static IBpfMap<U32, U8> getUidPermissionMap() {
         try {
             return new BpfMap<>(
                     UID_PERMISSION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U8.class);
@@ -518,6 +517,14 @@
         }
     }
 
+    private Set<Integer> asSet(final int[] uids) {
+        final Set<Integer> uidSet = new ArraySet<>();
+        for (final int uid: uids) {
+            uidSet.add(uid);
+        }
+        return uidSet;
+    }
+
     /**
      * Replaces the contents of the specified UID-based firewall chain.
      * Enables the chain for specified uids and disables the chain for non-specified uids.
@@ -539,15 +546,17 @@
                 // ConnectivityManager#replaceFirewallChain API
                 throw new IllegalArgumentException("Invalid firewall chain: " + chain);
             }
-            final Set<Integer> uidSet = Arrays.stream(uids).boxed().collect(Collectors.toSet());
-            final Set<Integer> uidSetToRemoveRule = new HashSet<>();
+            final Set<Integer> uidSet = asSet(uids);
+            final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
             try {
                 synchronized (sUidOwnerMap) {
                     sUidOwnerMap.forEach((uid, config) -> {
                         // config could be null if there is a concurrent entry deletion.
-                        // http://b/220084230.
-                        if (config != null
-                                && !uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
+                        // http://b/220084230. But sUidOwnerMap update must be done while holding a
+                        // lock, so this should not happen.
+                        if (config == null) {
+                            Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
+                        } else if (!uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
                             uidSetToRemoveRule.add((int) uid.val);
                         }
                     });
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
old mode 100644
new mode 100755
index 218cbde..afb3ca2
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1744,7 +1744,8 @@
         synchronized (mNetworkForNetId) {
             for (int i = 0; i < mNetworkForNetId.size(); i++) {
                 final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i);
-                if (nai.isVPN() && nai.everConnected && nai.networkCapabilities.appliesToUid(uid)) {
+                if (nai.isVPN() && nai.everConnected()
+                        && nai.networkCapabilities.appliesToUid(uid)) {
                     return nai;
                 }
             }
@@ -2478,7 +2479,7 @@
         final ArrayList<NetworkStateSnapshot> result = new ArrayList<>();
         for (Network network : getAllNetworks()) {
             final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
-            if (nai != null && nai.everConnected) {
+            if (nai != null && nai.everConnected()) {
                 // TODO (b/73321673) : NetworkStateSnapshot contains a copy of the
                 // NetworkCapabilities, which may contain UIDs of apps to which the
                 // network applies. Should the UIDs be cleared so as not to leak or
@@ -3531,7 +3532,7 @@
             }
 
             // If the network has been destroyed, the only thing that it can do is disconnect.
-            if (nai.destroyed && !isDisconnectRequest(msg)) {
+            if (nai.isDestroyed() && !isDisconnectRequest(msg)) {
                 return;
             }
 
@@ -3560,7 +3561,7 @@
                     break;
                 }
                 case NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED: {
-                    if (nai.everConnected) {
+                    if (nai.everConnected()) {
                         loge("ERROR: cannot call explicitlySelected on already-connected network");
                         // Note that if the NAI had been connected, this would affect the
                         // score, and therefore would require re-mixing the score and performing
@@ -3690,7 +3691,7 @@
             final int netId = msg.arg2;
             final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
             // If a network has already been destroyed, all NetworkMonitor updates are ignored.
-            if (nai != null && nai.destroyed) return true;
+            if (nai != null && nai.isDestroyed()) return true;
             switch (msg.what) {
                 default:
                     return false;
@@ -3739,12 +3740,10 @@
                 case EVENT_PROVISIONING_NOTIFICATION: {
                     final boolean visible = toBool(msg.arg1);
                     // If captive portal status has changed, update capabilities or disconnect.
-                    if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
-                        nai.lastCaptivePortalDetected = visible;
-                        nai.everCaptivePortalDetected |= visible;
-                        if (nai.lastCaptivePortalDetected &&
-                                ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
-                                        == getCaptivePortalMode()) {
+                    if (nai != null && (visible != nai.captivePortalDetected())) {
+                        nai.setCaptivePortalDetected(visible);
+                        if (visible && ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
+                                == getCaptivePortalMode()) {
                             if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
                             nai.onPreventAutomaticReconnect();
                             teardownUnneededNetwork(nai);
@@ -3796,11 +3795,10 @@
                 return;
             }
 
-            final boolean wasValidated = nai.lastValidated;
-            final boolean wasPartial = nai.partialConnectivity;
-            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
-            final boolean partialConnectivityChanged =
-                    (wasPartial != nai.partialConnectivity);
+            final boolean wasValidated = nai.isValidated();
+            final boolean wasPartial = nai.partialConnectivity();
+            nai.setPartialConnectivity((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
+            final boolean partialConnectivityChanged = (wasPartial != nai.partialConnectivity());
 
             if (DBG) {
                 final String logMsg = !TextUtils.isEmpty(redirectUrl)
@@ -3808,10 +3806,9 @@
                         : "";
                 log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
             }
-            if (valid != nai.lastValidated) {
+            if (valid != nai.isValidated()) {
                 final FullScore oldScore = nai.getScore();
-                nai.lastValidated = valid;
-                nai.everValidated |= valid;
+                nai.setValidated(valid);
                 updateCapabilities(oldScore, nai, nai.networkCapabilities);
                 if (valid) {
                     handleFreshlyValidatedNetwork(nai);
@@ -3844,13 +3841,13 @@
             // EVENT_PROMPT_UNVALIDATED arrives, show the partial connectivity notification
             // immediately. Re-notify partial connectivity silently if no internet
             // notification already there.
-            if (!wasPartial && nai.partialConnectivity) {
+            if (!wasPartial && nai.partialConnectivity()) {
                 // Remove delayed message if there is a pending message.
                 mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
                 handlePromptUnvalidated(nai.network);
             }
 
-            if (wasValidated && !nai.lastValidated) {
+            if (wasValidated && !nai.isValidated()) {
                 handleNetworkUnvalidated(nai);
             }
         }
@@ -4197,7 +4194,7 @@
     }
 
     private static boolean shouldDestroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
-        return nai.created && !nai.destroyed;
+        return nai.isCreated() && !nai.isDestroyed();
     }
 
     private boolean shouldIgnoreValidationFailureAfterRoam(NetworkAgentInfo nai) {
@@ -4207,8 +4204,8 @@
                 R.integer.config_validationFailureAfterRoamIgnoreTimeMillis));
         if (blockTimeOut <= MAX_VALIDATION_FAILURE_BLOCKING_TIME_MS
                 && blockTimeOut >= 0) {
-            final long currentTimeMs  = SystemClock.elapsedRealtime();
-            long timeSinceLastRoam = currentTimeMs - nai.lastRoamTimestamp;
+            final long currentTimeMs = SystemClock.elapsedRealtime();
+            long timeSinceLastRoam = currentTimeMs - nai.lastRoamTime;
             if (timeSinceLastRoam <= blockTimeOut) {
                 log ("blocked because only " + timeSinceLastRoam + "ms after roam");
                 return true;
@@ -4312,7 +4309,7 @@
         }
 
         // Delayed teardown.
-        if (nai.created) {
+        if (nai.isCreated()) {
             try {
                 mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
             } catch (RemoteException e) {
@@ -4333,7 +4330,7 @@
             // for an unnecessarily long time.
             destroyNativeNetwork(nai);
         }
-        if (!nai.created && !SdkLevel.isAtLeastT()) {
+        if (!nai.isCreated() && !SdkLevel.isAtLeastT()) {
             // Backwards compatibility: send onNetworkDestroyed even if network was never created.
             // This can never run if the code above runs because shouldDestroyNativeNetwork is
             // false if the network was never created.
@@ -4394,11 +4391,11 @@
         mDnsManager.removeNetwork(nai.network);
 
         // clean up tc police filters on interface.
-        if (nai.everConnected && canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
+        if (nai.everConnected() && canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
             mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
         }
 
-        nai.destroyed = true;
+        nai.setDestroyed();
         nai.onNetworkDestroyed();
     }
 
@@ -4527,7 +4524,7 @@
     private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
         ensureRunningOnConnectivityServiceThread();
 
-        if (!nai.everConnected || nai.isVPN() || nai.isInactive()
+        if (!nai.everConnected() || nai.isVPN() || nai.isInactive()
                 || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) {
             return false;
         }
@@ -4859,7 +4856,7 @@
             return;
         }
 
-        if (nai.everValidated) {
+        if (nai.everValidated()) {
             // The network validated while the dialog box was up. Take no action.
             return;
         }
@@ -4904,7 +4901,7 @@
             return;
         }
 
-        if (nai.lastValidated) {
+        if (nai.isValidated()) {
             // The network validated while the dialog box was up. Take no action.
             return;
         }
@@ -4936,12 +4933,12 @@
 
     private void handleSetAvoidUnvalidated(Network network) {
         NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
-        if (nai == null || nai.lastValidated) {
+        if (nai == null || nai.isValidated()) {
             // Nothing to do. The network either disconnected or revalidated.
             return;
         }
-        if (!nai.avoidUnvalidated) {
-            nai.avoidUnvalidated = true;
+        if (0L == nai.getAvoidUnvalidated()) {
+            nai.setAvoidUnvalidated();
             nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
         }
@@ -5091,7 +5088,7 @@
         pw.println("Network overrides:");
         pw.increaseIndent();
         for (NetworkAgentInfo nai : networksSortedById()) {
-            if (nai.avoidUnvalidated) {
+            if (0L != nai.getAvoidUnvalidated()) {
                 pw.println(nai.toShortString());
             }
         }
@@ -5162,7 +5159,7 @@
     private boolean shouldPromptUnvalidated(NetworkAgentInfo nai) {
         // Don't prompt if the network is validated, and don't prompt on captive portals
         // because we're already prompting the user to sign in.
-        if (nai.everValidated || nai.everCaptivePortalDetected) {
+        if (nai.everValidated() || nai.everCaptivePortalDetected()) {
             return false;
         }
 
@@ -5170,8 +5167,8 @@
         // partial connectivity and selected don't ask again. This ensures that if the device
         // automatically connects to a network that has partial Internet access, the user will
         // always be able to use it, either because they've already chosen "don't ask again" or
-        // because we have prompt them.
-        if (nai.partialConnectivity && !nai.networkAgentConfig.acceptPartialConnectivity) {
+        // because we have prompted them.
+        if (nai.partialConnectivity() && !nai.networkAgentConfig.acceptPartialConnectivity) {
             return true;
         }
 
@@ -5203,7 +5200,7 @@
         // TODO: Evaluate if it's needed to wait 8 seconds for triggering notification when
         // NetworkMonitor detects the network is partial connectivity. Need to change the design to
         // popup the notification immediately when the network is partial connectivity.
-        if (nai.partialConnectivity) {
+        if (nai.partialConnectivity()) {
             showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
         } else {
             showNetworkNotification(nai, NotificationType.NO_INTERNET);
@@ -5557,7 +5554,7 @@
             return;
         }
         // Revalidate if the app report does not match our current validated state.
-        if (hasConnectivity == nai.lastValidated) {
+        if (hasConnectivity == nai.isValidated()) {
             mConnectivityDiagnosticsHandler.sendMessage(
                     mConnectivityDiagnosticsHandler.obtainMessage(
                             ConnectivityDiagnosticsHandler.EVENT_NETWORK_CONNECTIVITY_REPORTED,
@@ -5571,7 +5568,7 @@
         }
         // Validating a network that has not yet connected could result in a call to
         // rematchNetworkAndRequests() which is not meant to work on such networks.
-        if (!nai.everConnected) {
+        if (!nai.everConnected()) {
             return;
         }
         final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
@@ -6923,6 +6920,7 @@
 
     @Override
     public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
+        Objects.requireNonNull(callback);
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_OFFER, callback));
     }
 
@@ -7470,9 +7468,7 @@
             notifyIfacesChangedForNetworkStats();
             networkAgent.networkMonitor().notifyLinkPropertiesChanged(
                     new LinkProperties(newLp, true /* parcelSensitiveFields */));
-            if (networkAgent.everConnected) {
-                notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
-            }
+            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
         }
 
         mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
@@ -7751,7 +7747,7 @@
             @NonNull final NetworkCapabilities newNc) {
         final int oldPermission = getNetworkPermission(nai.networkCapabilities);
         final int newPermission = getNetworkPermission(newNc);
-        if (oldPermission != newPermission && nai.created && !nai.isVPN()) {
+        if (oldPermission != newPermission && nai.isCreated() && !nai.isVPN()) {
             try {
                 mNetd.networkSetPermissionForNetwork(nai.network.getNetId(), newPermission);
             } catch (RemoteException | ServiceSpecificException e) {
@@ -7841,9 +7837,9 @@
          // causing a connect/teardown loop.
          // TODO: remove this altogether and make it the responsibility of the NetworkProviders to
          // avoid connect/teardown loops.
-        if (nai.everConnected &&
-                !nai.isVPN() &&
-                !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) {
+        if (nai.everConnected()
+                && !nai.isVPN()
+                && !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) {
             // TODO: consider not complaining when a network agent degrades its capabilities if this
             // does not cause any request (that is not a listen) currently matching that agent to
             // stop being matched by the updated agent.
@@ -7855,12 +7851,12 @@
 
         // Don't modify caller's NetworkCapabilities.
         final NetworkCapabilities newNc = new NetworkCapabilities(nc);
-        if (nai.lastValidated) {
+        if (nai.isValidated()) {
             newNc.addCapability(NET_CAPABILITY_VALIDATED);
         } else {
             newNc.removeCapability(NET_CAPABILITY_VALIDATED);
         }
-        if (nai.lastCaptivePortalDetected) {
+        if (nai.captivePortalDetected()) {
             newNc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
         } else {
             newNc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
@@ -7870,7 +7866,7 @@
         } else {
             newNc.addCapability(NET_CAPABILITY_FOREGROUND);
         }
-        if (nai.partialConnectivity) {
+        if (nai.partialConnectivity()) {
             newNc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
         } else {
             newNc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
@@ -8117,7 +8113,7 @@
         //  that happens to prevent false alarms.
         final Set<UidRange> prevUids = prevNc == null ? null : prevNc.getUidRanges();
         final Set<UidRange> newUids = newNc == null ? null : newNc.getUidRanges();
-        if (nai.isVPN() && nai.everConnected && !UidRange.hasSameUids(prevUids, newUids)
+        if (nai.isVPN() && nai.everConnected() && !UidRange.hasSameUids(prevUids, newUids)
                 && (nai.linkProperties.getHttpProxy() != null || isProxySetOnAnyDefaultNetwork())) {
             mProxyTracker.sendProxyBroadcast();
         }
@@ -8237,8 +8233,8 @@
         }
         if (VDBG || DDBG) {
             log("Update of LinkProperties for " + nai.toShortString()
-                    + "; created=" + nai.created
-                    + "; everConnected=" + nai.everConnected);
+                    + "; created=" + nai.getCreatedTime()
+                    + "; firstConnected=" + nai.getConnectedTime());
         }
         // TODO: eliminate this defensive copy after confirming that updateLinkProperties does not
         // modify its oldLp parameter.
@@ -8670,7 +8666,7 @@
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
                 if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
-                        && !previousSatisfier.destroyed) {
+                        && !previousSatisfier.isDestroyed()) {
                     // If this network switch can't be supported gracefully, the request is not
                     // lingered. This allows letting go of the network sooner to reclaim some
                     // performance on the new network, since the radio can't do both at the same
@@ -8732,9 +8728,6 @@
         // Gather the list of all relevant agents.
         final ArrayList<NetworkAgentInfo> nais = new ArrayList<>();
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
-            if (!nai.everConnected) {
-                continue;
-            }
             nais.add(nai);
         }
 
@@ -8858,7 +8851,6 @@
         }
 
         for (final NetworkAgentInfo nai : nais) {
-            if (!nai.everConnected) continue;
             final boolean oldBackground = oldBgNetworks.contains(nai);
             // Process listen requests and update capabilities if the background state has
             // changed for this network. For consistency with previous behavior, send onLost
@@ -8942,7 +8934,7 @@
                 // The new default network can be newly null if and only if the old default
                 // network doesn't satisfy the default request any more because it lost a
                 // capability.
-                mDefaultInetConditionPublished = newDefaultNetwork.lastValidated ? 100 : 0;
+                mDefaultInetConditionPublished = newDefaultNetwork.isValidated() ? 100 : 0;
                 mLegacyTypeTracker.add(
                         newDefaultNetwork.networkInfo.getType(), newDefaultNetwork);
             }
@@ -8963,7 +8955,7 @@
         // they may get old info. Reverse this after the old startUsing api is removed.
         // This is on top of the multiple intent sequencing referenced in the todo above.
         for (NetworkAgentInfo nai : nais) {
-            if (nai.everConnected) {
+            if (nai.everConnected()) {
                 addNetworkToLegacyTypeTracker(nai);
             }
         }
@@ -9089,12 +9081,12 @@
 
     private void updateInetCondition(NetworkAgentInfo nai) {
         // Don't bother updating until we've graduated to validated at least once.
-        if (!nai.everValidated) return;
+        if (!nai.everValidated()) return;
         // For now only update icons for the default connection.
         // TODO: Update WiFi and cellular icons separately. b/17237507
         if (!isDefaultNetwork(nai)) return;
 
-        int newInetCondition = nai.lastValidated ? 100 : 0;
+        int newInetCondition = nai.isValidated() ? 100 : 0;
         // Don't repeat publish.
         if (newInetCondition == mDefaultInetConditionPublished) return;
 
@@ -9121,7 +9113,7 @@
             // SUSPENDED state is currently only overridden from CONNECTED state. In the case the
             // network agent is created, then goes to suspended, then goes out of suspended without
             // ever setting connected. Check if network agent is ever connected to update the state.
-            newInfo.setDetailedState(nai.everConnected
+            newInfo.setDetailedState(nai.everConnected()
                     ? NetworkInfo.DetailedState.CONNECTED
                     : NetworkInfo.DetailedState.CONNECTING,
                     info.getReason(),
@@ -9146,7 +9138,7 @@
                     + oldInfo.getState() + " to " + state);
         }
 
-        if (!networkAgent.created
+        if (!networkAgent.isCreated()
                 && (state == NetworkInfo.State.CONNECTED
                 || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
 
@@ -9160,13 +9152,13 @@
                 // anything happens to the network.
                 updateCapabilitiesForNetwork(networkAgent);
             }
-            networkAgent.created = true;
+            networkAgent.setCreated();
             networkAgent.onNetworkCreated();
             updateAllowedUids(networkAgent, null, networkAgent.networkCapabilities);
         }
 
-        if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
-            networkAgent.everConnected = true;
+        if (!networkAgent.everConnected() && state == NetworkInfo.State.CONNECTED) {
+            networkAgent.setConnected();
 
             // NetworkCapabilities need to be set before sending the private DNS config to
             // NetworkMonitor, otherwise NetworkMonitor cannot determine if validation is required.
@@ -9250,8 +9242,8 @@
                 // TODO(b/122649188): send the broadcast only to VPN users.
                 mProxyTracker.sendProxyBroadcast();
             }
-        } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
-                state == NetworkInfo.State.SUSPENDED)) {
+        } else if (networkAgent.isCreated() && (oldInfo.getState() == NetworkInfo.State.SUSPENDED
+                || state == NetworkInfo.State.SUSPENDED)) {
             mLegacyTypeTracker.update(networkAgent);
         }
     }
@@ -9451,7 +9443,7 @@
             }
         }
         for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-            if (nai.everConnected && (activeNetIds.contains(nai.network().netId) || nai.isVPN())) {
+            if (activeNetIds.contains(nai.network().netId) || nai.isVPN()) {
                 defaultNetworks.add(nai.network);
             }
         }
@@ -9679,7 +9671,7 @@
             return;
         }
         if (!TextUtils.equals(((WifiInfo)prevInfo).getBSSID(), ((WifiInfo)newInfo).getBSSID())) {
-            nai.lastRoamTimestamp = SystemClock.elapsedRealtime();
+            nai.lastRoamTime = SystemClock.elapsedRealtime();
         }
     }
 
diff --git a/service/src/com/android/server/connectivity/LingerMonitor.java b/service/src/com/android/server/connectivity/LingerMonitor.java
index 032612c..df34ce7 100644
--- a/service/src/com/android/server/connectivity/LingerMonitor.java
+++ b/service/src/com/android/server/connectivity/LingerMonitor.java
@@ -229,8 +229,8 @@
             @Nullable final NetworkAgentInfo toNai) {
         if (VDBG) {
             Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString()
-                    + " everValidated=" + fromNai.everValidated
-                    + " lastValidated=" + fromNai.lastValidated
+                    + " firstValidated=" + fromNai.getFirstValidationTime()
+                    + " lastValidated=" + fromNai.getCurrentValidationTime()
                     + " to=" + toNai.toShortString());
         }
 
@@ -253,7 +253,7 @@
         // 1. User connects to wireless printer.
         // 2. User turns on cellular data.
         // 3. We show a notification.
-        if (!fromNai.everValidated) return;
+        if (!fromNai.everValidated()) return;
 
         // If this network is a captive portal, don't notify. This cannot happen on initial connect
         // to a captive portal, because the everValidated check above will fail. However, it can
@@ -286,7 +286,7 @@
         // because its score changed.
         // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
         // unvalidated.
-        if (fromNai.lastValidated) return;
+        if (fromNai.isValidated()) return;
 
         if (!isNotificationEnabled(fromNai, toNai)) return;
 
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index e4ad391..a57e992 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -144,7 +144,7 @@
                 && nai.netAgentConfig().skip464xlat;
 
         return (supported || isTestNetwork) && connected && isIpv6OnlyNetwork && !skip464xlat
-                && !nai.destroyed && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+                && !nai.isDestroyed() && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
                 ? isCellular464XlatEnabled() : true);
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 04f378f..d4d7d96 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -189,42 +189,193 @@
     // field instead.
     private @Nullable NetworkCapabilities mDeclaredCapabilitiesUnsanitized;
 
-    // Indicates if netd has been told to create this Network. From this point on the appropriate
-    // routing rules are setup and routes are added so packets can begin flowing over the Network.
-    // This is a sticky bit; once set it is never cleared.
-    public boolean created;
-    // Set to true after the first time this network is marked as CONNECTED. Once set, the network
-    // shows up in API calls, is able to satisfy NetworkRequests and can become the default network.
-    // This is a sticky bit; once set it is never cleared.
-    public boolean everConnected;
-    // Whether this network has been destroyed and is being kept temporarily until it is replaced.
-    public boolean destroyed;
-    // To check how long it has been since last roam.
-    public long lastRoamTimestamp;
+    // Timestamp (SystemClock.elapsedRealtime()) when netd has been told to create this Network, or
+    // 0 if it hasn't been done yet.
+    // From this point on, the appropriate routing rules are setup and routes are added so packets
+    // can begin flowing over the Network.
+    // This is a sticky value; once set != 0 it is never changed.
+    private long mCreatedTime;
 
-    // Set to true if this Network successfully passed validation or if it did not satisfy the
-    // default NetworkRequest in which case validation will not be attempted.
-    // This is a sticky bit; once set it is never cleared even if future validation attempts fail.
-    public boolean everValidated;
+    /** Notify this NAI that netd was just told to create this network */
+    public void setCreated() {
+        if (0L != mCreatedTime) throw new IllegalStateException("Already created");
+        mCreatedTime = SystemClock.elapsedRealtime();
+    }
 
-    // The result of the last validation attempt on this network (true if validated, false if not).
-    public boolean lastValidated;
+    /** Returns whether netd was told to create this network */
+    public boolean isCreated() {
+        return mCreatedTime != 0L;
+    }
 
-    // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
-    // system is configured not to do this for certain networks, e.g., if the
-    // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
-    // Settings.Global.NETWORK_AVOID_BAD_WIFI.
-    public boolean avoidUnvalidated;
+    // Get the time (SystemClock.elapsedRealTime) when this network was created (or 0 if never).
+    public long getCreatedTime() {
+        return mCreatedTime;
+    }
 
-    // Whether a captive portal was ever detected on this network.
-    // This is a sticky bit; once set it is never cleared.
-    public boolean everCaptivePortalDetected;
+    // Timestamp of the first time (SystemClock.elapsedRealtime()) this network is marked as
+    // connected, or 0 if this network has never been marked connected. Once set to non-zero, the
+    // network shows up in API calls, is able to satisfy NetworkRequests and can become the default
+    // network.
+    // This is a sticky value; once set != 0 it is never changed.
+    private long mConnectedTime;
 
-    // Whether a captive portal was found during the last network validation attempt.
-    public boolean lastCaptivePortalDetected;
+    /** Notify this NAI that this network just connected */
+    public void setConnected() {
+        if (0L != mConnectedTime) throw new IllegalStateException("Already connected");
+        mConnectedTime = SystemClock.elapsedRealtime();
+    }
 
-    // Set to true when partial connectivity was detected.
-    public boolean partialConnectivity;
+    /** Return whether this network ever connected */
+    public boolean everConnected() {
+        return mConnectedTime != 0L;
+    }
+
+    // Get the time (SystemClock.elapsedRealTime()) when this network was first connected, or 0 if
+    // never.
+    public long getConnectedTime() {
+        return mConnectedTime;
+    }
+
+    // When this network has been destroyed and is being kept temporarily until it is replaced,
+    // this is set to that timestamp (SystemClock.elapsedRealtime()). Zero otherwise.
+    private long mDestroyedTime;
+
+    /** Notify this NAI that this network was destroyed */
+    public void setDestroyed() {
+        if (0L != mDestroyedTime) throw new IllegalStateException("Already destroyed");
+        mDestroyedTime = SystemClock.elapsedRealtime();
+    }
+
+    /** Return whether this network was destroyed */
+    public boolean isDestroyed() {
+        return 0L != mDestroyedTime;
+    }
+
+    // Timestamp of the last roaming (SystemClock.elapsedRealtime()) or 0 if never roamed.
+    public long lastRoamTime;
+
+    // Timestamp (SystemClock.elapsedRealtime()) of the first time this network successfully
+    // passed validation or was deemed exempt of validation (see
+    // {@link NetworkMonitorUtils#isValidationRequired}). Zero if the network requires
+    // validation but never passed it successfully.
+    // This is a sticky value; once set it is never changed even if further validation attempts are
+    // made (whether they succeed or fail).
+    private long mFirstValidationTime;
+
+    // Timestamp (SystemClock.elapsedRealtime()) at which the latest validation attempt succeeded,
+    // or 0 if the latest validation attempt failed.
+    private long mCurrentValidationTime;
+
+    /** Notify this NAI that this network just finished a validation check */
+    public void setValidated(final boolean validated) {
+        final long nowOrZero = validated ? SystemClock.elapsedRealtime() : 0L;
+        if (validated && 0L == mFirstValidationTime) {
+            mFirstValidationTime = nowOrZero;
+        }
+        mCurrentValidationTime = nowOrZero;
+    }
+
+    /**
+     * Returns whether this network is currently validated.
+     *
+     * This is the result of the latest validation check. {@see #getCurrentValidationTime} for
+     * when that check was performed.
+     */
+    public boolean isValidated() {
+        return 0L != mCurrentValidationTime;
+    }
+
+    /**
+     * Returns whether this network ever passed the validation checks successfully.
+     *
+     * Note that the network may no longer be validated at this time ever if this is true.
+     * @see #isValidated
+     */
+    public boolean everValidated() {
+        return 0L != mFirstValidationTime;
+    }
+
+    // Get the time (SystemClock.elapsedRealTime()) when this network was most recently validated,
+    // or 0 if this network was found not to validate on the last attempt.
+    public long getCurrentValidationTime() {
+        return mCurrentValidationTime;
+    }
+
+    // Get the time (SystemClock.elapsedRealTime()) when this network was validated for the first
+    // time (or 0 if never).
+    public long getFirstValidationTime() {
+        return mFirstValidationTime;
+    }
+
+    // Timestamp (SystemClock.elapsedRealtime()) at which the user requested this network be
+    // avoided when unvalidated. Zero if this never happened for this network.
+    // This is only meaningful if the system is configured to have some cell networks yield
+    // to bad wifi, e.g., if the config_networkAvoidBadWifi option is set to 0 and the user has
+    // not overridden that via Settings.Global.NETWORK_AVOID_BAD_WIFI.
+    //
+    // Normally the system always prefers a validated network to a non-validated one, even if
+    // the non-validated one is cheaper. However, some cell networks may be configured by the
+    // setting above to yield to WiFi even if that WiFi network goes bad. When this configuration
+    // is active, specific networks can be marked to override this configuration so that the
+    // system will revert to preferring such a cell to this network when this network goes bad. This
+    // is achieved by calling {@link ConnectivityManager#setAvoidUnvalidated()}, and this field
+    // is set to non-zero when this happened to this network.
+    private long mAvoidUnvalidated;
+
+    /** Set this network as being avoided when unvalidated. {@see mAvoidUnvalidated} */
+    public void setAvoidUnvalidated() {
+        if (0L != mAvoidUnvalidated) throw new IllegalStateException("Already avoided unvalidated");
+        mAvoidUnvalidated = SystemClock.elapsedRealtime();
+    }
+
+    // Get the time (SystemClock.elapsedRealTime()) when this network was set to being avoided
+    // when unvalidated, or 0 if this never happened.
+    public long getAvoidUnvalidated() {
+        return mAvoidUnvalidated;
+    }
+
+    // Timestamp (SystemClock.elapsedRealtime()) at which a captive portal was first detected
+    // on this network, or zero if this never happened.
+    // This is a sticky value; once set != 0 it is never changed.
+    private long mFirstCaptivePortalDetectedTime;
+
+    // Timestamp (SystemClock.elapsedRealtime()) at which the latest validation attempt found a
+    // captive portal, or zero if the latest attempt didn't find a captive portal.
+    private long mCurrentCaptivePortalDetectedTime;
+
+    /** Notify this NAI that a captive portal has just been detected on this network */
+    public void setCaptivePortalDetected(final boolean hasCaptivePortal) {
+        if (!hasCaptivePortal) {
+            mCurrentCaptivePortalDetectedTime = 0L;
+            return;
+        }
+        final long now = SystemClock.elapsedRealtime();
+        if (0L == mFirstCaptivePortalDetectedTime) mFirstCaptivePortalDetectedTime = now;
+        mCurrentCaptivePortalDetectedTime = now;
+    }
+
+    /** Return whether a captive portal has ever been detected on this network */
+    public boolean everCaptivePortalDetected() {
+        return 0L != mFirstCaptivePortalDetectedTime;
+    }
+
+    /** Return whether this network has been detected to be behind a captive portal at the moment */
+    public boolean captivePortalDetected() {
+        return 0L != mCurrentCaptivePortalDetectedTime;
+    }
+
+    // Timestamp (SystemClock.elapsedRealtime()) at which the latest validation attempt found
+    // partial connectivity, or zero if the latest attempt didn't find partial connectivity.
+    private long mPartialConnectivityTime;
+
+    public void setPartialConnectivity(final boolean value) {
+        mPartialConnectivityTime = value ? SystemClock.elapsedRealtime() : 0L;
+    }
+
+    /** Return whether this NAI has partial connectivity */
+    public boolean partialConnectivity() {
+        return 0L != mPartialConnectivityTime;
+    }
 
     // Delay between when the network is disconnected and when the native network is destroyed.
     public int teardownDelayMs;
@@ -820,7 +971,7 @@
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
-                yieldToBadWiFi(), destroyed);
+                yieldToBadWiFi(), isDestroyed());
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -983,13 +1134,13 @@
 
     // Does this network satisfy request?
     public boolean satisfies(NetworkRequest request) {
-        return created &&
-                request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities);
+        return everConnected()
+                && request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities);
     }
 
     public boolean satisfiesImmutableCapabilitiesOf(NetworkRequest request) {
-        return created &&
-                request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
+        return everConnected()
+                && request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
                         networkCapabilities);
     }
 
@@ -1023,7 +1174,7 @@
      */
     public void setScore(final NetworkScore score) {
         mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), destroyed);
+                everValidatedForYield(), yieldToBadWiFi(), isDestroyed());
     }
 
     /**
@@ -1033,11 +1184,11 @@
      */
     public void updateScoreForNetworkAgentUpdate() {
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), destroyed);
+                everValidatedForYield(), yieldToBadWiFi(), isDestroyed());
     }
 
     private boolean everValidatedForYield() {
-        return everValidated && !avoidUnvalidated;
+        return everValidated() && 0L == mAvoidUnvalidated;
     }
 
     /**
@@ -1324,14 +1475,17 @@
                 + networkInfo.toShortString() + "} "
                 + "created=" + Instant.ofEpochMilli(mCreationTime) + " "
                 + mScore + " "
-                + (created ? " created" : "")
-                + (destroyed ? " destroyed" : "")
+                + (isCreated() ? " created " + getCreatedTime() : "")
+                + (isDestroyed() ? " destroyed " + mDestroyedTime : "")
                 + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
-                + (everValidated ? " everValidated" : "")
-                + (lastValidated ? " lastValidated" : "")
-                + (partialConnectivity ? " partialConnectivity" : "")
-                + (everCaptivePortalDetected ? " everCaptivePortal" : "")
-                + (lastCaptivePortalDetected ? " isCaptivePortal" : "")
+                + (everValidated() ? " firstValidated " + getFirstValidationTime() : "")
+                + (isValidated() ? " lastValidated " + getCurrentValidationTime() : "")
+                + (partialConnectivity()
+                        ? " partialConnectivity " + mPartialConnectivityTime : "")
+                + (everCaptivePortalDetected()
+                        ? " firstCaptivePortalDetected " + mFirstCaptivePortalDetectedTime : "")
+                + (captivePortalDetected()
+                        ? " currentCaptivePortalDetected " + mCurrentCaptivePortalDetectedTime : "")
                 + (networkAgentConfig.explicitlySelected ? " explicitlySelected" : "")
                 + (networkAgentConfig.acceptUnvalidated ? " acceptUnvalidated" : "")
                 + (networkAgentConfig.acceptPartialConnectivity ? " acceptPartialConnectivity" : "")
@@ -1349,7 +1503,7 @@
      *
      * This is often not enough for debugging purposes for anything complex, but the full form
      * is very long and hard to read, so this is useful when there isn't a lot of ambiguity.
-     * This represents the network with something like "[100 WIFI|VPN]" or "[108 MOBILE]".
+     * This represents the network with something like "[100 WIFI|VPN]" or "[108 CELLULAR]".
      */
     public String toShortString() {
         return "[" + network.getNetId() + " "
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
index 9599d4e..3a36cee 100644
--- a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -27,6 +27,9 @@
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
 
 private val TAG = DeviceConfigRule::class.simpleName
 
@@ -110,17 +113,60 @@
      * 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)
+    fun setConfig(namespace: String, key: String, value: String?): String? {
+        Log.i(TAG, "Setting config \"$key\" to \"$value\"")
+        val readWritePermissions = arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+
+        val keyPair = Pair(namespace, key)
+        val existingValue = runAsShell(*readWritePermissions) {
+            DeviceConfig.getProperty(namespace, key)
+        }
+        if (!originalConfig.containsKey(keyPair)) {
+            originalConfig[keyPair] = existingValue
+        }
+        usedConfig[keyPair] = value
+        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, "\"$key\" 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(key)) return@OnPropertiesChangedListener
+            // "null" means absent in DeviceConfig : there is no such thing as a present but
+            // null value, so the following works even if |value| is null.
+            if (it.getString(key, null) == value) {
+                future.complete(value)
             }
-            usedConfig[keyPair] = value
-            DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+        }
+
+        return tryTest {
+            runAsShell(*readWritePermissions) {
+                DeviceConfig.addOnPropertiesChangedListener(
+                        DeviceConfig.NAMESPACE_CONNECTIVITY,
+                        inlineExecutor,
+                        listener)
+                DeviceConfig.setProperty(
+                        DeviceConfig.NAMESPACE_CONNECTIVITY,
+                        key,
+                        value,
+                        false /* makeDefault */)
+                // Don't drop the permission until the config is applied, just in case
+                future.get(NetworkValidationTestUtil.TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            }.also {
+                Log.i(TAG, "Config \"$key\" successfully set to \"$value\"")
+            }
+        } cleanup {
+            DeviceConfig.removeOnPropertiesChangedListener(listener)
         }
     }
 
+    private val inlineExecutor get() = Executor { r -> r.run() }
+
     /**
      * Add an action to be run after config cleanup when the current test case ends.
      */
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index ce8584f..b21c5b4 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -77,6 +77,7 @@
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.Inet6Address
@@ -95,12 +96,11 @@
 import kotlin.test.fail
 
 private const val TAG = "EthernetManagerTest"
-// 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 TIMEOUT_MS = 1000L
 // Timeout used to confirm no callbacks matching given criteria are received. Must be long enough to
 // process all callbacks including ip provisioning when using the updateConfiguration API.
-private const val NO_CALLBACK_TIMEOUT_MS = 500L
+// Note that increasing this timeout increases the test duration.
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
 
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
         IpConfiguration.ProxySettings.NONE, null, null)
@@ -151,7 +151,9 @@
                 context.getSystemService(TestNetworkManager::class.java)
             }
             tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
-                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+                // setting RS delay to 0 and disabling DAD speeds up tests.
+                tnm.createTapInterface(hasCarrier, false /* bringUp */,
+                        true /* disableIpv6ProvisioningDelay */)
             }
             val mtu = tapInterface.mtu
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
@@ -193,9 +195,35 @@
                 val state: Int,
                 val role: Int,
                 val configuration: IpConfiguration?
-            ) : CallbackEntry()
+            ) : CallbackEntry() {
+                override fun toString(): String {
+                    val stateString = when (state) {
+                        STATE_ABSENT -> "STATE_ABSENT"
+                        STATE_LINK_UP -> "STATE_LINK_UP"
+                        STATE_LINK_DOWN -> "STATE_LINK_DOWN"
+                        else -> state.toString()
+                    }
+                    val roleString = when (role) {
+                        ROLE_NONE -> "ROLE_NONE"
+                        ROLE_CLIENT -> "ROLE_CLIENT"
+                        ROLE_SERVER -> "ROLE_SERVER"
+                        else -> role.toString()
+                    }
+                    return ("InterfaceStateChanged(iface=$iface, state=$stateString, " +
+                            "role=$roleString, ipConfig=$configuration)")
+                }
+            }
 
-            data class EthernetStateChanged(val state: Int) : CallbackEntry()
+            data class EthernetStateChanged(val state: Int) : CallbackEntry() {
+                override fun toString(): String {
+                    val stateString = when (state) {
+                        ETHERNET_STATE_ENABLED -> "ETHERNET_STATE_ENABLED"
+                        ETHERNET_STATE_DISABLED -> "ETHERNET_STATE_DISABLED"
+                        else -> state.toString()
+                    }
+                    return "EthernetStateChanged(state=$stateString)"
+                }
+            }
         }
 
         override fun onInterfaceStateChanged(
@@ -236,11 +264,13 @@
         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)))
+            val event = createChangeEvent(iface.name, state, role)
+            assertNotNull(eventuallyExpect(event), "Never received expected $event")
         }
 
         fun eventuallyExpect(state: Int) {
-            assertNotNull(eventuallyExpect(EthernetStateChanged(state)))
+            val event = EthernetStateChanged(state)
+            assertNotNull(eventuallyExpect(event), "Never received expected $event")
         }
 
         fun assertNoCallback() {
@@ -306,14 +336,24 @@
         }
     }
 
+    private fun isEthernetSupported() = em != null
+
     @Before
     fun setUp() {
+        assumeTrue(isEthernetSupported())
         setIncludeTestInterfaces(true)
         addInterfaceStateListener(ifaceListener)
+        // Handler.post() events may get processed after native fd events, so it is possible that
+        // RTM_NEWLINK (from a subsequent createInterface() call) arrives before the interface state
+        // listener is registered. This affects the callbacks and breaks the tests.
+        // setEthernetEnabled() will always wait on a callback, so it is used as a barrier to ensure
+        // proper listener registration before proceeding.
+        setEthernetEnabled(true)
     }
 
     @After
     fun tearDown() {
+        if (!isEthernetSupported()) return
         // Reenable ethernet, so ABSENT callbacks are received.
         setEthernetEnabled(true)
 
@@ -496,10 +536,7 @@
         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()
-        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)
 
@@ -778,7 +815,7 @@
                 .Builder(ETH_REQUEST.networkCapabilities)
                 .addCapability(testCapability)
                 .build()
-        updateConfiguration(iface, STATIC_IP_CONFIGURATION, nc)
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION, nc).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
@@ -796,7 +833,7 @@
         val network = cb.expectAvailable()
         cb.assertNeverLost()
 
-        updateConfiguration(iface, STATIC_IP_CONFIGURATION)
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
@@ -807,6 +844,9 @@
                 STATIC_IP_CONFIGURATION.staticIpConfiguration.ipAddress!!)
     }
 
+    // TODO(b/240323229): This test is currently flaky due to a race between IpClient restarting and
+    // NetworkAgent tearing down the routes. This problem is exacerbated by disabling RS delay.
+    @Ignore
     @Test
     fun testUpdateConfiguration_forOnlyCapabilities() {
         val iface: EthernetTestInterface = createInterface()
@@ -819,7 +859,7 @@
                 .Builder(ETH_REQUEST.networkCapabilities)
                 .addCapability(testCapability)
                 .build()
-        updateConfiguration(iface, capabilities = nc)
+        updateConfiguration(iface, capabilities = nc).expectResult(iface.name)
 
         // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
         // will be expected first before available, as part of the restart.
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 3e9662d..6c39169 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -19,7 +19,10 @@
 import android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA
 import android.content.Context
 import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_TEST
 import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
@@ -31,6 +34,7 @@
 import android.net.NetworkStats.ROAMING_ALL
 import android.net.NetworkTemplate.MATCH_MOBILE
 import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_TEST
 import android.net.NetworkTemplate.MATCH_WIFI
 import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
 import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -97,6 +101,14 @@
                     (oemManaged and OEM_PAID) == OEM_PAID)
             setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE,
                     (oemManaged and OEM_PRIVATE) == OEM_PRIVATE)
+            if (type == TYPE_TEST) {
+                wifiKey?.let { TestNetworkSpecifier(it) }?.let {
+                    // Must have a single non-test transport specified to use setNetworkSpecifier.
+                    // Put an arbitrary transport type which is not used in this test.
+                    addTransportType(TRANSPORT_TEST)
+                    addTransportType(TRANSPORT_WIFI)
+                    setNetworkSpecifier(it) }
+            }
             setTransportInfo(mockWifiInfo)
         }
         return NetworkStateSnapshot(mock(Network::class.java), caps, lp, subscriberId, type)
@@ -233,6 +245,32 @@
     }
 
     @Test
+    fun testTestNetworkTemplateMatches() {
+        val templateTestKey1 = NetworkTemplate.Builder(MATCH_TEST)
+            .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+        val templateTestKey2 = NetworkTemplate.Builder(MATCH_TEST)
+            .setWifiNetworkKeys(setOf(TEST_WIFI_KEY2)).build()
+        val templateTestAll = NetworkTemplate.Builder(MATCH_TEST).build()
+
+        val stateWifiKey1 = buildNetworkState(TYPE_WIFI, null /* subscriberId */, TEST_WIFI_KEY1,
+            OEM_NONE, true /* metered */)
+        val stateTestKey1 = buildNetworkState(TYPE_TEST, null /* subscriberId */, TEST_WIFI_KEY1,
+            OEM_NONE, true /* metered */)
+        val identWifi1 = buildNetworkIdentity(mockContext, stateWifiKey1,
+            false /* defaultNetwork */, NetworkTemplate.NETWORK_TYPE_ALL)
+        val identTest1 = buildNetworkIdentity(mockContext, stateTestKey1,
+            false /* defaultNetwork */, NETWORK_TYPE_ALL)
+
+        // Verify that the template matches corresponding type and the subscriberId.
+        templateTestKey1.assertDoesNotMatch(identWifi1)
+        templateTestKey1.assertMatches(identTest1)
+        templateTestKey2.assertDoesNotMatch(identWifi1)
+        templateTestKey2.assertDoesNotMatch(identTest1)
+        templateTestAll.assertDoesNotMatch(identWifi1)
+        templateTestAll.assertMatches(identTest1)
+    }
+
+    @Test
     fun testCarrierMeteredMatches() {
         val templateCarrierImsi1Metered = buildTemplateCarrierMetered(TEST_IMSI1)
 
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index be286ec..15a2e56 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -58,7 +58,7 @@
 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.IBpfMap;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -113,10 +113,10 @@
     @Mock INetd mNetd;
     @Mock BpfNetMaps.Dependencies mDeps;
     @Mock Context mContext;
-    private final BpfMap<U32, U32> mConfigurationMap = new TestBpfMap<>(U32.class, U32.class);
-    private final BpfMap<U32, UidOwnerValue> mUidOwnerMap =
+    private final IBpfMap<U32, U32> mConfigurationMap = new TestBpfMap<>(U32.class, U32.class);
+    private final IBpfMap<U32, UidOwnerValue> mUidOwnerMap =
             new TestBpfMap<>(U32.class, UidOwnerValue.class);
-    private final BpfMap<U32, U8> mUidPermissionMap = new TestBpfMap<>(U32.class, U8.class);
+    private final IBpfMap<U32, U8> mUidPermissionMap = new TestBpfMap<>(U32.class, U8.class);
 
     @Before
     public void setUp() throws Exception {
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
old mode 100644
new mode 100755
index 01db1c3..e23e72d
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3021,6 +3021,43 @@
     }
 
     @Test
+    public void testNetworkDoesntMatchRequestsUntilConnected() throws Exception {
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.requestNetwork(wifiRequest, cb);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        // Updating the score triggers a rematch.
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().build());
+        cb.assertNoCallback();
+        mWiFiNetworkAgent.connect(false);
+        cb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        cb.assertNoCallback();
+        mCm.unregisterNetworkCallback(cb);
+    }
+
+    @Test
+    public void testNetworkNotVisibleUntilConnected() throws Exception {
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, cb);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final NetworkCapabilities nc = mWiFiNetworkAgent.getNetworkCapabilities();
+        nc.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        mWiFiNetworkAgent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
+        cb.assertNoCallback();
+        mWiFiNetworkAgent.connect(false);
+        cb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        final CallbackEntry found = CollectionUtils.findLast(cb.getHistory(),
+                it -> it instanceof CallbackEntry.CapabilitiesChanged);
+        assertTrue(((CallbackEntry.CapabilitiesChanged) found).getCaps()
+                .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        cb.assertNoCallback();
+        mCm.unregisterNetworkCallback(cb);
+    }
+
+    @Test
     public void testStateChangeNetworkCallbacks() throws Exception {
         final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
         final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
@@ -16415,6 +16452,26 @@
     }
 
     @Test
+    public void testOfferNetwork_ChecksArgumentsOutsideOfHandler() throws Exception {
+        final TestableNetworkOfferCallback callback = new TestableNetworkOfferCallback(
+                TIMEOUT_MS /* timeout */, TEST_CALLBACK_TIMEOUT_MS /* noCallbackTimeout */);
+        final NetworkProvider testProvider = new NetworkProvider(mServiceContext,
+                mCsHandlerThread.getLooper(), "Test provider");
+        final NetworkCapabilities caps = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build();
+
+        final NetworkScore score = new NetworkScore.Builder().build();
+        testProvider.registerNetworkOffer(score, caps, r -> r.run(), callback);
+        testProvider.unregisterNetworkOffer(callback);
+
+        assertThrows(NullPointerException.class,
+                () -> mService.offerNetwork(100, score, caps, null));
+        assertThrows(NullPointerException.class, () -> mService.unofferNetwork(null));
+    }
+
+    @Test
     public void testIgnoreValidationAfterRoamDisabled() throws Exception {
         assumeFalse(SdkLevel.isAtLeastT());
         // testIgnoreValidationAfterRoam off
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index ad8613f..719314a 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -47,6 +48,7 @@
 import android.net.metrics.ValidationProbeEvent;
 import android.os.Build;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.system.OsConstants;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
@@ -138,7 +140,7 @@
     private void logDefaultNetworkEvent(long timeMs, NetworkAgentInfo nai,
             NetworkAgentInfo oldNai) {
         final Network network = (nai != null) ? nai.network() : null;
-        final boolean validated = (nai != null) ? nai.lastValidated : false;
+        final boolean validated = (nai != null) ? nai.isValidated() : false;
         final LinkProperties lp = (nai != null) ? nai.linkProperties : null;
         final NetworkCapabilities nc = (nai != null) ? nai.networkCapabilities : null;
 
@@ -614,7 +616,10 @@
         when(nai.network()).thenReturn(new Network(netId));
         nai.linkProperties = new LinkProperties();
         nai.networkCapabilities = new NetworkCapabilities();
-        nai.lastValidated = true;
+        nai.setValidated(true);
+        doReturn(true).when(nai).isValidated();
+        doReturn(SystemClock.elapsedRealtime()).when(nai).getFirstValidationTime();
+        doReturn(SystemClock.elapsedRealtime()).when(nai).getCurrentValidationTime();
         for (int t : BitUtils.unpackBits(transports)) {
             nai.networkCapabilities.addTransportType(t);
         }
@@ -629,8 +634,6 @@
         return nai;
     }
 
-
-
     static void verifySerialization(String want, String output) {
         try {
             byte[] got = Base64.decode(output, Base64.DEFAULT);
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 58a7c89..01249a1 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -272,9 +272,8 @@
     public void testIgnoreNeverValidatedNetworks() {
         setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
         setNotificationSwitch(transition(WIFI, CELLULAR));
-        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo from = wifiNai(100, false /* setEverValidated */);
         NetworkAgentInfo to = cellNai(101);
-        from.everValidated = false;
 
         mMonitor.noteLingerDefaultNetwork(from, to);
         verifyNoNotifications();
@@ -286,7 +285,7 @@
         setNotificationSwitch(transition(WIFI, CELLULAR));
         NetworkAgentInfo from = wifiNai(100);
         NetworkAgentInfo to = cellNai(101);
-        from.lastValidated = true;
+        from.setValidated(true);
 
         mMonitor.noteLingerDefaultNetwork(from, to);
         verifyNoNotifications();
@@ -363,7 +362,8 @@
                 eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true));
     }
 
-    NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) {
+    NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName,
+            boolean setEverValidated) {
         NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, "");
         NetworkCapabilities caps = new NetworkCapabilities();
         caps.addCapability(0);
@@ -373,18 +373,32 @@
                 mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
                 mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
                 mQosCallbackTracker, new ConnectivityService.Dependencies());
-        nai.everValidated = true;
+        if (setEverValidated) {
+            // As tests in this class deal with testing lingering, most tests are interested
+            // in networks that can be lingered, and therefore must have validated in the past.
+            // Thus, pretend the network validated once, then became invalidated.
+            nai.setValidated(true);
+            nai.setValidated(false);
+        }
         return nai;
     }
 
     NetworkAgentInfo wifiNai(int netId) {
+        return wifiNai(netId, true /* setEverValidated */);
+    }
+
+    NetworkAgentInfo wifiNai(int netId, boolean setEverValidated) {
         return nai(netId, NetworkCapabilities.TRANSPORT_WIFI,
-                ConnectivityManager.TYPE_WIFI, WIFI);
+                ConnectivityManager.TYPE_WIFI, WIFI, setEverValidated);
     }
 
     NetworkAgentInfo cellNai(int netId) {
+        return cellNai(netId, true /* setEverValidated */);
+    }
+
+    NetworkAgentInfo cellNai(int netId, boolean setEverValidated) {
         return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR,
-                ConnectivityManager.TYPE_MOBILE, CELLULAR);
+                ConnectivityManager.TYPE_MOBILE, CELLULAR, setEverValidated);
     }
 
     public static class TestableLingerMonitor extends LingerMonitor {
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index f159859..1fda04e 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -970,31 +970,6 @@
                 AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
     }
 
-    private void setAppOpsPermission() {
-        doAnswer(invocation -> {
-            when(mAppOps.noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN,
-                    Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */))
-                    .thenReturn(AppOpsManager.MODE_ALLOWED);
-            return null;
-        }).when(mAppOps).setMode(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testProvisionVpnProfileNotPreconsented_withControlVpnPermission() throws Exception {
-        setAppOpsPermission();
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpnAndSetupUidChecks();
-
-        // ACTIVATE_PLATFORM_VPN will be granted if VPN app has CONTROL_VPN permission.
-        checkProvisionVpnProfile(vpn, true /* expectedResult */,
-                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-    }
-
     @Test
     public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 503d920..aad80d5 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -38,9 +37,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
-import android.net.EthernetNetworkManagementException;
 import android.net.EthernetNetworkSpecifier;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -55,7 +52,6 @@
 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;
 
@@ -74,9 +70,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
@@ -84,7 +77,6 @@
 public class EthernetNetworkFactoryTest {
     private static final int TIMEOUT_MS = 2_000;
     private static final String TEST_IFACE = "test123";
-    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
     private static final String IP_ADDR = "192.0.2.2/25";
     private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
     private static final String HW_ADDR = "01:02:03:04:05:06";
@@ -241,7 +233,7 @@
         final IpConfiguration ipConfig = createDefaultIpConfig();
         mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
                 createInterfaceCapsBuilder(transportType).build());
-        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true));
 
         ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
                 NetworkOfferCallback.class);
@@ -295,7 +287,7 @@
         // then calling onNetworkUnwanted.
         mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
                 createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
-        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true));
 
         clearInvocations(mIpClient);
         clearInvocations(mNetworkAgent);
@@ -305,81 +297,63 @@
     public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
         initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
         // verify that the IpClient gets shut down when interface state changes to down.
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(ret);
         verify(mIpClient).shutdown();
-        assertEquals(TEST_IFACE, listener.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listenerDown = new TestNetworkManagementListener();
-        final TestNetworkManagementListener listenerUp = new TestNetworkManagementListener();
 
-        final boolean retDown =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listenerDown);
+        final boolean retDown = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(retDown);
         verifyStop();
-        assertEquals(TEST_IFACE, listenerDown.expectOnResult());
 
-        final boolean retUp =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listenerUp);
+        final boolean retUp = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertTrue(retUp);
-        assertEquals(TEST_IFACE, listenerUp.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
         initEthernetNetworkFactory();
         createUnprovisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */);
 
         assertTrue(ret);
         // There should not be an active IPClient or NetworkAgent.
         verify(mDeps, never()).makeIpClient(any(), any(), any());
         verify(mDeps, never())
                 .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
-        assertEquals(TEST_IFACE, listener.expectOnResult());
     }
 
     @Test
     public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
         initEthernetNetworkFactory();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
         // if interface was never added, link state cannot be updated.
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
     public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+        final boolean ret = mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */);
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
@@ -571,127 +545,16 @@
         verify(mNetworkAgent).markConnected();
     }
 
-    private static final class TestNetworkManagementListener
-            implements INetworkInterfaceOutcomeReceiver {
-        private final CompletableFuture<String> mResult = new CompletableFuture<>();
-
-        @Override
-        public void onResult(@NonNull String iface) {
-            mResult.complete(iface);
-        }
-
-        @Override
-        public void onError(@NonNull EthernetNetworkManagementException exception) {
-            mResult.completeExceptionally(exception);
-        }
-
-        String expectOnResult() throws Exception {
-            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        }
-
-        void expectOnError() throws Exception {
-            assertThrows(EthernetNetworkManagementException.class, () -> {
-                try {
-                    mResult.get();
-                } catch (ExecutionException e) {
-                    throw e.getCause();
-                }
-            });
-        }
-
-        @Override
-        public IBinder asBinder() {
-            return null;
-        }
-    }
-
-    @Test
-    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
-        initEthernetNetworkFactory();
-        createAndVerifyProvisionedInterface(TEST_IFACE);
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
-
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
-        triggerOnProvisioningSuccess();
-
-        assertEquals(TEST_IFACE, listener.expectOnResult());
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetFactory.removeInterface(TEST_IFACE));
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
-    }
-
-    @Test
-    public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
-        initEthernetNetworkFactory();
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
-    }
-
-    @Test
-    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
-        initEthernetNetworkFactory();
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener successfulListener =
-                new TestNetworkManagementListener();
-
-        // If two calls come in before the first one completes, the first listener will be aborted
-        // and the second one will be successful.
-        verifyNetworkManagementCallIsAbortedWhenInterrupted(
-                TEST_IFACE,
-                () -> {
-                    mNetFactory.updateInterface(
-                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
-                    triggerOnProvisioningSuccess();
-                });
-
-        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
-        assertEquals(TEST_IFACE, successfulListener.expectOnResult());
-    }
-
-    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
-            @NonNull final String iface,
-            @NonNull final Runnable interruptingRunnable) throws Exception {
-        createAndVerifyProvisionedInterface(iface);
-        final NetworkCapabilities capabilities = createDefaultFilterCaps();
-        final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
-
-        // An active update request will be aborted on interrupt prior to provisioning completion.
-        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
-        interruptingRunnable.run();
-
-        failedListener.expectOnError();
-    }
-
     @Test
     public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities);
         triggerOnProvisioningSuccess();
 
-        assertEquals(TEST_IFACE, listener.expectOnResult());
         verify(mDeps).makeEthernetNetworkAgent(any(), any(),
                 eq(capabilities), any(), any(), any(), any());
         verifyRestart(ipConfiguration);
@@ -703,12 +566,10 @@
         // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
         final IpConfiguration ipConfiguration = createStaticIpConfig();
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
 
-        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities);
 
         verifyNoStopOrStart();
-        listener.expectOnError();
     }
 
     @Test
@@ -717,8 +578,8 @@
         createAndVerifyProvisionedInterface(TEST_IFACE);
 
         final IpConfiguration initialIpConfig = createStaticIpConfig();
-        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
-                null /*listener*/);
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/);
+
         triggerOnProvisioningSuccess();
         verifyRestart(initialIpConfig);
 
@@ -729,8 +590,7 @@
 
 
         // verify that sending a null ipConfig does not update the current ipConfig.
-        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
-                null /*listener*/);
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/);
         triggerOnProvisioningSuccess();
         verifyRestart(initialIpConfig);
     }
@@ -739,7 +599,7 @@
     public void testOnNetworkNeededOnStaleNetworkOffer() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, null);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false);
         verify(mNetworkProvider).unregisterNetworkOffer(mNetworkOfferCallback);
         // It is possible that even after a network offer is unregistered, CS still sends it
         // onNetworkNeeded() callbacks.
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index a1d93a0..9bf893a 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -21,6 +21,7 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -209,7 +210,8 @@
                 NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
-                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     private void denyManageEthPermission() {
@@ -285,7 +287,8 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 eq(UPDATE_REQUEST.getIpConfiguration()),
-                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+                eq(UPDATE_REQUEST.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -303,19 +306,20 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 isNull(),
-                eq(ncWithSpecifier), eq(NULL_LISTENER));
+                eq(ncWithSpecifier), any(EthernetCallback.class));
     }
 
     @Test
     public void testEnableInterface() {
         mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     @Test
     public void testDisableInterface() {
         mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), any(EthernetCallback.class));
     }
 
     @Test
@@ -328,7 +332,7 @@
         mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(request.getIpConfiguration()),
-                eq(request.getNetworkCapabilities()), isNull());
+                eq(request.getNetworkCapabilities()), any(EthernetCallback.class));
     }
 
     @Test
@@ -337,7 +341,8 @@
                 NULL_LISTENER);
         verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
                 eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
-                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -369,7 +374,7 @@
         verify(mEthernetTracker).updateConfiguration(
                 eq(TEST_IFACE),
                 eq(request.getIpConfiguration()),
-                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+                eq(request.getNetworkCapabilities()), any(EthernetCallback.class));
     }
 
     @Test
@@ -379,7 +384,8 @@
         denyManageEthPermission();
 
         mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     @Test
@@ -389,7 +395,8 @@
         denyManageEthPermission();
 
         mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE),
+                any(EthernetCallback.class));
     }
 
     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 0376a2a..ea3d392 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -39,7 +39,6 @@
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
@@ -76,7 +75,7 @@
     private static final String TEST_IFACE = "test123";
     private static final int TIMEOUT_MS = 1_000;
     private static final String THREAD_NAME = "EthernetServiceThread";
-    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final EthernetCallback NULL_CB = new EthernetCallback(null);
     private EthernetTracker tracker;
     private HandlerThread mHandlerThread;
     @Mock private Context mContext;
@@ -88,8 +87,8 @@
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         initMockResources();
-        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
-        when(mNetd.interfaceGetList()).thenReturn(new String[0]);
+        doReturn(false).when(mFactory).updateInterfaceLinkState(anyString(), anyBoolean());
+        doReturn(new String[0]).when(mNetd).interfaceGetList();
         mHandlerThread = new HandlerThread(THREAD_NAME);
         mHandlerThread.start();
         tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
@@ -102,8 +101,8 @@
     }
 
     private void initMockResources() {
-        when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn("");
-        when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]);
+        doReturn("").when(mDeps).getInterfaceRegexFromResource(eq(mContext));
+        doReturn(new String[0]).when(mDeps).getInterfaceConfigFromResource(eq(mContext));
     }
 
     private void waitForIdle() {
@@ -344,31 +343,29 @@
                 new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
         final IpConfiguration ipConfig =
                 new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
-        final INetworkInterfaceOutcomeReceiver listener = null;
+        final EthernetCallback listener = new EthernetCallback(null);
 
         tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
         waitForIdle();
 
         verify(mFactory).updateInterface(
-                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities));
     }
 
     @Test
     public void testEnableInterfaceCorrectlyCallsFactory() {
-        tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
+        tracker.enableInterface(TEST_IFACE, NULL_CB);
         waitForIdle();
 
-        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
-                eq(NULL_LISTENER));
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */));
     }
 
     @Test
     public void testDisableInterfaceCorrectlyCallsFactory() {
-        tracker.disableInterface(TEST_IFACE, NULL_LISTENER);
+        tracker.disableInterface(TEST_IFACE, NULL_CB);
         waitForIdle();
 
-        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
-                eq(NULL_LISTENER));
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */));
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 14455fa..04db6d3 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -25,6 +25,7 @@
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
 import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 
@@ -89,6 +90,7 @@
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
         doReturn(mBpfNetMaps).when(mDeps).createBpfNetMaps(any());
+
         mFactory = new NetworkStatsFactory(mContext, mDeps);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
     }
@@ -462,6 +464,46 @@
         assertNoStatsEntry(stats, "wlan0", 1029, SET_DEFAULT, 0x0);
     }
 
+    @Test
+    public void testRemoveUidsStats() throws Exception {
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
+                        256L, 16L, 512L, 32L, 0L)
+                .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 64L, 3L, 1024L, 8L, 0L);
+
+        doReturn(stats).when(mDeps).getNetworkStatsDetail(anyInt(), any(),
+                anyInt());
+
+        final String[] ifaces = new String[]{TEST_IFACE};
+        final NetworkStats res = mFactory.readNetworkStatsDetail(UID_ALL, ifaces, TAG_ALL);
+
+        // Verify that the result of the mocked stats are expected.
+        assertValues(res, TEST_IFACE, UID_RED, 16L, 1L, 16L, 1L);
+        assertValues(res, TEST_IFACE, UID_BLUE, 256L, 16L, 512L, 32L);
+        assertValues(res, TEST_IFACE, UID_GREEN, 64L, 3L, 1024L, 8L);
+
+        // Assume the apps were removed.
+        final int[] removedUids = new int[]{UID_RED, UID_BLUE};
+        mFactory.removeUidsLocked(removedUids);
+
+        // Return empty stats for reading the result of removing uids stats later.
+        doReturn(buildEmptyStats()).when(mDeps).getNetworkStatsDetail(anyInt(), any(),
+                anyInt());
+
+        final NetworkStats removedUidsStats =
+                mFactory.readNetworkStatsDetail(UID_ALL, ifaces, TAG_ALL);
+
+        // Verify that the stats of the removed uids were removed.
+        assertValues(removedUidsStats, TEST_IFACE, UID_RED, 0L, 0L, 0L, 0L);
+        assertValues(removedUidsStats, TEST_IFACE, UID_BLUE, 0L, 0L, 0L, 0L);
+        assertValues(removedUidsStats, TEST_IFACE, UID_GREEN, 64L, 3L, 1024L, 8L);
+    }
+
+    private NetworkStats buildEmptyStats() {
+        return new NetworkStats(SystemClock.elapsedRealtime(), 0);
+    }
+
     private NetworkStats parseNetworkStatsFromGoldenSample(int resourceId, int initialSize,
             boolean consumeHeader, boolean checkActive, boolean isUidData) throws IOException {
         final NetworkStats stats =
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 153f121..2febd2f 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -25,6 +25,7 @@
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_TEST;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkIdentity.OEM_PAID;
 import static android.net.NetworkIdentity.OEM_PRIVATE;
@@ -48,6 +49,7 @@
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStatsHistory.FIELD_ALL;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_TEST;
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.NetworkTemplate.OEM_MANAGED_NO;
 import static android.net.NetworkTemplate.OEM_MANAGED_YES;
@@ -84,6 +86,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -107,6 +110,7 @@
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.TestNetworkSpecifier;
 import android.net.TetherStatsParcel;
 import android.net.TetheringManager;
 import android.net.UnderlyingNetworkInfo;
@@ -121,6 +125,7 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Pair;
 
@@ -172,6 +177,7 @@
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -209,9 +215,11 @@
     private static final Network WIFI_NETWORK =  new Network(100);
     private static final Network MOBILE_NETWORK =  new Network(101);
     private static final Network VPN_NETWORK = new Network(102);
+    private static final Network TEST_NETWORK = new Network(103);
 
     private static final Network[] NETWORKS_WIFI = new Network[]{ WIFI_NETWORK };
     private static final Network[] NETWORKS_MOBILE = new Network[]{ MOBILE_NETWORK };
+    private static final Network[] NETWORKS_TEST = new Network[]{ TEST_NETWORK };
 
     private static final long WAIT_TIMEOUT = 2 * 1000;  // 2 secs
     private static final int INVALID_TYPE = -1;
@@ -355,9 +363,9 @@
 
         mElapsedRealtime = 0L;
 
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
         mService.systemReady();
         // Verify that system ready fetches realtime stats
         verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
@@ -514,10 +522,10 @@
     private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {snapshot};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -526,10 +534,10 @@
     private void incrementWifiStats(long durationMillis, String iface,
             long rxb, long rxp, long txb, long txp) throws Exception {
         incrementCurrentTime(durationMillis);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(iface, rxb, rxp, txb, txp));
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
     }
 
@@ -595,10 +603,10 @@
 
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -609,10 +617,10 @@
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+        mockNetworkStatsUidDetail(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)
@@ -640,14 +648,14 @@
 
         // graceful shutdown system, which should trigger persist of stats, and
         // clear any values in memory.
-        expectDefaultSettings();
+        mockDefaultSettings();
         mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
 
         mService.systemReady();
 
@@ -672,20 +680,20 @@
 
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(2 * HOUR_IN_MILLIS);
-        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 512L, 4L, 512L, 4L));
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -697,9 +705,9 @@
 
         // now change bucket duration setting and trigger another poll with
         // exact same values, which should resize existing buckets.
-        expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockSettings(30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
 
         // verify identical stats, but spread across 4 buckets now
@@ -713,20 +721,20 @@
     @Test
     public void testUidStatsAcrossNetworks() throws Exception {
         // pretend first mobile network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobileState(IMSI_1)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some traffic on first network
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
@@ -744,11 +752,11 @@
         // now switch networks; this also tests that we're okay with interfaces
         // disappearing, to verify we don't count backwards.
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
+        mockDefaultSettings();
         states = new NetworkStateSnapshot[] {buildMobileState(IMSI_2)};
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
@@ -760,10 +768,10 @@
 
         // create traffic on second network
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 2176L, 17L, 1536L, 12L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L)
@@ -788,20 +796,20 @@
     @Test
     public void testUidRemovedIsMoved() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
@@ -817,13 +825,12 @@
         assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0);
         assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
 
-
         // now pretend two UIDs are uninstalled, which should migrate stats to
         // special "removed" bucket.
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
@@ -860,8 +867,8 @@
                 new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
 
         // 3G network comes online.
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
@@ -869,7 +876,7 @@
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
         forcePollAndWaitForIdle();
@@ -883,7 +890,7 @@
         // 4G network comes online.
         incrementCurrentTime(MINUTE_IN_MILLIS);
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 // Append more traffic on existing 3g stats entry.
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                          METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
@@ -903,7 +910,7 @@
         // 5g network comes online.
         incrementCurrentTime(MINUTE_IN_MILLIS);
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 // Existing stats remains.
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
@@ -934,14 +941,16 @@
                 .setRatType(TelephonyManager.NETWORK_TYPE_NR)
                 .setMeteredness(METERED_NO).build();
 
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         // Pretend that 5g mobile network comes online
         final NetworkStateSnapshot[] mobileStates =
-                new NetworkStateSnapshot[] {buildMobileState(IMSI_1), buildMobileState(TEST_IFACE2,
-                IMSI_1, true /* isTemporarilyNotMetered */, false /* isRoaming */)};
+                new NetworkStateSnapshot[] {buildMobileState(IMSI_1), buildStateOfTransport(
+                        NetworkCapabilities.TRANSPORT_CELLULAR, TYPE_MOBILE,
+                        TEST_IFACE2, IMSI_1, null /* wifiNetworkKey */,
+                        true /* isTemporarilyNotMetered */, false /* isRoaming */)};
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR);
         mService.notifyNetworkStatus(NETWORKS_MOBILE, mobileStates,
                 getActiveIface(mobileStates), new UnderlyingNetworkInfo[0]);
@@ -951,7 +960,7 @@
         // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer.
         // They are layered on top by inspecting the iface properties.
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
                         DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L)
                 .insertEntry(TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
@@ -984,14 +993,14 @@
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
                 buildOemManagedMobileState(IMSI_1, false,
                 new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 36L, 41L, 24L, 96L, 0L)));
         forcePollAndWaitForIdle();
@@ -999,14 +1008,14 @@
         // OEM_PRIVATE network comes online.
         states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false,
                 new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE})};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 49L, 71L, 72L, 48L, 0L)));
         forcePollAndWaitForIdle();
@@ -1015,28 +1024,28 @@
         states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false,
                 new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE,
                           NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 57L, 86L, 83L, 93L, 0L)));
         forcePollAndWaitForIdle();
 
         // OEM_NONE network comes online.
         states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, new int[]{})};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 29L, 73L, 34L, 31L, 0L)));
         forcePollAndWaitForIdle();
@@ -1079,19 +1088,19 @@
     @Test
     public void testSummaryForAllUid() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some traffic for two apps
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L));
@@ -1106,9 +1115,9 @@
 
         // now create more traffic in next hour, but only for one app
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
@@ -1138,10 +1147,10 @@
     @Test
     public void testGetLatestSummary() throws Exception {
         // Pretend that network comes online.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -1151,8 +1160,8 @@
         NetworkStats.Entry entry = new NetworkStats.Entry(
                 TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                 DEFAULT_NETWORK_NO, 50L, 5L, 51L, 1L, 3L);
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1).insertEntry(entry));
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1).insertEntry(entry));
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
 
         // Verify the mocked stats is returned by querying with the range of the latest bucket.
@@ -1173,12 +1182,47 @@
     }
 
     @Test
+    public void testQueryTestNetworkUsage() throws Exception {
+        final NetworkTemplate templateTestAll = new NetworkTemplate.Builder(MATCH_TEST).build();
+        final NetworkTemplate templateTestIface1 = new NetworkTemplate.Builder(MATCH_TEST)
+                .setWifiNetworkKeys(Set.of(TEST_IFACE)).build();
+        final NetworkTemplate templateTestIface2 = new NetworkTemplate.Builder(MATCH_TEST)
+                .setWifiNetworkKeys(Set.of(TEST_IFACE2)).build();
+        // Test networks might use interface as subscriberId to identify individual networks.
+        // Simulate both cases.
+        final NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildTestState(TEST_IFACE, TEST_IFACE),
+                        buildTestState(TEST_IFACE2, null /* wifiNetworkKey */)};
+
+        // Test networks comes online.
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_TEST, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic on both interfaces.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L))
+                .addEntry(new NetworkStats.Entry(TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 7L, 3L, 5L, 1L, 1L)));
+        forcePollAndWaitForIdle();
+
+        // Verify test network templates gets stats. Stats of test networks without subscriberId
+        // can only be matched by templates without subscriberId requirement.
+        assertUidTotal(templateTestAll, UID_RED, 19L, 21L, 19L, 2L, 1);
+        assertUidTotal(templateTestIface1, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(templateTestIface2, UID_RED, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
     public void testUidStatsForTransport() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -1194,9 +1238,9 @@
                 DEFAULT_NETWORK_NO, 1024L, 8L, 512L, 4L, 0L);
 
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
                 .insertEntry(entry1)
                 .insertEntry(entry2)
                 .insertEntry(entry3));
@@ -1218,19 +1262,19 @@
     @Test
     public void testForegroundBackground() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some initial traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L));
         mService.incrementOperationCount(UID_RED, 0xF00D, 1);
@@ -1243,9 +1287,9 @@
 
         // now switch to foreground
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
@@ -1277,23 +1321,23 @@
     @Test
     public void testMetered() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states =
                 new NetworkStateSnapshot[] {buildWifiState(true /* isMetered */, TEST_IFACE)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some initial traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
         // Note that all traffic from NetworkManagementService is tagged as METERED_NO, ROAMING_NO
         // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer.
         // We layer them on top by inspecting the iface properties.
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
@@ -1317,24 +1361,26 @@
     @Test
     public void testRoaming() throws Exception {
         // pretend that network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states =
-            new NetworkStateSnapshot[] {buildMobileState(TEST_IFACE, IMSI_1,
-            false /* isTemporarilyNotMetered */, true /* isRoaming */)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+            new NetworkStateSnapshot[] {buildStateOfTransport(
+                    NetworkCapabilities.TRANSPORT_CELLULAR, TYPE_MOBILE,
+                    TEST_IFACE,  IMSI_1, null /* wifiNetworkKey */,
+                    false /* isTemporarilyNotMetered */, true /* isRoaming */)};
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
         // Note that all traffic from NetworkManagementService is tagged as METERED_NO and
         // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it
         // on top by inspecting the iface properties.
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO,
                         DEFAULT_NETWORK_YES,  128L, 2L, 128L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_NO,
@@ -1357,18 +1403,18 @@
     @Test
     public void testTethering() throws Exception {
         // pretend first mobile network comes online
-        expectDefaultSettings();
+        mockDefaultSettings();
         final NetworkStateSnapshot[] states =
                 new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // create some tethering traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
+        mockDefaultSettings();
 
         // Register custom provider and retrieve callback.
         final TestableNetworkStatsProviderBinder provider =
@@ -1400,8 +1446,8 @@
         final TetherStatsParcel[] tetherStatsParcels =
                 {buildTetherStatsParcel(TEST_IFACE, 1408L, 10L, 256L, 1L, 0)};
 
-        expectNetworkStatsSummary(swIfaceStats);
-        expectNetworkStatsUidDetail(localUidStats, tetherStatsParcels);
+        mockNetworkStatsSummary(swIfaceStats);
+        mockNetworkStatsUidDetail(localUidStats, tetherStatsParcels);
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -1414,10 +1460,10 @@
     public void testRegisterUsageCallback() throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
@@ -1429,9 +1475,9 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes);
 
         // Force poll
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockDefaultSettings();
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         // Register and verify request and that binder was called
         DataUsageRequest request = mService.registerUsageCallback(
@@ -1449,10 +1495,10 @@
         // modify some number on wifi, and trigger poll event
         // not enough traffic to call data usage callback
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L));
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -1464,10 +1510,10 @@
         // and bump forward again, with counters going higher. this is
         // important, since it will trigger the data usage callback
         incrementCurrentTime(DAY_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockDefaultSettings();
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 4096000L, 4L, 8192000L, 8L));
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -1504,11 +1550,11 @@
     @Test
     public void testStatsProviderUpdateStats() throws Exception {
         // Pretend that network comes online.
-        expectDefaultSettings();
+        mockDefaultSettings();
         final NetworkStateSnapshot[] states =
                 new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         // Register custom provider and retrieve callback.
         final TestableNetworkStatsProviderBinder provider =
@@ -1537,7 +1583,7 @@
         // Make another empty mutable stats object. This is necessary since the new NetworkStats
         // object will be used to compare with the old one in NetworkStatsRecoder, two of them
         // cannot be the same object.
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         forcePollAndWaitForIdle();
 
@@ -1566,14 +1612,14 @@
     @Test
     public void testDualVilteProviderStats() throws Exception {
         // Pretend that network comes online.
-        expectDefaultSettings();
+        mockDefaultSettings();
         final int subId1 = 1;
         final int subId2 = 2;
         final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
                 buildImsState(IMSI_1, subId1, TEST_IFACE),
                 buildImsState(IMSI_2, subId2, TEST_IFACE2)};
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         // Register custom provider and retrieve callback.
         final TestableNetworkStatsProviderBinder provider =
@@ -1604,7 +1650,7 @@
         // Make another empty mutable stats object. This is necessary since the new NetworkStats
         // object will be used to compare with the old one in NetworkStatsRecoder, two of them
         // cannot be the same object.
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         forcePollAndWaitForIdle();
 
@@ -1637,7 +1683,7 @@
     @Test
     public void testStatsProviderSetAlert() throws Exception {
         // Pretend that network comes online.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states =
                 new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)};
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
@@ -1687,8 +1733,8 @@
         final NetworkStateSnapshot[] states =
                 new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
 
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
 
         // 3G network comes online.
         setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
@@ -1697,7 +1743,7 @@
 
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
         forcePollAndWaitForIdle();
@@ -1721,7 +1767,7 @@
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
         // Append more traffic on existing snapshot.
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L + 4L, 18L + 4L, 14L + 3L,
                         1L + 1L, 0L))
@@ -1744,7 +1790,7 @@
         // Create some traffic.
         incrementCurrentTime(MINUTE_IN_MILLIS);
         // Append more traffic on existing snapshot.
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 22L, 26L, 19L, 5L, 0L))
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
@@ -1761,16 +1807,16 @@
     @Test
     public void testOperationCount_nonDefault_traffic() throws Exception {
         // Pretend mobile network comes online, but wifi is the default network.
-        expectDefaultSettings();
+        mockDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
                 buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobileState(IMSI_1)};
-        expectNetworkStatsUidDetail(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
                 new UnderlyingNetworkInfo[0]);
 
         // Create some traffic on mobile network.
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         DEFAULT_NETWORK_NO, 2L, 1L, 3L, 4L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
@@ -1843,7 +1889,7 @@
     @Test
     public void testDataMigration() throws Exception {
         assertStatsFilesExist(false);
-        expectDefaultSettings();
+        mockDefaultSettings();
 
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
 
@@ -1852,10 +1898,9 @@
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(HOUR_IN_MILLIS);
-        // expectDefaultSettings();
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+        mockNetworkStatsUidDetail(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)
@@ -1892,9 +1937,9 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
 
@@ -1905,9 +1950,9 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
         mService.systemReady();
 
         // After systemReady(), the service should have historical stats loaded again.
@@ -1928,7 +1973,7 @@
     @Test
     public void testDataMigration_differentFromFallback() throws Exception {
         assertStatsFilesExist(false);
-        expectDefaultSettings();
+        mockDefaultSettings();
 
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
 
@@ -1937,9 +1982,9 @@
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(HOUR_IN_MILLIS);
-        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+        mockNetworkStatsUidDetail(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
@@ -1980,9 +2025,9 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
 
@@ -1993,9 +2038,9 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        expectDefaultSettings();
-        expectNetworkStatsUidDetail(buildEmptyStats());
-        expectSystemReady();
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+        prepareForSystemReady();
         mService.systemReady();
 
         // Verify the result read from public API matches the result returned from the importer.
@@ -2028,6 +2073,59 @@
         }
     }
 
+    @Test
+    public void testStatsFactoryRemoveUids() throws Exception {
+        // pretend that network comes online
+        mockDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        mockNetworkStatsSummary(buildEmptyStats());
+        mockNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        mockDefaultSettings();
+        final NetworkStats stats = new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
+                        4096L, 258L, 512L, 32L, 0L)
+                .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 64L, 3L, 1024L, 8L, 0L);
+        mockNetworkStatsUidDetail(stats);
+
+        forcePollAndWaitForIdle();
+
+        // Verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 0);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
+
+        // Simulate that the apps are removed.
+        final Intent intentBlue = new Intent(ACTION_UID_REMOVED);
+        intentBlue.putExtra(EXTRA_UID, UID_BLUE);
+        mServiceContext.sendBroadcast(intentBlue);
+
+        final Intent intentRed = new Intent(ACTION_UID_REMOVED);
+        intentRed.putExtra(EXTRA_UID, UID_RED);
+        mServiceContext.sendBroadcast(intentRed);
+
+        final int[] removedUids = {UID_BLUE, UID_RED};
+
+        final ArgumentCaptor<int[]> removedUidsCaptor = ArgumentCaptor.forClass(int[].class);
+        verify(mStatsFactory, times(2)).removeUidsLocked(removedUidsCaptor.capture());
+        final List<int[]> captureRemovedUids = removedUidsCaptor.getAllValues();
+        // Simulate that the stats are removed in NetworkStatsFactory.
+        if (captureRemovedUids.contains(removedUids)) {
+            stats.removeUids(removedUids);
+        }
+
+        // Verify the stats of the removed uid is removed.
+        assertUidTotal(sTemplateWifi, UID_RED, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
+    }
+
     private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
         assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
                 expected, mService.shouldRunComparison());
@@ -2090,8 +2188,8 @@
                 rxBytes, rxPackets, txBytes, txPackets, operations);
     }
 
-    private void expectSystemReady() throws Exception {
-        expectNetworkStatsSummary(buildEmptyStats());
+    private void prepareForSystemReady() throws Exception {
+        mockNetworkStatsSummary(buildEmptyStats());
     }
 
     private String getActiveIface(NetworkStateSnapshot... states) throws Exception {
@@ -2101,27 +2199,25 @@
         return states[0].getLinkProperties().getInterfaceName();
     }
 
-    // TODO: These expect* methods are used to have NetworkStatsService returns the given stats
-    //       instead of expecting anything. Therefore, these methods should be renamed properly.
-    private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
-        expectNetworkStatsSummaryDev(summary.clone());
-        expectNetworkStatsSummaryXt(summary.clone());
+    private void mockNetworkStatsSummary(NetworkStats summary) throws Exception {
+        mockNetworkStatsSummaryDev(summary.clone());
+        mockNetworkStatsSummaryXt(summary.clone());
     }
 
-    private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
+    private void mockNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
         when(mStatsFactory.readNetworkStatsSummaryDev()).thenReturn(summary);
     }
 
-    private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
+    private void mockNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
         when(mStatsFactory.readNetworkStatsSummaryXt()).thenReturn(summary);
     }
 
-    private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
+    private void mockNetworkStatsUidDetail(NetworkStats detail) throws Exception {
         final TetherStatsParcel[] tetherStatsParcels = {};
-        expectNetworkStatsUidDetail(detail, tetherStatsParcels);
+        mockNetworkStatsUidDetail(detail, tetherStatsParcels);
     }
 
-    private void expectNetworkStatsUidDetail(NetworkStats detail,
+    private void mockNetworkStatsUidDetail(NetworkStats detail,
             TetherStatsParcel[] tetherStatsParcels) throws Exception {
         when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL))
                 .thenReturn(detail);
@@ -2130,12 +2226,11 @@
         when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
     }
 
-    private void expectDefaultSettings() throws Exception {
-        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+    private void mockDefaultSettings() throws Exception {
+        mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
     }
 
-    private void expectSettings(long persistBytes, long bucketDuration, long deleteAge)
-            throws Exception {
+    private void mockSettings(long bucketDuration, long deleteAge) throws Exception {
         when(mSettings.getPollInterval()).thenReturn(HOUR_IN_MILLIS);
         when(mSettings.getPollDelay()).thenReturn(0L);
         when(mSettings.getSampleEnabled()).thenReturn(true);
@@ -2193,24 +2288,34 @@
     }
 
     private static NetworkStateSnapshot buildMobileState(String subscriberId) {
-        return buildMobileState(TEST_IFACE, subscriberId, false /* isTemporarilyNotMetered */,
-                false /* isRoaming */);
+        return buildStateOfTransport(NetworkCapabilities.TRANSPORT_CELLULAR, TYPE_MOBILE,
+                TEST_IFACE, subscriberId, null /* wifiNetworkKey */,
+                false /* isTemporarilyNotMetered */, false /* isRoaming */);
     }
 
-    private static NetworkStateSnapshot buildMobileState(String iface, String subscriberId,
+    private static NetworkStateSnapshot buildTestState(@NonNull String iface,
+            @Nullable String wifiNetworkKey) {
+        return buildStateOfTransport(NetworkCapabilities.TRANSPORT_TEST, TYPE_TEST,
+                iface, null /* subscriberId */, wifiNetworkKey,
+                false /* isTemporarilyNotMetered */, false /* isRoaming */);
+    }
+
+    private static NetworkStateSnapshot buildStateOfTransport(int transport, int legacyType,
+            String iface, String subscriberId, String wifiNetworkKey,
             boolean isTemporarilyNotMetered, boolean isRoaming) {
         final LinkProperties prop = new LinkProperties();
         prop.setInterfaceName(iface);
         final NetworkCapabilities capabilities = new NetworkCapabilities();
 
-        if (isTemporarilyNotMetered) {
-            capabilities.addCapability(
-                    NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        }
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED,
+                isTemporarilyNotMetered);
         capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming);
-        capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        capabilities.addTransportType(transport);
+        if (legacyType == TYPE_TEST && !TextUtils.isEmpty(wifiNetworkKey)) {
+            capabilities.setNetworkSpecifier(new TestNetworkSpecifier(wifiNetworkKey));
+        }
         return new NetworkStateSnapshot(
-                MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE);
+                MOBILE_NETWORK, capabilities, prop, subscriberId, legacyType);
     }
 
     private NetworkStats buildEmptyStats() {