Merge changes from topic "idiag_sock_id"

* changes:
  Switch SkDestroyListener based on the flag.
  Add startSkDestroyListener flag to native_init
  Add SkDestroyListenerTest
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1e8babf..6e30fd1 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -205,6 +205,9 @@
     },
     {
       "path": "packages/modules/CaptivePortalLogin"
+    },
+    {
+      "path": "vendor/xts/gts-tests/hostsidetests/networkstack"
     }
   ]
 }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 880a285..c6d2b3b 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -139,6 +139,9 @@
     // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
     // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
     private static final int UDP_STREAM_TS_MS = 2000;
+    // Give slack time for waiting UDP stream mode because handling conntrack event in user space
+    // may not in precise time. Used to reduce the flaky rate.
+    private static final int UDP_STREAM_SLACK_MS = 500;
     // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
     private static final int RX_UDP_PACKET_SIZE = 30;
     private static final int RX_UDP_PACKET_COUNT = 456;
@@ -1171,6 +1174,9 @@
             Thread.sleep(UDP_STREAM_TS_MS);
             sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
 
+            // Give a slack time for handling conntrack event in user space.
+            Thread.sleep(UDP_STREAM_SLACK_MS);
+
             // [1] Verify IPv4 upstream rule map.
             final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
                     Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
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/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/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 f80d7e8..8d5c881 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -2497,13 +2497,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
     }
 
     /**
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/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
index fdca468..b24dee0 100644
--- a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
@@ -22,7 +22,7 @@
     <string name="network_available_sign_in" msgid="2622520134876355561">"Acceder a la red"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
-    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>no tiene acceso a Internet"</string>
+    <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Presiona para ver opciones"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"La red móvil no tiene acceso a Internet"</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"La red no tiene acceso a Internet"</string>
diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml
index 00a0728..4439048 100644
--- a/service/ServiceConnectivityResources/res/values-nb/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml
@@ -34,7 +34,7 @@
     <string name="network_switch_metered_toast" msgid="70691146054130335">"Byttet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
   <string-array name="network_switch_type_name">
     <item msgid="3004933964374161223">"mobildata"</item>
-    <item msgid="5624324321165953608">"Wi-Fi"</item>
+    <item msgid="5624324321165953608">"Wifi"</item>
     <item msgid="5667906231066981731">"Bluetooth"</item>
     <item msgid="346574747471703768">"Ethernet"</item>
     <item msgid="5734728378097476003">"VPN"</item>
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 19c5a27..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();
+        }
     }
 
     /**
@@ -251,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. */
@@ -826,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
      *
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 359a3bd..7e63761 100755
--- 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
@@ -3039,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);
+        }
     }
 
     /**
@@ -3531,7 +3537,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 +3566,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 +3696,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 +3745,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 +3800,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 +3811,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 +3846,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 +4199,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 +4209,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 +4314,7 @@
         }
 
         // Delayed teardown.
-        if (nai.created) {
+        if (nai.isCreated()) {
             try {
                 mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
             } catch (RemoteException e) {
@@ -4333,7 +4335,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 +4396,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 +4529,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;
         }
@@ -4582,7 +4584,7 @@
             if (req.isListen() || req.isListenForBest()) {
                 continue;
             }
-            // If this Network is already the highest scoring Network for a request, or if
+            // If this Network is already the best Network for a request, or if
             // there is hope for it to become one if it validated, then it is needed.
             if (candidate.satisfies(req)) {
                 // As soon as a network is found that satisfies a request, return. Specifically for
@@ -4859,7 +4861,7 @@
             return;
         }
 
-        if (nai.everValidated) {
+        if (nai.everValidated()) {
             // The network validated while the dialog box was up. Take no action.
             return;
         }
@@ -4904,7 +4906,7 @@
             return;
         }
 
-        if (nai.lastValidated) {
+        if (nai.isValidated()) {
             // The network validated while the dialog box was up. Take no action.
             return;
         }
@@ -4936,12 +4938,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 +5093,7 @@
         pw.println("Network overrides:");
         pw.increaseIndent();
         for (NetworkAgentInfo nai : networksSortedById()) {
-            if (nai.avoidUnvalidated) {
+            if (0L != nai.getAvoidUnvalidated()) {
                 pw.println(nai.toShortString());
             }
         }
@@ -5162,7 +5164,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 +5172,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 +5205,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 +5559,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 +5573,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);
@@ -7750,7 +7752,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) {
@@ -7840,9 +7842,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.
@@ -7854,12 +7856,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);
@@ -7869,7 +7871,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);
@@ -8116,7 +8118,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();
         }
@@ -8236,8 +8238,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.
@@ -8669,7 +8671,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
@@ -8937,7 +8939,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);
             }
@@ -8958,7 +8960,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);
             }
         }
@@ -9084,12 +9086,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;
 
@@ -9116,7 +9118,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(),
@@ -9141,7 +9143,7 @@
                     + oldInfo.getState() + " to " + state);
         }
 
-        if (!networkAgent.created
+        if (!networkAgent.isCreated()
                 && (state == NetworkInfo.State.CONNECTED
                 || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
 
@@ -9155,13 +9157,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.
@@ -9245,8 +9247,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);
         }
     }
@@ -9674,7 +9676,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/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/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 88a5f9c..654d195 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;
@@ -819,8 +970,8 @@
             @NonNull final NetworkCapabilities nc) {
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
-                yieldToBadWiFi(), destroyed);
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidated(),
+                0L != getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -983,13 +1134,14 @@
 
     // Does this network satisfy request?
     public boolean satisfies(NetworkRequest request) {
-        return everConnected
+        return everConnected()
                 && request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities);
     }
 
     public boolean satisfiesImmutableCapabilitiesOf(NetworkRequest request) {
-        return everConnected && request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
-                networkCapabilities);
+        return everConnected()
+                && request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
+                        networkCapabilities);
     }
 
     /** Whether this network is a VPN. */
@@ -1022,7 +1174,7 @@
      */
     public void setScore(final NetworkScore score) {
         mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), destroyed);
+                everValidated(), 0L == getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
     }
 
     /**
@@ -1032,11 +1184,7 @@
      */
     public void updateScoreForNetworkAgentUpdate() {
         mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
-                everValidatedForYield(), yieldToBadWiFi(), destroyed);
-    }
-
-    private boolean everValidatedForYield() {
-        return everValidated && !avoidUnvalidated;
+                everValidated(), 0L != getAvoidUnvalidated(), yieldToBadWiFi(), isDestroyed());
     }
 
     /**
@@ -1323,14 +1471,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" : "")
@@ -1348,7 +1499,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/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/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 7842eec..deca6a2 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -117,6 +117,7 @@
             return false;
         }
         if (mDataSaverSupported == null) {
+            setRestrictBackgroundInternal(false);
             assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
             try {
                 setRestrictBackgroundInternal(true);
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/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index a02be85..d598830 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -65,6 +65,7 @@
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
+import com.android.testutils.waitForIdle
 import org.junit.After
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertFalse
@@ -121,6 +122,7 @@
             cm.unregisterNetworkCallback(requestCb)
             agent.unregister()
             iface.fileDescriptor.close()
+            agent.waitForIdle(TIMEOUT_MS)
         }
     }
 
@@ -291,7 +293,7 @@
         val agent = registerTestNetworkAgent(iface.interfaceName)
         val network = agent.network ?: fail("Registered agent should have a network")
         // The network has no INTERNET capability, so will be marked validated immediately
-        cb.expectAvailableThenValidatedCallbacks(network)
+        cb.expectAvailableThenValidatedCallbacks(network, TIMEOUT_MS)
         return TestTapNetwork(iface, cb, agent, network)
     }
 
@@ -319,6 +321,7 @@
                 testNetwork2.close(cm)
             }
         }
+        handlerThread.waitForIdle(TIMEOUT_MS)
         handlerThread.quitSafely()
     }
 
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index f035f72..9d1fa60 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -357,7 +357,7 @@
 
     public Network connectToCell() throws InterruptedException {
         if (cellConnectAttempted()) {
-            throw new IllegalStateException("Already connected");
+            mCm.unregisterNetworkCallback(mCellNetworkCallback);
         }
         NetworkRequest cellRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 5cb014f..2f5b0ab 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -471,6 +471,23 @@
                 new Ikev2VpnProfile.Builder(tunnelParams2).build());
     }
 
+    @Test
+    public void testBuildProfileWithNullProxy() throws Exception {
+        final Ikev2VpnProfile ikev2VpnProfile =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                        .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
+                        .build();
+
+        // ProxyInfo should be null for the profile without setting ProxyInfo.
+        assertNull(ikev2VpnProfile.getProxyInfo());
+
+        // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
+        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
+        assertNull(vpnProfile.proxy);
+
+        final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
+        assertNull(convertedIkev2VpnProfile.getProxyInfo());
+    }
 
     private static class CertificateAndKey {
         public final X509Certificate cert;
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/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/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/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/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index 082a016..ea3d392 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -87,8 +87,8 @@
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         initMockResources();
-        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean())).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,
@@ -101,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() {
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 d560dc2..e27e4e2 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -86,8 +86,8 @@
 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;
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
@@ -177,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;
@@ -347,9 +348,9 @@
 
         final Context context = InstrumentationRegistry.getContext();
         mServiceContext = new MockContext(context);
-        when(mLocationPermissionChecker.checkCallersLocationPermission(
-                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
-        when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
+        doReturn(true).when(mLocationPermissionChecker).checkCallersLocationPermission(
+                any(), any(), anyInt(), anyBoolean(), any());
+        doReturn(TEST_WIFI_NETWORK_KEY).when(sWifiInfo).getNetworkKey();
         mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
         mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
                 getClass().getSimpleName() + "-legacy");
@@ -1093,8 +1094,8 @@
 
     // TODO: support per IMSI state
     private void setMobileRatTypeAndWaitForIdle(int ratType) {
-        when(mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(anyString()))
-                .thenReturn(ratType);
+        doReturn(ratType).when(mNetworkStatsSubscriptionsMonitor)
+                .getRatTypeForSubscriberId(anyString());
         mService.handleOnCollapsedRatTypeChanged();
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
     }
@@ -1538,8 +1539,8 @@
         mUsageCallback.expectOnThresholdReached(request);
 
         // Allow binder to disconnect
-        when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()))
-                .thenReturn(true);
+        doReturn(true).when(mUsageCallbackBinder)
+                .unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         // Unregister request
         mService.unregisterUsageRequest(request);
@@ -1721,7 +1722,7 @@
     }
 
     private void setCombineSubtypeEnabled(boolean enable) {
-        when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable);
+        doReturn(enable).when(mSettings).getCombineSubtypeEnabled();
         mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
                     .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED)));
         waitForIdle();
@@ -1881,8 +1882,8 @@
      */
     @Test
     public void testEnforceTemplateLocationPermission() throws Exception {
-        when(mLocationPermissionChecker.checkCallersLocationPermission(
-                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(false);
+        doReturn(false).when(mLocationPermissionChecker)
+                .checkCallersLocationPermission(any(), any(), anyInt(), anyBoolean(), any());
         initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1));
         assertThrows(SecurityException.class, () ->
                 assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0));
@@ -1890,8 +1891,8 @@
         assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
 
-        when(mLocationPermissionChecker.checkCallersLocationPermission(
-                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
+        doReturn(true).when(mLocationPermissionChecker)
+                .checkCallersLocationPermission(any(), any(), anyInt(), anyBoolean(), any());
         assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
         assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
@@ -2087,6 +2088,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());
@@ -2166,11 +2220,11 @@
     }
 
     private void mockNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
-        when(mStatsFactory.readNetworkStatsSummaryDev()).thenReturn(summary);
+        doReturn(summary).when(mStatsFactory).readNetworkStatsSummaryDev();
     }
 
     private void mockNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
-        when(mStatsFactory.readNetworkStatsSummaryXt()).thenReturn(summary);
+        doReturn(summary).when(mStatsFactory).readNetworkStatsSummaryXt();
     }
 
     private void mockNetworkStatsUidDetail(NetworkStats detail) throws Exception {
@@ -2180,11 +2234,11 @@
 
     private void mockNetworkStatsUidDetail(NetworkStats detail,
             TetherStatsParcel[] tetherStatsParcels) throws Exception {
-        when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL))
-                .thenReturn(detail);
+        doReturn(detail).when(mStatsFactory)
+                .readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
 
         // also include tethering details, since they are folded into UID
-        when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
+        doReturn(tetherStatsParcels).when(mNetd).tetherGetStats();
     }
 
     private void mockDefaultSettings() throws Exception {
@@ -2192,22 +2246,22 @@
     }
 
     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);
-        when(mSettings.getCombineSubtypeEnabled()).thenReturn(false);
+        doReturn(HOUR_IN_MILLIS).when(mSettings).getPollInterval();
+        doReturn(0L).when(mSettings).getPollDelay();
+        doReturn(true).when(mSettings).getSampleEnabled();
+        doReturn(false).when(mSettings).getCombineSubtypeEnabled();
 
         final Config config = new Config(bucketDuration, deleteAge, deleteAge);
-        when(mSettings.getDevConfig()).thenReturn(config);
-        when(mSettings.getXtConfig()).thenReturn(config);
-        when(mSettings.getUidConfig()).thenReturn(config);
-        when(mSettings.getUidTagConfig()).thenReturn(config);
+        doReturn(config).when(mSettings).getDevConfig();
+        doReturn(config).when(mSettings).getXtConfig();
+        doReturn(config).when(mSettings).getUidConfig();
+        doReturn(config).when(mSettings).getUidTagConfig();
 
-        when(mSettings.getGlobalAlertBytes(anyLong())).thenReturn(MB_IN_BYTES);
-        when(mSettings.getDevPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
-        when(mSettings.getXtPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
-        when(mSettings.getUidPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
-        when(mSettings.getUidTagPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        doReturn(MB_IN_BYTES).when(mSettings).getGlobalAlertBytes(anyLong());
+        doReturn(MB_IN_BYTES).when(mSettings).getDevPersistBytes(anyLong());
+        doReturn(MB_IN_BYTES).when(mSettings).getXtPersistBytes(anyLong());
+        doReturn(MB_IN_BYTES).when(mSettings).getUidPersistBytes(anyLong());
+        doReturn(MB_IN_BYTES).when(mSettings).getUidTagPersistBytes(anyLong());
     }
 
     private void assertStatsFilesExist(boolean exist) {