Merge "Add ServiceSpecificException to avoid system crash"
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 9e6fc0b..f486c1e 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
@@ -75,6 +76,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.PacketBuilder;
@@ -151,7 +153,7 @@
     private static final long WAIT_RA_TIMEOUT_MS = 2000;
 
     private static final MacAddress TEST_MAC = MacAddress.fromString("1:2:3:4:5:6");
-    private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
+    private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/24");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
     private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
     private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
@@ -314,6 +316,13 @@
     }
 
     private boolean isInterfaceForTetheringAvailable() throws Exception {
+        // Before T, all ethernet interfaces could be used for server mode. Instead of
+        // waiting timeout, just checking whether the system currently has any
+        // ethernet interface is more reliable.
+        if (!SdkLevel.isAtLeastT()) {
+            return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+        }
+
         // If previous test case doesn't release tethering interface successfully, the other tests
         // after that test may be skipped as unexcepted.
         // TODO: figure out a better way to check default tethering interface existenion.
@@ -1518,7 +1527,7 @@
         final TestDnsPacket dnsQuery = TestDnsPacket.getTestDnsPacket(buf);
         assertNotNull(dnsQuery);
         Log.d(TAG, "Forwarded UDP source port: " + udpHeader.srcPort + ", DNS query id: "
-                + dnsQuery.getHeader().getId());
+                + dnsQuery.getHeader().id);
 
         // [2] Send DNS reply.
         // DNS server --> upstream --> dnsmasq forwarding --> downstream --> tethered device
@@ -1528,7 +1537,7 @@
         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().getId(), tester);
+                (short) udpHeader.srcPort, (short) dnsQuery.getHeader().id, tester);
     }
 
     private <T> List<T> toList(T... array) {
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index a84fdd2..ae36499 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -41,6 +41,8 @@
         "ctstestrunner-axt",
         "junit",
         "junit-params",
+        "connectivity-net-module-utils-bpf",
+        "net-utils-device-common-bpf",
     ],
 
     jni_libs: [
diff --git a/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java b/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java
new file mode 100644
index 0000000..9494aa4
--- /dev/null
+++ b/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tethering.mts;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.Process;
+import android.system.Os;
+import android.util.Pair;
+
+import com.android.net.module.util.BpfDump;
+import com.android.net.module.util.bpf.CookieTagMapKey;
+import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+public class SkDestroyListenerTest {
+    private static final int COOKIE_TAG = 0x1234abcd;
+    private static final int SOCKET_COUNT = 100;
+    private static final int SOCKET_CLOSE_WAIT_MS = 200;
+    private static final String LINE_DELIMITER = "\\n";
+    private static final String DUMP_COMMAND = "dumpsys netstats --bpfRawMap --cookieTagMap";
+
+    private Map<CookieTagMapKey, CookieTagMapValue> parseBpfRawMap(final String dump) {
+        final Map<CookieTagMapKey, CookieTagMapValue> map = new HashMap<>();
+        for (final String line: dump.split(LINE_DELIMITER)) {
+            final Pair<CookieTagMapKey, CookieTagMapValue> keyValue =
+                    BpfDump.fromBase64EncodedString(CookieTagMapKey.class,
+                            CookieTagMapValue.class, line.trim());
+            map.put(keyValue.first, keyValue.second);
+        }
+        return map;
+    }
+
+    private int countTaggedSocket() {
+        final String dump = runShellCommandOrThrow(DUMP_COMMAND);
+        final Map<CookieTagMapKey, CookieTagMapValue> cookieTagMap = parseBpfRawMap(dump);
+        int count = 0;
+        for (final CookieTagMapValue value: cookieTagMap.values()) {
+            if (value.tag == COOKIE_TAG && value.uid == Process.myUid()) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    private boolean noTaggedSocket() {
+        return countTaggedSocket() == 0;
+    }
+
+    private void doTestSkDestroyListener(final int family, final int type) throws Exception {
+        assertTrue("There are tagged sockets before test", noTaggedSocket());
+
+        TrafficStats.setThreadStatsTag(COOKIE_TAG);
+        final List<FileDescriptor> fds = new ArrayList<>();
+        for (int i = 0; i < SOCKET_COUNT; i++) {
+            fds.add(Os.socket(family, type, 0 /* protocol */));
+        }
+        TrafficStats.clearThreadStatsTag();
+        assertEquals("Number of tagged socket does not match after creating sockets",
+                SOCKET_COUNT, countTaggedSocket());
+
+        for (final FileDescriptor fd: fds) {
+            Os.close(fd);
+        }
+        // Wait a bit for skDestroyListener to handle all the netlink messages.
+        Thread.sleep(SOCKET_CLOSE_WAIT_MS);
+        assertTrue("There are tagged sockets after closing sockets", noTaggedSocket());
+    }
+
+    @Test
+    public void testSkDestroyListener() throws Exception {
+        doTestSkDestroyListener(AF_INET, SOCK_STREAM);
+        doTestSkDestroyListener(AF_INET, SOCK_DGRAM);
+        doTestSkDestroyListener(AF_INET6, SOCK_STREAM);
+        doTestSkDestroyListener(AF_INET6, SOCK_DGRAM);
+    }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index e114cb5..38f1e9c 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -664,17 +664,17 @@
             assertEquals("Internal callback is not registered", 1, callbacks.size());
             assertNotNull(weakTm.get());
 
+            // Calling System.gc() or System.runFinalization() doesn't guarantee GCs or finalizers
+            // are executed synchronously. The finalizer is called after GC on a separate thread.
             final int attempts = 100;
             final long waitIntervalMs = 50;
             for (int i = 0; i < attempts; i++) {
                 forceGc();
-                if (weakTm.get() == null) break;
+                if (weakTm.get() == null && callbacks.size() == 0) break;
 
                 Thread.sleep(waitIntervalMs);
             }
-            assertNull("TetheringManager weak reference still not null after " + attempts
-                    + " attempts", weakTm.get());
-
+            assertNull("TetheringManager weak reference is not null", weakTm.get());
             assertEquals("Internal callback is not unregistered", 0, callbacks.size());
         });
     }
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 09b4538..547b7e2 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -188,7 +188,6 @@
     field public static final int FLAG_NO_RETRY = 1; // 0x1
     field public static final int TYPE_A = 1; // 0x1
     field public static final int TYPE_AAAA = 28; // 0x1c
-    field public static final int TYPE_CNAME = 5; // 0x5
   }
 
   public static interface DnsResolver.Callback<T> {
diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java
index 33722fc..5e637f9 100644
--- a/framework/src/android/net/DnsResolver.java
+++ b/framework/src/android/net/DnsResolver.java
@@ -71,14 +71,12 @@
 
     @IntDef(prefix = { "TYPE_" },  value = {
             TYPE_A,
-            TYPE_AAAA,
-            TYPE_CNAME
+            TYPE_AAAA
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface QueryType {}
     public static final int TYPE_A = 1;
     public static final int TYPE_AAAA = 28;
-    public static final int TYPE_CNAME = 5;
 
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_EMPTY,
@@ -544,7 +542,7 @@
 
         DnsAddressAnswer(@NonNull byte[] data) throws ParseException {
             super(data);
-            if ((mHeader.getFlags() & (1 << 15)) == 0) {
+            if ((mHeader.flags & (1 << 15)) == 0) {
                 throw new ParseException("Not an answer packet");
             }
             if (mHeader.getRecordCount(QDSECTION) == 0) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
index 17abbab..156b526 100644
--- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -116,6 +116,10 @@
     }
 
     public void write(String iface, IpConfiguration config) {
+        final File directory = new File(APEX_IP_CONFIG_FILE_PATH);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
         write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE);
     }
 
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
index 3b44d81..57466a6 100644
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -22,10 +22,12 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
@@ -136,4 +138,37 @@
             mHandler.post(() -> addInterface(ifName));
         }
     }
+
+    /** get interface name by interface index from bpf map */
+    public String getIfNameByIndex(final long index) {
+        try {
+            final InterfaceMapValue value = mBpfMap.getValue(new U32(index));
+            if (value == null) {
+                Log.e(TAG, "No if name entry for index " + index);
+                return null;
+            }
+            return value.getInterfaceNameString();
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
+            return null;
+        }
+    }
+
+    /**
+     * Dump BPF map
+     *
+     * @param pw print writer
+     */
+    public void dump(final IndentingPrintWriter pw) {
+        pw.println("BPF map status:");
+        pw.increaseIndent();
+        BpfDump.dumpMapStatus(mBpfMap, pw, "IfaceIndexNameMap", IFACE_INDEX_NAME_MAP_PATH);
+        pw.decreaseIndent();
+        pw.println("BPF map content:");
+        pw.increaseIndent();
+        BpfDump.dumpMap(mBpfMap, pw, "IfaceIndexNameMap",
+                (key, value) -> "ifaceIndex=" + key.val
+                        + " ifaceName=" + value.getInterfaceNameString());
+        pw.decreaseIndent();
+    }
 }
diff --git a/service-t/src/com/android/server/net/InterfaceMapValue.java b/service-t/src/com/android/server/net/InterfaceMapValue.java
index 42c0044..95da981 100644
--- a/service-t/src/com/android/server/net/InterfaceMapValue.java
+++ b/service-t/src/com/android/server/net/InterfaceMapValue.java
@@ -16,20 +16,45 @@
 package com.android.server.net;
 
 import com.android.net.module.util.Struct;
-import com.android.net.module.util.Struct.Field;
-import com.android.net.module.util.Struct.Type;
+
+import java.util.Arrays;
 
 /**
  * The value of bpf interface index map which is used for NetworkStatsService.
  */
 public class InterfaceMapValue extends Struct {
+    private static final int IF_NAME_SIZE = 16;
+
     @Field(order = 0, type = Type.ByteArray, arraysize = 16)
     public final byte[] interfaceName;
 
     public InterfaceMapValue(String iface) {
-        final byte[] ifaceArray = iface.getBytes();
-        interfaceName = new byte[16];
         // All array bytes after the interface name, if any, must be 0.
-        System.arraycopy(ifaceArray, 0, interfaceName, 0, ifaceArray.length);
+        interfaceName = Arrays.copyOf(iface.getBytes(), IF_NAME_SIZE);
+    }
+
+    /**
+     * Constructor for Struct#parse. Build this struct from byte array of interface name.
+     *
+     * @param ifName Byte array of interface name, length is expected to be IF_NAME_SIZE(16).
+     *               If longer or shorter, interface name will be truncated or padded with zeros.
+     *               All array bytes after the interface name, if any, must be 0.
+     */
+    public InterfaceMapValue(final byte[] ifName) {
+        interfaceName = Arrays.copyOf(ifName, IF_NAME_SIZE);
+    }
+
+    /** Returns the length of the null-terminated string. */
+    private int strlen(byte[] str) {
+        for (int i = 0; i < str.length; ++i) {
+            if (str[i] == '\0') {
+                return i;
+            }
+        }
+        return str.length;
+    }
+
+    public String getInterfaceNameString() {
+        return new String(interfaceName, 0 /* offset */,  strlen(interfaceName));
     }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index bad3567..807f5d7 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -162,11 +162,13 @@
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.net.module.util.NetworkStatsUtils;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.server.BpfNetMaps;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -454,6 +456,9 @@
     @NonNull
     private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
 
+    @Nullable
+    private final SkDestroyListener mSkDestroyListener;
+
     private static @NonNull Clock getDefaultClock() {
         return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
                 Clock.systemUTC());
@@ -587,6 +592,18 @@
         mStatsMapA = mDeps.getStatsMapA();
         mStatsMapB = mDeps.getStatsMapB();
         mAppUidStatsMap = mDeps.getAppUidStatsMap();
+
+        // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
+        // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
+        // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
+        // NetworkStatsService starts Java SkDestroyListener (new code).
+        final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
+        if (bpfNetMaps.isSkDestroyListenerRunning()) {
+            mSkDestroyListener = null;
+        } else {
+            mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+            mHandler.post(mSkDestroyListener::start);
+        }
     }
 
     /**
@@ -782,6 +799,17 @@
         public boolean isDebuggable() {
             return Build.isDebuggable();
         }
+
+        /** Create a new BpfNetMaps. */
+        public BpfNetMaps makeBpfNetMaps(Context ctx) {
+            return new BpfNetMaps(ctx);
+        }
+
+        /** Create a new SkDestroyListener. */
+        public SkDestroyListener makeSkDestroyListener(
+                IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+            return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
+        }
     }
 
     /**
@@ -2713,6 +2741,12 @@
             }
 
             pw.println();
+            pw.println("InterfaceMapUpdater:");
+            pw.increaseIndent();
+            mInterfaceMapUpdater.dump(pw);
+            pw.decreaseIndent();
+
+            pw.println();
             pw.println("BPF map status:");
             pw.increaseIndent();
             dumpMapStatus(pw);
@@ -2727,6 +2761,8 @@
             dumpCookieTagMapLocked(pw);
             dumpUidCounterSetMapLocked(pw);
             dumpAppUidStatsMapLocked(pw);
+            dumpStatsMapLocked(mStatsMapA, pw, "mStatsMapA");
+            dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
             pw.decreaseIndent();
         }
     }
@@ -2812,6 +2848,8 @@
         pw.println("mUidCounterSetMap: "
                 + getMapStatus(mUidCounterSetMap, UID_COUNTERSET_MAP_PATH));
         pw.println("mAppUidStatsMap: " + getMapStatus(mAppUidStatsMap, APP_UID_STATS_MAP_PATH));
+        pw.println("mStatsMapA: " + getMapStatus(mStatsMapA, STATS_MAP_A_PATH));
+        pw.println("mStatsMapB: " + getMapStatus(mStatsMapB, STATS_MAP_B_PATH));
     }
 
     @GuardedBy("mStatsLock")
@@ -2848,6 +2886,30 @@
                         + value.txPackets);
     }
 
+    @GuardedBy("mStatsLock")
+    private void dumpStatsMapLocked(final IBpfMap<StatsMapKey, StatsMapValue> statsMap,
+            final IndentingPrintWriter pw, final String mapName) {
+        if (statsMap == null) {
+            return;
+        }
+
+        BpfDump.dumpMap(statsMap, pw, mapName,
+                "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
+                (key, value) -> {
+                    final long ifIndex = key.ifaceIndex;
+                    final String ifName = mInterfaceMapUpdater.getIfNameByIndex(ifIndex);
+                    return ifIndex + " "
+                            + (ifName != null ? ifName : "unknown") + " "
+                            + "0x" + Long.toHexString(key.tag) + " "
+                            + key.uid + " "
+                            + key.counterSet + " "
+                            + value.rxBytes + " "
+                            + value.rxPackets + " "
+                            + value.txBytes + " "
+                            + value.txPackets;
+                });
+    }
+
     private NetworkStats readNetworkStatsSummaryDev() {
         try {
             return mStatsFactory.readNetworkStatsSummaryDev();
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
new file mode 100644
index 0000000..7b68f89
--- /dev/null
+++ b/service-t/src/com/android/server/net/SkDestroyListener.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import android.os.Handler;
+import android.system.ErrnoException;
+
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.bpf.CookieTagMapKey;
+import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.ip.NetlinkMonitor;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.StructInetDiagSockId;
+
+/**
+ * Monitor socket destroy and delete entry from cookie tag bpf map.
+ */
+public class SkDestroyListener extends NetlinkMonitor {
+    private static final int SKNLGRP_INET_TCP_DESTROY = 1;
+    private static final int SKNLGRP_INET_UDP_DESTROY = 2;
+    private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
+    private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
+
+    // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
+    // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
+    // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+    // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
+    // ENOBUFS and leaking mCookieTagMap entries.
+    private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
+
+    private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap;
+
+    SkDestroyListener(final IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap,
+            final Handler handler, final SharedLog log) {
+        super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
+                1 << (SKNLGRP_INET_TCP_DESTROY - 1)
+                        | 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
+                        | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1)
+                        | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1),
+                SOCK_RCV_BUF_SIZE);
+        mCookieTagMap = cookieTagMap;
+    }
+
+    @Override
+    public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof InetDiagMessage)) {
+            mLog.e("Received non InetDiagMessage");
+            return;
+        }
+        final StructInetDiagSockId sockId = ((InetDiagMessage) nlMsg).inetDiagMsg.id;
+        try {
+            mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
+        } catch (ErrnoException e) {
+            mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie  + ": " + e);
+        }
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
index b68d389..68c1722 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -143,6 +143,7 @@
         "src/**/*.java",
         ":framework-connectivity-shared-srcs",
         ":services-connectivity-shared-srcs",
+        ":statslog-connectivity-java-gen",
     ],
     libs: [
         "framework-annotations-lib",
@@ -152,6 +153,7 @@
         "framework-wifi.stubs.module_lib",
         "unsupportedappusage",
         "ServiceConnectivityResources",
+        "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
         // Do not add libs here if they are already included
@@ -332,3 +334,10 @@
         "--output $(out)",
     visibility: ["//visibility:private"],
 }
+
+genrule {
+  name: "statslog-connectivity-java-gen",
+  tools: ["stats-log-api-gen"],
+  cmd: "$(location stats-log-api-gen) --java $(out) --module connectivity --javaPackage com.android.server --javaClass ConnectivityStatsLog",
+  out: ["com/android/server/ConnectivityStatsLog.java"],
+}
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index 71fa8e4..799ac5c 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -47,8 +47,8 @@
       ALOGE("%s failed, error code = %d", __func__, status.code()); \
   } while (0)
 
-static void native_init(JNIEnv* env, jclass clazz) {
-  Status status = mTc.start();
+static void native_init(JNIEnv* env, jclass clazz, jboolean startSkDestroyListener) {
+  Status status = mTc.start(startSkDestroyListener);
   CHECK_LOG(status);
   if (!isOk(status)) {
     uid_t uid = getuid();
@@ -201,7 +201,7 @@
 // clang-format off
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    {"native_init", "()V",
+    {"native_init", "(Z)V",
     (void*)native_init},
     {"native_addNaughtyApp", "(I)I",
     (void*)native_addNaughtyApp},
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index a26d1e6..49b23c9 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -181,9 +181,13 @@
     return netdutils::status::ok;
 }
 
-Status TrafficController::start() {
+Status TrafficController::start(bool startSkDestroyListener) {
     RETURN_IF_NOT_OK(initMaps());
 
+    if (!startSkDestroyListener) {
+        return netdutils::status::ok;
+    }
+
     auto result = makeSkDestroyListener();
     if (!isOk(result)) {
         ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
@@ -643,48 +647,6 @@
         dw.println("mCookieTagMap print end with error: %s", res.error().message().c_str());
     }
 
-
-    // Print uidStatsMap content.
-    std::string statsHeader = StringPrintf("ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes"
-                                           " rxPackets txBytes txPackets");
-    dumpBpfMap("mStatsMapA", dw, statsHeader);
-    const auto printStatsInfo = [&dw, this](const StatsKey& key, const StatsValue& value,
-                                            const BpfMap<StatsKey, StatsValue>&) {
-        uint32_t ifIndex = key.ifaceIndex;
-        auto ifname = mIfaceIndexNameMap.readValue(ifIndex);
-        if (!ifname.ok()) {
-            ifname = IfaceValue{"unknown"};
-        }
-        dw.println("%u %s 0x%x %u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, ifIndex,
-                   ifname.value().name, key.tag, key.uid, key.counterSet, value.rxBytes,
-                   value.rxPackets, value.txBytes, value.txPackets);
-        return base::Result<void>();
-    };
-    res = mStatsMapA.iterateWithValue(printStatsInfo);
-    if (!res.ok()) {
-        dw.println("mStatsMapA print end with error: %s", res.error().message().c_str());
-    }
-
-    // Print TagStatsMap content.
-    dumpBpfMap("mStatsMapB", dw, statsHeader);
-    res = mStatsMapB.iterateWithValue(printStatsInfo);
-    if (!res.ok()) {
-        dw.println("mStatsMapB print end with error: %s", res.error().message().c_str());
-    }
-
-    // Print ifaceIndexToNameMap content.
-    dumpBpfMap("mIfaceIndexNameMap", dw, "");
-    const auto printIfaceNameInfo = [&dw](const uint32_t& key, const IfaceValue& value,
-                                          const BpfMap<uint32_t, IfaceValue>&) {
-        const char* ifname = value.name;
-        dw.println("ifaceIndex=%u ifaceName=%s", key, ifname);
-        return base::Result<void>();
-    };
-    res = mIfaceIndexNameMap.iterateWithValue(printIfaceNameInfo);
-    if (!res.ok()) {
-        dw.println("mIfaceIndexNameMap print end with error: %s", res.error().message().c_str());
-    }
-
     // Print ifaceStatsMap content
     std::string ifaceStatsHeader = StringPrintf("ifaceIndex ifaceName rxBytes rxPackets txBytes"
                                                 " txPackets");
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index d08ffee..8525738 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -60,7 +60,6 @@
 constexpr uint32_t TEST_TAG = 42;
 constexpr uint32_t TEST_COUNTERSET = 1;
 constexpr int TEST_COOKIE = 1;
-constexpr char TEST_IFNAME[] = "test0";
 constexpr int TEST_IFINDEX = 999;
 constexpr int RXPACKETS = 1;
 constexpr int RXBYTES = 100;
@@ -792,17 +791,7 @@
     // 999 test0 0x2a 10086 1 100 1 0 0
     std::vector<std::string> expectedLines = {
         "mCookieTagMap:",
-        fmt::format("cookie={} tag={:#x} uid={}", TEST_COOKIE, TEST_TAG, TEST_UID),
-        "mStatsMapA",
-        "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
-        fmt::format("{} {} {:#x} {} {} {} {} {} {}",
-                    TEST_IFINDEX, TEST_IFNAME, TEST_TAG, TEST_UID, TEST_COUNTERSET, RXBYTES,
-                    RXPACKETS, TXBYTES, TXPACKETS)};
-
-    populateFakeIfaceIndexName(TEST_IFNAME, TEST_IFINDEX);
-    expectedLines.emplace_back("mIfaceIndexNameMap:");
-    expectedLines.emplace_back(fmt::format("ifaceIndex={} ifaceName={}",
-                                           TEST_IFINDEX, TEST_IFNAME));
+        fmt::format("cookie={} tag={:#x} uid={}", TEST_COOKIE, TEST_TAG, TEST_UID)};
 
     ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, HAPPY_BOX_MATCH,
                                         TrafficController::IptOpInsert)));
@@ -829,9 +818,6 @@
 
     std::vector<std::string> expectedLines = {
         fmt::format("mCookieTagMap {}", kErrIterate),
-        fmt::format("mStatsMapA {}", kErrIterate),
-        fmt::format("mStatsMapB {}", kErrIterate),
-        fmt::format("mIfaceIndexNameMap {}", kErrIterate),
         fmt::format("mIfaceStatsMap {}", kErrIterate),
         fmt::format("mConfigurationMap {}", kErrReadRulesConfig),
         fmt::format("mConfigurationMap {}", kErrReadStatsMapConfig),
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 8512929..b44d795 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -38,7 +38,7 @@
     /*
      * Initialize the whole controller
      */
-    netdutils::Status start();
+    netdutils::Status start(bool startSkDestroyListener);
 
     /*
      * Swap the stats map config from current active stats map to the idle one.
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index dc5c4c7..bfa0808 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -33,6 +33,9 @@
 import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.EOPNOTSUPP;
 
+import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
+
+import android.app.StatsManager;
 import android.content.Context;
 import android.net.INetd;
 import android.os.RemoteException;
@@ -42,17 +45,23 @@
 import android.system.Os;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.StatsEvent;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BackgroundThread;
 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;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
+import com.android.net.module.util.bpf.CookieTagMapKey;
+import com.android.net.module.util.bpf.CookieTagMapValue;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -94,6 +103,8 @@
             "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
     private static final String UID_PERMISSION_MAP_PATH =
             "/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
+    private static final String COOKIE_TAG_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
     private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
     private static final U32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new U32(1);
     private static final long UID_RULES_DEFAULT_CONFIGURATION = 0;
@@ -104,6 +115,7 @@
     // BpfMap for UID_OWNER_MAP_PATH. This map is not accessed by others.
     private static IBpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
     private static IBpfMap<U32, U8> sUidPermissionMap = null;
+    private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
 
     // LINT.IfChange(match_type)
     @VisibleForTesting public static final long NO_MATCH = 0;
@@ -153,6 +165,15 @@
         sUidPermissionMap = uidPermissionMap;
     }
 
+    /**
+     * Set cookieTagMap for test.
+     */
+    @VisibleForTesting
+    public static void setCookieTagMapForTest(
+            IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap) {
+        sCookieTagMap = cookieTagMap;
+    }
+
     private static IBpfMap<U32, U32> getConfigurationMap() {
         try {
             return new BpfMap<>(
@@ -180,6 +201,15 @@
         }
     }
 
+    private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+        try {
+            return new BpfMap<>(COOKIE_TAG_MAP_PATH, BpfMap.BPF_F_RDWR,
+                    CookieTagMapKey.class, CookieTagMapValue.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open cookie tag map", e);
+        }
+    }
+
     private static void initBpfMaps() {
         if (sConfigurationMap == null) {
             sConfigurationMap = getConfigurationMap();
@@ -209,6 +239,10 @@
         if (sUidPermissionMap == null) {
             sUidPermissionMap = getUidPermissionMap();
         }
+
+        if (sCookieTagMap == null) {
+            sCookieTagMap = getCookieTagMap();
+        }
     }
 
     /**
@@ -225,10 +259,14 @@
         Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
 
         initBpfMaps();
-        native_init();
+        native_init(!sEnableJavaBpfMap /* startSkDestroyListener */);
         sInitialized = true;
     }
 
+    public boolean isSkDestroyListenerRunning() {
+        return !sEnableJavaBpfMap;
+    }
+
     /**
      * Dependencies of BpfNetMaps, for injection in tests.
      */
@@ -247,6 +285,15 @@
         public int synchronizeKernelRCU() {
             return native_synchronizeKernelRCU();
         }
+
+        /**
+         * Build Stats Event for NETWORK_BPF_MAP_INFO atom
+         */
+        public StatsEvent buildStatsEvent(final int cookieTagMapSize, final int uidOwnerMapSize,
+                final int uidPermissionMapSize) {
+            return ConnectivityStatsLog.buildStatsEvent(NETWORK_BPF_MAP_INFO, cookieTagMapSize,
+                    uidOwnerMapSize, uidPermissionMapSize);
+        }
     }
 
     /** Constructor used after T that doesn't need to use netd anymore. */
@@ -822,6 +869,43 @@
         }
     }
 
+    /** Register callback for statsd to pull atom. */
+    public void setPullAtomCallback(final Context context) {
+        throwIfPreT("setPullAtomCallback is not available on pre-T devices");
+
+        final StatsManager statsManager = context.getSystemService(StatsManager.class);
+        statsManager.setPullAtomCallback(NETWORK_BPF_MAP_INFO, null /* metadata */,
+                BackgroundThread.getExecutor(), this::pullBpfMapInfoAtom);
+    }
+
+    private <K extends Struct, V extends Struct> int getMapSize(IBpfMap<K, V> map)
+            throws ErrnoException {
+        // forEach could restart iteration from the beginning if there is a concurrent entry
+        // deletion. netd and skDestroyListener could delete CookieTagMap entry concurrently.
+        // So using Set to count the number of entry in the map.
+        Set<K> keySet = new ArraySet<>();
+        map.forEach((k, v) -> keySet.add(k));
+        return keySet.size();
+    }
+
+    /** Callback for StatsManager#setPullAtomCallback */
+    @VisibleForTesting
+    public int pullBpfMapInfoAtom(final int atomTag, final List<StatsEvent> data) {
+        if (atomTag != NETWORK_BPF_MAP_INFO) {
+            Log.e(TAG, "Unexpected atom tag: " + atomTag);
+            return StatsManager.PULL_SKIP;
+        }
+
+        try {
+            data.add(mDeps.buildStatsEvent(getMapSize(sCookieTagMap), getMapSize(sUidOwnerMap),
+                    getMapSize(sUidPermissionMap)));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to pull NETWORK_BPF_MAP_INFO atom: " + e);
+            return StatsManager.PULL_SKIP;
+        }
+        return StatsManager.PULL_SUCCESS;
+    }
+
     /**
      * Dump BPF maps
      *
@@ -839,7 +923,7 @@
         native_dump(fd, verbose);
     }
 
-    private static native void native_init();
+    private static native void native_init(boolean startSkDestroyListener);
     private native int native_addNaughtyApp(int uid);
     private native int native_removeNaughtyApp(int uid);
     private native int native_addNiceApp(int uid);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0035aa0..7e63761 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -3040,6 +3040,11 @@
         if (!ConnectivitySettingsManager.getMobileDataPreferredUids(mContext).isEmpty()) {
             updateMobileDataPreferredUids();
         }
+
+        // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
+        if (SdkLevel.isAtLeastT()) {
+            mBpfNetMaps.setPullAtomCallback(mContext);
+        }
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index c4754eb..22a820b 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -23,7 +23,6 @@
 import static android.net.NetworkScore.KEEP_CONNECTED_NONE;
 import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
@@ -35,8 +34,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.MessageUtils;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.StringJoiner;
 
 /**
@@ -49,53 +46,44 @@
 public class FullScore {
     private static final String TAG = FullScore.class.getSimpleName();
 
-    /** @hide */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = {"POLICY_"}, value = {
-            POLICY_IS_VALIDATED,
-            POLICY_IS_VPN,
-            POLICY_EVER_USER_SELECTED,
-            POLICY_ACCEPT_UNVALIDATED,
-            POLICY_IS_UNMETERED
-    })
-    public @interface Policy {
-    }
-
     // Agent-managed policies are in NetworkScore. They start from 1.
     // CS-managed policies, counting from 63 downward
     // This network is validated. CS-managed because the source of truth is in NetworkCapabilities.
     /** @hide */
     public static final int POLICY_IS_VALIDATED = 63;
 
+    // This network has been validated at least once since it was connected.
+    /** @hide */
+    public static final int POLICY_EVER_VALIDATED = 62;
+
     // This is a VPN and behaves as one for scoring purposes.
     /** @hide */
-    public static final int POLICY_IS_VPN = 62;
+    public static final int POLICY_IS_VPN = 61;
 
     // This network has been selected by the user manually from settings or a 3rd party app
     // at least once. @see NetworkAgentConfig#explicitlySelected.
     /** @hide */
-    public static final int POLICY_EVER_USER_SELECTED = 61;
+    public static final int POLICY_EVER_USER_SELECTED = 60;
 
     // The user has indicated in UI that this network should be used even if it doesn't
     // validate. @see NetworkAgentConfig#acceptUnvalidated.
     /** @hide */
-    public static final int POLICY_ACCEPT_UNVALIDATED = 60;
+    public static final int POLICY_ACCEPT_UNVALIDATED = 59;
+
+    // The user explicitly said in UI to avoid this network when unvalidated.
+    // TODO : remove setAvoidUnvalidated and instead disconnect the network when the user
+    // chooses to move away from this network, and remove this flag.
+    /** @hide */
+    public static final int POLICY_AVOIDED_WHEN_UNVALIDATED = 58;
 
     // This network is unmetered. @see NetworkCapabilities.NET_CAPABILITY_NOT_METERED.
     /** @hide */
-    public static final int POLICY_IS_UNMETERED = 59;
+    public static final int POLICY_IS_UNMETERED = 57;
 
     // This network is invincible. This is useful for offers until there is an API to listen
     // to requests.
     /** @hide */
-    public static final int POLICY_IS_INVINCIBLE = 58;
-
-    // This network has been validated at least once since it was connected, but not explicitly
-    // avoided in UI.
-    // TODO : remove setAvoidUnvalidated and instead disconnect the network when the user
-    // chooses to move away from this network, and remove this flag.
-    /** @hide */
-    public static final int POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD = 57;
+    public static final int POLICY_IS_INVINCIBLE = 56;
 
     // The network agent has communicated that this network no longer functions, and the underlying
     // native network has been destroyed. The network will still be reported to clients as connected
@@ -103,7 +91,7 @@
     // This network should lose to an identical network that has not been destroyed, but should
     // otherwise be scored exactly the same.
     /** @hide */
-    public static final int POLICY_IS_DESTROYED = 56;
+    public static final int POLICY_IS_DESTROYED = 55;
 
     // To help iterate when printing
     @VisibleForTesting
@@ -154,6 +142,7 @@
      * @param caps the NetworkCapabilities of the network
      * @param config the NetworkAgentConfig of the network
      * @param everValidated whether this network has ever validated
+     * @param avoidUnvalidated whether the user said in UI to avoid this network when unvalidated
      * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad
      * @param destroyed whether this network has been destroyed pending a replacement connecting
      * @return a FullScore that is appropriate to use for ranking.
@@ -163,18 +152,19 @@
     // connectivity for backward compatibility.
     public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
             @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config,
-            final boolean everValidated, final boolean yieldToBadWiFi, final boolean destroyed) {
+            final boolean everValidated, final boolean avoidUnvalidated,
+            final boolean yieldToBadWiFi, final boolean destroyed) {
         return withPolicies(score.getPolicies(),
                 score.getKeepConnectedReason(),
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
-                caps.hasTransport(TRANSPORT_VPN),
-                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
-                everValidated,
+                everValidated, caps.hasTransport(TRANSPORT_VPN),
                 config.explicitlySelected,
                 config.acceptUnvalidated,
+                avoidUnvalidated,
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
                 yieldToBadWiFi,
-                destroyed,
-                false /* invincible */); // only prospective scores can be invincible
+                false /* invincible */, // only prospective scores can be invincible
+                destroyed);
     }
 
     /**
@@ -194,25 +184,29 @@
             @NonNull final NetworkCapabilities caps, final boolean yieldToBadWiFi) {
         // If the network offers Internet access, it may validate.
         final boolean mayValidate = caps.hasCapability(NET_CAPABILITY_INTERNET);
-        // VPN transports are known in advance.
-        final boolean vpn = caps.hasTransport(TRANSPORT_VPN);
-        // Prospective scores are always unmetered, because unmetered networks are stronger
-        // than metered networks, and it's not known in advance whether the network is metered.
-        final boolean unmetered = true;
         // If the offer may validate, then it should be considered to have validated at some point
         final boolean everValidated = mayValidate;
+        // VPN transports are known in advance.
+        final boolean vpn = caps.hasTransport(TRANSPORT_VPN);
         // The network hasn't been chosen by the user (yet, at least).
         final boolean everUserSelected = false;
         // Don't assume the user will accept unvalidated connectivity.
         final boolean acceptUnvalidated = false;
+        // A prospective network is never avoided when unvalidated, because the user has never
+        // had the opportunity to say so in UI.
+        final boolean avoidUnvalidated = false;
+        // Prospective scores are always unmetered, because unmetered networks are stronger
+        // than metered networks, and it's not known in advance whether the network is metered.
+        final boolean unmetered = true;
         // A network can only be destroyed once it has connected.
         final boolean destroyed = false;
         // A prospective score is invincible if the legacy int in the filter is over the maximum
         // score.
         final boolean invincible = score.getLegacyInt() > NetworkRanker.LEGACY_INT_MAX;
         return withPolicies(score.getPolicies(), KEEP_CONNECTED_NONE,
-                mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated,
-                yieldToBadWiFi, destroyed, invincible);
+                mayValidate, everValidated, vpn, everUserSelected, acceptUnvalidated,
+                avoidUnvalidated, unmetered,
+                yieldToBadWiFi, invincible, destroyed);
     }
 
     /**
@@ -228,18 +222,19 @@
     public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
             @NonNull final NetworkAgentConfig config,
             final boolean everValidated,
+            final boolean avoidUnvalidated,
             final boolean yieldToBadWifi,
             final boolean destroyed) {
         return withPolicies(mPolicies, mKeepConnectedReason,
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
-                caps.hasTransport(TRANSPORT_VPN),
-                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
-                everValidated,
+                everValidated, caps.hasTransport(TRANSPORT_VPN),
                 config.explicitlySelected,
                 config.acceptUnvalidated,
+                avoidUnvalidated,
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
                 yieldToBadWifi,
-                destroyed,
-                false /* invincible */); // only prospective scores can be invincible
+                false /* invincible */, // only prospective scores can be invincible
+                destroyed);
     }
 
     // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
@@ -248,24 +243,26 @@
     private static FullScore withPolicies(final long externalPolicies,
             @KeepConnectedReason final int keepConnectedReason,
             final boolean isValidated,
-            final boolean isVpn,
-            final boolean isUnmetered,
             final boolean everValidated,
+            final boolean isVpn,
             final boolean everUserSelected,
             final boolean acceptUnvalidated,
+            final boolean avoidUnvalidated,
+            final boolean isUnmetered,
             final boolean yieldToBadWiFi,
-            final boolean destroyed,
-            final boolean invincible) {
+            final boolean invincible,
+            final boolean destroyed) {
         return new FullScore((externalPolicies & EXTERNAL_POLICIES_MASK)
                 | (isValidated       ? 1L << POLICY_IS_VALIDATED : 0)
+                | (everValidated     ? 1L << POLICY_EVER_VALIDATED : 0)
                 | (isVpn             ? 1L << POLICY_IS_VPN : 0)
-                | (isUnmetered       ? 1L << POLICY_IS_UNMETERED : 0)
-                | (everValidated     ? 1L << POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD : 0)
                 | (everUserSelected  ? 1L << POLICY_EVER_USER_SELECTED : 0)
                 | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0)
+                | (avoidUnvalidated  ? 1L << POLICY_AVOIDED_WHEN_UNVALIDATED : 0)
+                | (isUnmetered       ? 1L << POLICY_IS_UNMETERED : 0)
                 | (yieldToBadWiFi    ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0)
-                | (destroyed         ? 1L << POLICY_IS_DESTROYED : 0)
-                | (invincible        ? 1L << POLICY_IS_INVINCIBLE : 0),
+                | (invincible        ? 1L << POLICY_IS_INVINCIBLE : 0)
+                | (destroyed         ? 1L << POLICY_IS_DESTROYED : 0),
                 keepConnectedReason);
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index d4d7d96..d7be421 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -392,6 +392,9 @@
     // URL, Terms & Conditions URL, and network friendly name.
     public CaptivePortalData networkAgentPortalData;
 
+    // Indicate whether this device has the automotive feature.
+    private final boolean mHasAutomotiveFeature;
+
     /**
      * Sets the capabilities sent by the agent for later retrieval.
      *
@@ -433,9 +436,8 @@
                     + networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
             nc.setOwnerUid(networkCapabilities.getOwnerUid());
         }
-        restrictCapabilitiesFromNetworkAgent(nc, creatorUid,
-                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE),
-                carrierPrivilegeAuthenticator);
+        restrictCapabilitiesFromNetworkAgent(
+                nc, creatorUid, mHasAutomotiveFeature, carrierPrivilegeAuthenticator);
         return nc;
     }
 
@@ -604,6 +606,8 @@
                 ? nc.getUnderlyingNetworks().toArray(new Network[0])
                 : null;
         mCreationTime = System.currentTimeMillis();
+        mHasAutomotiveFeature =
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
     }
 
     private class AgentDeathMonitor implements IBinder.DeathRecipient {
@@ -970,8 +974,8 @@
             @NonNull final NetworkCapabilities nc) {
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
-                yieldToBadWiFi(), isDestroyed());
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidated(),
+                0L != getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -1174,7 +1178,7 @@
      */
     public void setScore(final NetworkScore score) {
         mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), isDestroyed());
+                everValidated(), 0L == getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
     }
 
     /**
@@ -1184,11 +1188,7 @@
      */
     public void updateScoreForNetworkAgentUpdate() {
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), isDestroyed());
-    }
-
-    private boolean everValidatedForYield() {
-        return everValidated() && 0L == mAvoidUnvalidated;
+                everValidated(), 0L != getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index babc353..f2c6aa1 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -26,8 +26,9 @@
 
 import static com.android.net.module.util.CollectionUtils.filter;
 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
+import static com.android.server.connectivity.FullScore.POLICY_AVOIDED_WHEN_UNVALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
-import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
+import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
@@ -104,7 +105,9 @@
     }
 
     private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) {
-        return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
+        final FullScore score = candidate.getScore();
+        return score.hasPolicy(POLICY_EVER_VALIDATED)
+                && !score.hasPolicy(POLICY_AVOIDED_WHEN_UNVALIDATED)
                 && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
     }
 
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a6179fc..23cb15c 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -61,7 +61,9 @@
     // uncomment when b/13249961 is fixed
     // sdk_version: "current",
     platform_apis: true,
-    required: ["ConnectivityChecker"],
+    data: [":ConnectivityChecker"],
+    per_testcase_directory: true,
+    host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate.xml",
 }
 
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 3821cea..0c53411 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -200,13 +200,13 @@
             super(data);
 
             // Check QR field.(query (0), or a response (1)).
-            if ((mHeader.getFlags() & (1 << 15)) == 0) {
+            if ((mHeader.flags & (1 << 15)) == 0) {
                 throw new DnsParseException("Not an answer packet");
             }
         }
 
         int getRcode() {
-            return mHeader.getFlags() & 0x0F;
+            return mHeader.rcode;
         }
 
         int getANCount() {
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 15a2e56..eb5d2ef 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -30,6 +30,7 @@
 import static android.net.INetd.PERMISSION_NONE;
 import static android.net.INetd.PERMISSION_UNINSTALLED;
 import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
+import static android.system.OsConstants.EINVAL;
 import static android.system.OsConstants.EPERM;
 
 import static com.android.server.BpfNetMaps.DOZABLE_MATCH;
@@ -40,6 +41,7 @@
 import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
 import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
 import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
+import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -47,13 +49,18 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.app.StatsManager;
 import android.content.Context;
 import android.net.INetd;
 import android.os.Build;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
 
 import androidx.test.filters.SmallTest;
 
@@ -61,6 +68,8 @@
 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.net.module.util.bpf.CookieTagMapKey;
+import com.android.net.module.util.bpf.CookieTagMapValue;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -74,6 +83,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -117,6 +127,8 @@
     private final IBpfMap<U32, UidOwnerValue> mUidOwnerMap =
             new TestBpfMap<>(U32.class, UidOwnerValue.class);
     private final IBpfMap<U32, U8> mUidPermissionMap = new TestBpfMap<>(U32.class, U8.class);
+    private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
+            spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
 
     @Before
     public void setUp() throws Exception {
@@ -127,6 +139,7 @@
         BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
         BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
         BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+        BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
         mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
     }
 
@@ -877,4 +890,40 @@
 
         assertThrows(ServiceSpecificException.class, () -> mBpfNetMaps.swapActiveStatsMap());
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testPullBpfMapInfo() throws Exception {
+        // mCookieTagMap has 1 entry
+        mCookieTagMap.updateEntry(new CookieTagMapKey(0), new CookieTagMapValue(0, 0));
+
+        // mUidOwnerMap has 2 entries
+        mUidOwnerMap.updateEntry(new U32(0), new UidOwnerValue(0, 0));
+        mUidOwnerMap.updateEntry(new U32(1), new UidOwnerValue(0, 0));
+
+        // mUidPermissionMap has 3 entries
+        mUidPermissionMap.updateEntry(new U32(0), new U8((short) 0));
+        mUidPermissionMap.updateEntry(new U32(1), new U8((short) 0));
+        mUidPermissionMap.updateEntry(new U32(2), new U8((short) 0));
+
+        final int ret = mBpfNetMaps.pullBpfMapInfoAtom(NETWORK_BPF_MAP_INFO, new ArrayList<>());
+        assertEquals(StatsManager.PULL_SUCCESS, ret);
+        verify(mDeps).buildStatsEvent(
+                1 /* cookieTagMapSize */, 2 /* uidOwnerMapSize */, 3 /* uidPermissionMapSize */);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testPullBpfMapInfoGetMapSizeFailure() throws Exception {
+        doThrow(new ErrnoException("", EINVAL)).when(mCookieTagMap).forEach(any());
+        final int ret = mBpfNetMaps.pullBpfMapInfoAtom(NETWORK_BPF_MAP_INFO, new ArrayList<>());
+        assertEquals(StatsManager.PULL_SKIP, ret);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testPullBpfMapInfoUnexpectedAtomTag() {
+        final int ret = mBpfNetMaps.pullBpfMapInfoAtom(-1 /* atomTag */, new ArrayList<>());
+        assertEquals(StatsManager.PULL_SKIP, ret);
+    }
 }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e23e72d..85aec56 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -15594,11 +15594,19 @@
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
         mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
 
-        // In this test the automotive feature will be enabled.
-        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+        // Has automotive feature.
+        validateAutomotiveEthernetAllowedUids(true);
+
+        // No automotive feature.
+        validateAutomotiveEthernetAllowedUids(false);
+    }
+
+    private void validateAutomotiveEthernetAllowedUids(final boolean hasAutomotiveFeature)
+            throws Exception {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, hasAutomotiveFeature);
 
         // Simulate a restricted ethernet network.
-        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+        final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_ETHERNET)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
@@ -15606,8 +15614,34 @@
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
 
         mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET,
-                new LinkProperties(), agentNetCaps.build());
-        validateAllowedUids(mEthernetNetworkAgent, TRANSPORT_ETHERNET, agentNetCaps, true);
+                new LinkProperties(), ncb.build());
+
+        final ArraySet<Integer> serviceUidSet = new ArraySet<>();
+        serviceUidSet.add(TEST_PACKAGE_UID);
+
+        final TestNetworkCallback cb = new TestNetworkCallback();
+
+        mCm.requestNetwork(new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_ETHERNET)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .build(), cb);
+        mEthernetNetworkAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+
+        // Cell gets to set the service UID as access UID
+        ncb.setAllowedUids(serviceUidSet);
+        mEthernetNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        if (SdkLevel.isAtLeastT() && hasAutomotiveFeature) {
+            cb.expectCapabilitiesThat(mEthernetNetworkAgent,
+                    caps -> caps.getAllowedUids().equals(serviceUidSet));
+        } else {
+            // S and no automotive feature must ignore access UIDs.
+            cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
+        }
+
+        mEthernetNetworkAgent.disconnect();
+        cb.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        mCm.unregisterNetworkCallback(cb);
     }
 
     @Test
@@ -15621,7 +15655,7 @@
 
         // Simulate a restricted telephony network. The telephony factory is entitled to set
         // the access UID to the service package on any of its restricted networks.
-        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+        final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
@@ -15630,13 +15664,8 @@
                 .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
 
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
-                new LinkProperties(), agentNetCaps.build());
-        validateAllowedUids(mCellNetworkAgent, TRANSPORT_CELLULAR, agentNetCaps, false);
-    }
+                new LinkProperties(), ncb.build());
 
-    private void validateAllowedUids(final TestNetworkAgentWrapper testAgent,
-            @NetworkCapabilities.Transport final int transportUnderTest,
-            final NetworkCapabilities.Builder ncb, final boolean forAutomotive) throws Exception {
         final ArraySet<Integer> serviceUidSet = new ArraySet<>();
         serviceUidSet.add(TEST_PACKAGE_UID);
         final ArraySet<Integer> nonServiceUidSet = new ArraySet<>();
@@ -15647,34 +15676,28 @@
 
         final TestNetworkCallback cb = new TestNetworkCallback();
 
-        /* Test setting UIDs */
         // Cell gets to set the service UID as access UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(transportUnderTest)
+                .addTransportType(TRANSPORT_CELLULAR)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        testAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(testAgent);
+        mCellNetworkAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
         ncb.setAllowedUids(serviceUidSet);
-        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(testAgent,
+            cb.expectCapabilitiesThat(mCellNetworkAgent,
                     caps -> caps.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
-        /* Test setting UIDs is rejected when expected */
-        if (forAutomotive) {
-            mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
-        }
-
         // ...but not to some other UID. Rejection sets UIDs to the empty set
         ncb.setAllowedUids(nonServiceUidSet);
-        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(testAgent,
+            cb.expectCapabilitiesThat(mCellNetworkAgent,
                     caps -> caps.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
@@ -15683,18 +15706,18 @@
 
         // ...and also not to multiple UIDs even including the service UID
         ncb.setAllowedUids(serviceUidSetPlus);
-        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
 
-        testAgent.disconnect();
-        cb.expectCallback(CallbackEntry.LOST, testAgent);
+        mCellNetworkAgent.disconnect();
+        cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         mCm.unregisterNetworkCallback(cb);
 
         // Must be unset before touching the transports, because remove and add transport types
         // check the specifier on the builder immediately, contradicting normal builder semantics
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
-        ncb.removeTransportType(transportUnderTest);
+        ncb.removeTransportType(TRANSPORT_CELLULAR);
         ncb.addTransportType(TRANSPORT_WIFI);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index a194131..c8a62be 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -66,7 +66,8 @@
             if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
             if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
         }.build()
-        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */, destroyed)
+        return mixInScore(nc, nac, validated, false /* avoidUnvalidated */,
+                false /* yieldToBadWifi */, destroyed)
     }
 
     private val TAG = this::class.simpleName
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 01249a1..0d371fa 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -30,6 +31,7 @@
 
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityResources;
@@ -85,12 +87,14 @@
     @Mock NetworkNotificationManager mNotifier;
     @Mock Resources mResources;
     @Mock QosCallbackTracker mQosCallbackTracker;
+    @Mock PackageManager mPackageManager;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mCtx.getResources()).thenReturn(mResources);
         when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity");
+        doReturn(mPackageManager).when(mCtx).getPackageManager();
         ConnectivityResources.setResourcesContextForTest(mCtx);
 
         mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT);
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 6f9f430..8b1c510 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -25,7 +25,8 @@
 import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI
 import android.os.Build
 import androidx.test.filters.SmallTest
-import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD
+import com.android.server.connectivity.FullScore.POLICY_AVOIDED_WHEN_UNVALIDATED
+import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED
 import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -61,8 +62,7 @@
     @Test
     fun testYieldToBadWiFiOneCellOneBadWiFi() {
         // Bad wifi wins against yielding validated cell
-        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
-                caps(TRANSPORT_WIFI))
+        val winner = TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
                 TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
@@ -72,14 +72,26 @@
     }
 
     @Test
+    fun testYieldToBadWifiAvoidUnvalidated() {
+        // Bad wifi avoided when unvalidated loses against yielding validated cell
+        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
+        val scores = listOf(
+                winner,
+                TestScore(score(POLICY_EVER_VALIDATED, POLICY_AVOIDED_WHEN_UNVALIDATED),
+                        caps(TRANSPORT_WIFI))
+        )
+        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
+    }
+
+    @Test
     fun testYieldToBadWiFiOneCellTwoBadWiFi() {
         // Bad wifi wins against yielding validated cell. Prefer the one that's primary.
-        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
+        val winner = TestScore(score(POLICY_EVER_VALIDATED,
                 POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
-                        caps(TRANSPORT_WIFI)),
+                TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI)),
                 TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
@@ -90,8 +102,7 @@
     fun testYieldToBadWiFiOneCellTwoBadWiFiOneNotAvoided() {
         // Bad wifi ever validated wins against bad wifi that never was validated (or was
         // avoided when bad).
-        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
-                caps(TRANSPORT_WIFI))
+        val winner = TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
                 TestScore(score(), caps(TRANSPORT_WIFI)),
@@ -104,12 +115,12 @@
     @Test
     fun testYieldToBadWiFiOneCellOneBadWiFiOneGoodWiFi() {
         // Good wifi wins
-        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
-                POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
+        val winner = TestScore(score(POLICY_EVER_VALIDATED, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
-                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
+                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                        caps(TRANSPORT_WIFI)),
                 TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
@@ -122,8 +133,8 @@
         val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
-                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
+                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                        caps(TRANSPORT_WIFI)),
                 TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
@@ -136,8 +147,8 @@
         val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
-                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
+                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                        caps(TRANSPORT_WIFI)),
                 TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)),
                 TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
@@ -164,8 +175,7 @@
                 caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
-                        POLICY_EXITING), caps(TRANSPORT_WIFI))
+                TestScore(score(POLICY_EVER_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
index c6852d1..83e6b5f 100644
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -16,7 +16,14 @@
 
 package com.android.server.net;
 
+import static android.system.OsConstants.EPERM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -27,6 +34,8 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
 
 import androidx.test.filters.SmallTest;
 
@@ -36,6 +45,7 @@
 import com.android.net.module.util.Struct.U32;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,6 +54,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -56,7 +69,8 @@
     private final TestLooper mLooper = new TestLooper();
     private BaseNetdUnsolicitedEventListener mListener;
     private BpfInterfaceMapUpdater mUpdater;
-    @Mock private IBpfMap<U32, InterfaceMapValue> mBpfMap;
+    private IBpfMap<U32, InterfaceMapValue> mBpfMap =
+            spy(new TestBpfMap<>(U32.class, InterfaceMapValue.class));
     @Mock private INetd mNetd;
     @Mock private Context mContext;
 
@@ -118,4 +132,43 @@
         mLooper.dispatchAll();
         verifyNoMoreInteractions(mBpfMap);
     }
+
+    @Test
+    public void testGetIfNameByIndex() throws Exception {
+        mBpfMap.updateEntry(new U32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+        assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    @Test
+    public void testGetIfNameByIndexNoEntry() {
+        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    @Test
+    public void testGetIfNameByIndexException() throws Exception {
+        doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new U32(TEST_INDEX));
+        assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+    }
+
+    private void assertDumpContains(final String dump, final String message) {
+        assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
+                dump.contains(message));
+    }
+
+    private String getDump() {
+        final StringWriter sw = new StringWriter();
+        mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
+        return sw.toString();
+    }
+
+    @Test
+    public void testDump() throws ErrnoException {
+        mBpfMap.updateEntry(new U32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+        mBpfMap.updateEntry(new U32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
+
+        final String dump = getDump();
+        assertDumpContains(dump, "IfaceIndexNameMap: OK");
+        assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
+        assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/InterfaceMapValueTest.java b/tests/unit/java/com/android/server/net/InterfaceMapValueTest.java
new file mode 100644
index 0000000..ee13d5f
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/InterfaceMapValueTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class InterfaceMapValueTest {
+    private static final String IF_NAME = "wlan0";
+    private static final byte[] IF_NAME_BYTE = new byte[]{'w', 'l', 'a', 'n', '0'};
+    private static final byte[] IF_NAME_BYTE_WITH_PADDING =
+            new byte[]{'w', 'l', 'a', 'n', '0', 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0}; // IF_NAME_BYTE_WITH_PADDING.length = 16
+    private static final byte[] IF_NAME_BYTE_LONG =
+            new byte[]{'w', 'l', 'a', 'n', '0', 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0}; // IF_NAME_BYTE_LONG.length = 24
+
+    @Test
+    public void testInterfaceMapValueFromString() {
+        final InterfaceMapValue value = new InterfaceMapValue(IF_NAME);
+        assertArrayEquals(IF_NAME_BYTE_WITH_PADDING, value.interfaceName);
+    }
+
+    @Test
+    public void testInterfaceMapValueFromByte() {
+        final InterfaceMapValue value = new InterfaceMapValue(IF_NAME_BYTE_WITH_PADDING);
+        assertArrayEquals(IF_NAME_BYTE_WITH_PADDING, value.interfaceName);
+    }
+
+    @Test
+    public void testInterfaceMapValueFromByteShort() {
+        final InterfaceMapValue value = new InterfaceMapValue(IF_NAME_BYTE);
+        assertArrayEquals(IF_NAME_BYTE_WITH_PADDING, value.interfaceName);
+    }
+
+    @Test
+    public void testInterfaceMapValueFromByteLong() {
+        final InterfaceMapValue value = new InterfaceMapValue(IF_NAME_BYTE_LONG);
+        assertArrayEquals(IF_NAME_BYTE_WITH_PADDING, value.interfaceName);
+    }
+
+    @Test
+    public void testGetInterfaceNameString() {
+        final InterfaceMapValue value = new InterfaceMapValue(IF_NAME_BYTE_WITH_PADDING);
+        assertEquals(IF_NAME, value.getInterfaceNameString());
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index e053252..fdbccba 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -143,6 +143,7 @@
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.server.BpfNetMaps;
 import com.android.server.net.NetworkStatsService.AlertObserver;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
@@ -249,6 +250,10 @@
     @Mock
     private LocationPermissionChecker mLocationPermissionChecker;
     private TestBpfMap<U32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(U32.class, U8.class));
+    @Mock
+    private BpfNetMaps mBpfNetMaps;
+    @Mock
+    private SkDestroyListener mSkDestroyListener;
 
     private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
             CookieTagMapKey.class, CookieTagMapValue.class);
@@ -501,6 +506,17 @@
             public boolean isDebuggable() {
                 return mIsDebuggable == Boolean.TRUE;
             }
+
+            @Override
+            public BpfNetMaps makeBpfNetMaps(Context ctx) {
+                return mBpfNetMaps;
+            }
+
+            @Override
+            public SkDestroyListener makeSkDestroyListener(
+                    IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+                return mSkDestroyListener;
+            }
         };
     }
 
@@ -2512,4 +2528,28 @@
         assertDumpContains(dump, "uid rxBytes rxPackets txBytes txPackets");
         assertDumpContains(dump, "1002 10000 10 6000 6");
     }
+
+    private void doTestDumpStatsMap(final String expectedIfaceName) throws ErrnoException {
+        initBpfMapsWithTagData(UID_BLUE);
+
+        final String dump = getDump();
+        assertDumpContains(dump, "mStatsMapA: OK");
+        assertDumpContains(dump, "mStatsMapB: OK");
+        assertDumpContains(dump,
+                "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets");
+        assertDumpContains(dump, "10 " + expectedIfaceName + " 0x2 1002 0 5000 5 3000 3");
+        assertDumpContains(dump, "10 " + expectedIfaceName + " 0x1 1002 0 5000 5 3000 3");
+    }
+
+    @Test
+    public void testDumpStatsMap() throws ErrnoException {
+        doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doTestDumpStatsMap("wlan0");
+    }
+
+    @Test
+    public void testDumpStatsMapUnknownInterface() throws ErrnoException {
+        doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+        doTestDumpStatsMap("unknown");
+    }
 }