Merge "Send a proxy broadcast when apps moved from/to a VPN"
diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp
index 289d75d..5b00dfe 100644
--- a/Tethering/bpf_progs/Android.bp
+++ b/Tethering/bpf_progs/Android.bp
@@ -48,10 +48,6 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        // TODO: get rid of system/netd.
-        "system/netd/bpf_progs",             // for bpf_net_helpers.h
-    ],
 }
 
 bpf {
@@ -61,8 +57,4 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        // TODO: get rid of system/netd.
-        "system/netd/bpf_progs",             // for bpf_net_helpers.h
-    ],
 }
diff --git a/Tethering/bpf_progs/bpf_net_helpers.h b/Tethering/bpf_progs/bpf_net_helpers.h
new file mode 100644
index 0000000..c798580
--- /dev/null
+++ b/Tethering/bpf_progs/bpf_net_helpers.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <linux/bpf.h>
+#include <linux/if_packet.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+// this returns 0 iff skb->sk is NULL
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
+
+static uint32_t (*bpf_get_socket_uid)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_uid;
+
+static int (*bpf_skb_pull_data)(struct __sk_buff* skb, __u32 len) = (void*)BPF_FUNC_skb_pull_data;
+
+static int (*bpf_skb_load_bytes)(struct __sk_buff* skb, int off, void* to,
+                                 int len) = (void*)BPF_FUNC_skb_load_bytes;
+
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+
+static int64_t (*bpf_csum_diff)(__be32* from, __u32 from_size, __be32* to, __u32 to_size,
+                                __wsum seed) = (void*)BPF_FUNC_csum_diff;
+
+static int64_t (*bpf_csum_update)(struct __sk_buff* skb, __wsum csum) = (void*)BPF_FUNC_csum_update;
+
+static int (*bpf_skb_change_proto)(struct __sk_buff* skb, __be16 proto,
+                                   __u64 flags) = (void*)BPF_FUNC_skb_change_proto;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+static int (*bpf_l4_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l4_csum_replace;
+static int (*bpf_redirect)(__u32 ifindex, __u64 flags) = (void*)BPF_FUNC_redirect;
+static int (*bpf_redirect_map)(const struct bpf_map_def* map, __u32 key,
+                               __u64 flags) = (void*)BPF_FUNC_redirect_map;
+
+static int (*bpf_skb_change_head)(struct __sk_buff* skb, __u32 head_room,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_change_head;
+static int (*bpf_skb_adjust_room)(struct __sk_buff* skb, __s32 len_diff, __u32 mode,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_adjust_room;
+
+// Android only supports little endian architectures
+#define htons(x) (__builtin_constant_p(x) ? ___constant_swab16(x) : __builtin_bswap16(x))
+#define htonl(x) (__builtin_constant_p(x) ? ___constant_swab32(x) : __builtin_bswap32(x))
+#define ntohs(x) htons(x)
+#define ntohl(x) htonl(x)
+
+static inline __always_inline __unused bool is_received_skb(struct __sk_buff* skb) {
+    return skb->pkt_type == PACKET_HOST || skb->pkt_type == PACKET_BROADCAST ||
+           skb->pkt_type == PACKET_MULTICAST;
+}
+
+// try to make the first 'len' header bytes readable via direct packet access
+static inline __always_inline void try_make_readable(struct __sk_buff* skb, int len) {
+    if (len > skb->len) len = skb->len;
+    if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
+}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 4bc8f93..fce4360 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -25,8 +25,8 @@
     ],
 
     srcs: [":framework-tethering-srcs"],
-    libs: ["framework-connectivity"],
-    stub_only_libs: ["framework-connectivity"],
+    libs: ["framework-connectivity.stubs.module_lib"],
+    stub_only_libs: ["framework-connectivity.stubs.module_lib"],
     aidl: {
         include_dirs: [
             "packages/modules/Connectivity/framework/aidl-export",
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c45ce83..3428c1d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1054,8 +1054,16 @@
         mLastRaParams = newParams;
     }
 
-    private void logMessage(State state, int what) {
-        mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+    private void maybeLogMessage(State state, int what) {
+        switch (what) {
+            // Suppress some CMD_* to avoid log flooding.
+            case CMD_IPV6_TETHER_UPDATE:
+            case CMD_NEIGHBOR_EVENT:
+                break;
+            default:
+                mLog.log(state.getName() + " got "
+                        + sMagicDecoderRing.get(what, Integer.toString(what)));
+        }
     }
 
     private void sendInterfaceState(int newInterfaceState) {
@@ -1095,7 +1103,7 @@
 
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
@@ -1180,7 +1188,6 @@
 
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
@@ -1238,7 +1245,7 @@
         public boolean processMessage(Message message) {
             if (super.processMessage(message)) return true;
 
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode.");
@@ -1306,7 +1313,7 @@
         public boolean processMessage(Message message) {
             if (super.processMessage(message)) return true;
 
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLog.e("CMD_TETHER_REQUESTED while already tethering.");
@@ -1417,7 +1424,7 @@
     class WaitingForRestartState extends State {
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 8adcbd9..36a2f10 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -48,6 +48,7 @@
 import android.net.util.SharedLog;
 import android.net.util.TetheringUtils.ForwardedStats;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.text.TextUtils;
 import android.util.Log;
@@ -773,6 +774,12 @@
         }
         pw.decreaseIndent();
 
+        pw.println("BPF stats:");
+        pw.increaseIndent();
+        dumpBpfStats(pw);
+        pw.decreaseIndent();
+        pw.println();
+
         pw.println("Forwarding rules:");
         pw.increaseIndent();
         dumpIpv6UpstreamRules(pw);
@@ -800,6 +807,22 @@
                     upstreamIfindex), stats.toString()));
         }
     }
+    private void dumpBpfStats(@NonNull IndentingPrintWriter pw) {
+        try (BpfMap<TetherStatsKey, TetherStatsValue> map = mDeps.getBpfStatsMap()) {
+            if (map == null) {
+                pw.println("No BPF stats map");
+                return;
+            }
+            if (map.isEmpty()) {
+                pw.println("<empty>");
+            }
+            map.forEach((k, v) -> {
+                pw.println(String.format("%s: %s", k, v));
+            });
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF stats map: " + e);
+        }
+    }
 
     private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) {
         if (mIpv6ForwardingRules.size() == 0) {
@@ -849,7 +872,7 @@
         }
     }
 
-    private String ipv4RuleToString(Tether4Key key, Tether4Value value) {
+    private String ipv4RuleToString(long now, Tether4Key key, Tether4Value value) {
         final String private4, public4, dst4;
         try {
             private4 = InetAddress.getByAddress(key.src4).getHostAddress();
@@ -858,29 +881,43 @@
         } catch (UnknownHostException impossible) {
             throw new AssertionError("4-byte array not valid IPv4 address!");
         }
-        return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d",
+        long ageMs = (now - value.lastUsed) / 1_000_000;
+        return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d %dms",
                 key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort,
                 value.oif, getIfName(value.oif),
-                public4, value.srcPort, dst4, key.dstPort);
+                public4, value.srcPort, dst4, key.dstPort, ageMs);
+    }
+
+    private void dumpIpv4ForwardingRuleMap(long now, BpfMap<Tether4Key, Tether4Value> map,
+            IndentingPrintWriter pw) throws ErrnoException {
+        if (map == null) {
+            pw.println("No IPv4 support");
+            return;
+        }
+        if (map.isEmpty()) {
+            pw.println("No rules");
+            return;
+        }
+        map.forEach((k, v) -> pw.println(ipv4RuleToString(now, k, v)));
     }
 
     private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) {
-        try (BpfMap<Tether4Key, Tether4Value> map = mDeps.getBpfUpstream4Map()) {
-            if (map == null) {
-                pw.println("No IPv4 support");
-                return;
-            }
-            if (map.isEmpty()) {
-                pw.println("No IPv4 rules");
-                return;
-            }
-            pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst");
+        final long now = SystemClock.elapsedRealtimeNanos();
+
+        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map();
+                BpfMap<Tether4Key, Tether4Value> downstreamMap = mDeps.getBpfDownstream4Map()) {
+            pw.println("IPv4 Upstream: [inDstMac] iif(iface) src -> nat -> dst");
             pw.increaseIndent();
-            map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v)));
+            dumpIpv4ForwardingRuleMap(now, upstreamMap, pw);
+            pw.decreaseIndent();
+
+            pw.println("IPv4 Downstream: [inDstMac] iif(iface) src -> nat -> dst");
+            pw.increaseIndent();
+            dumpIpv4ForwardingRuleMap(now, downstreamMap, pw);
+            pw.decreaseIndent();
         } catch (ErrnoException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
-        pw.decreaseIndent();
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 7596380..e4fbd71 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -495,11 +495,11 @@
         // See NetlinkHandler.cpp: notifyInterfaceChanged.
         if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
         if (up) {
-            maybeTrackNewInterfaceLocked(iface);
+            maybeTrackNewInterface(iface);
         } else {
             if (ifaceNameToType(iface) == TETHERING_BLUETOOTH
                     || ifaceNameToType(iface) == TETHERING_WIGIG) {
-                stopTrackingInterfaceLocked(iface);
+                stopTrackingInterface(iface);
             } else {
                 // Ignore usb0 down after enabling RNDIS.
                 // We will handle disconnect in interfaceRemoved.
@@ -535,12 +535,12 @@
 
     void interfaceAdded(String iface) {
         if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
-        maybeTrackNewInterfaceLocked(iface);
+        maybeTrackNewInterface(iface);
     }
 
     void interfaceRemoved(String iface) {
         if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
-        stopTrackingInterfaceLocked(iface);
+        stopTrackingInterface(iface);
     }
 
     void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
@@ -694,14 +694,14 @@
             mEthernetCallback = new EthernetCallback();
             mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback);
         } else {
-            stopEthernetTetheringLocked();
+            stopEthernetTethering();
         }
         return TETHER_ERROR_NO_ERROR;
     }
 
-    private void stopEthernetTetheringLocked() {
+    private void stopEthernetTethering() {
         if (mConfiguredEthernetIface != null) {
-            stopTrackingInterfaceLocked(mConfiguredEthernetIface);
+            stopTrackingInterface(mConfiguredEthernetIface);
             mConfiguredEthernetIface = null;
         }
         if (mEthernetCallback != null) {
@@ -718,7 +718,7 @@
                 // Ethernet callback arrived after Ethernet tethering stopped. Ignore.
                 return;
             }
-            maybeTrackNewInterfaceLocked(iface, TETHERING_ETHERNET);
+            maybeTrackNewInterface(iface, TETHERING_ETHERNET);
             changeInterfaceState(iface, getRequestedState(TETHERING_ETHERNET));
             mConfiguredEthernetIface = iface;
         }
@@ -729,7 +729,7 @@
                 // onAvailable called after stopping Ethernet tethering.
                 return;
             }
-            stopEthernetTetheringLocked();
+            stopEthernetTethering();
         }
     }
 
@@ -1030,7 +1030,7 @@
                     // We can see this state on the way to both enabled and failure states.
                     break;
                 case WifiManager.WIFI_AP_STATE_ENABLED:
-                    enableWifiIpServingLocked(ifname, ipmode);
+                    enableWifiIpServing(ifname, ipmode);
                     break;
                 case WifiManager.WIFI_AP_STATE_DISABLING:
                     // We can see this state on the way to disabled.
@@ -1038,7 +1038,7 @@
                 case WifiManager.WIFI_AP_STATE_DISABLED:
                 case WifiManager.WIFI_AP_STATE_FAILED:
                 default:
-                    disableWifiIpServingLocked(ifname, curState);
+                    disableWifiIpServing(ifname, curState);
                     break;
             }
         }
@@ -1062,7 +1062,7 @@
 
             // if no group is formed, bring it down if needed.
             if (p2pInfo == null || !p2pInfo.groupFormed) {
-                disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
+                disableWifiP2pIpServingIfNeeded(mWifiP2pTetherInterface);
                 mWifiP2pTetherInterface = null;
                 return;
             }
@@ -1078,12 +1078,12 @@
                 mLog.w("P2P tethered interface " + mWifiP2pTetherInterface
                         + "is different from current interface "
                         + group.getInterface() + ", re-tether it");
-                disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
+                disableWifiP2pIpServingIfNeeded(mWifiP2pTetherInterface);
             }
 
             // Finally bring up serving on the new interface
             mWifiP2pTetherInterface = group.getInterface();
-            enableWifiIpServingLocked(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+            enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
         }
 
         private void handleUserRestrictionAction() {
@@ -1164,7 +1164,7 @@
         }
     }
 
-    private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) {
+    private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
         mLog.log("Canceling WiFi tethering request -"
                 + " type=" + tetheringType
                 + " interface=" + ifname
@@ -1191,23 +1191,23 @@
                                            : "specified interface: " + ifname));
     }
 
-    private void disableWifiIpServingLocked(String ifname, int apState) {
+    private void disableWifiIpServing(String ifname, int apState) {
         // Regardless of whether we requested this transition, the AP has gone
         // down.  Don't try to tether again unless we're requested to do so.
         // TODO: Remove this altogether, once Wi-Fi reliably gives us an
         // interface name with every broadcast.
         mWifiTetherRequested = false;
 
-        disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState);
+        disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
     }
 
-    private void disableWifiP2pIpServingLockedIfNeeded(String ifname) {
+    private void disableWifiP2pIpServingIfNeeded(String ifname) {
         if (TextUtils.isEmpty(ifname)) return;
 
-        disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
     }
 
-    private void enableWifiIpServingLocked(String ifname, int wifiIpMode) {
+    private void enableWifiIpServing(String ifname, int wifiIpMode) {
         // Map wifiIpMode values to IpServer.Callback serving states, inferring
         // from mWifiTetherRequested as a final "best guess".
         final int ipServingMode;
@@ -1224,7 +1224,7 @@
         }
 
         if (!TextUtils.isEmpty(ifname)) {
-            maybeTrackNewInterfaceLocked(ifname);
+            maybeTrackNewInterface(ifname);
             changeInterfaceState(ifname, ipServingMode);
         } else {
             mLog.e(String.format(
@@ -2437,7 +2437,7 @@
         mTetherMainSM.sendMessage(which, state, 0, newLp);
     }
 
-    private void maybeTrackNewInterfaceLocked(final String iface) {
+    private void maybeTrackNewInterface(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
         if (interfaceType == TETHERING_INVALID) {
@@ -2457,10 +2457,10 @@
             return;
         }
 
-        maybeTrackNewInterfaceLocked(iface, interfaceType);
+        maybeTrackNewInterface(iface, interfaceType);
     }
 
-    private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) {
+    private void maybeTrackNewInterface(final String iface, int interfaceType) {
         // If we have already started a TISM for this interface, skip.
         if (mTetherStates.containsKey(iface)) {
             mLog.log("active iface (" + iface + ") reported as added, ignoring");
@@ -2477,7 +2477,7 @@
         tetherState.ipServer.start();
     }
 
-    private void stopTrackingInterfaceLocked(final String iface) {
+    private void stopTrackingInterface(final String iface) {
         final TetherState tetherState = mTetherStates.get(iface);
         if (tetherState == null) {
             mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index cd27318..69471a1 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -398,10 +398,10 @@
             // notifications (e.g. matching more than one of our callbacks).
             //
             // Also, it can happen that onLinkPropertiesChanged is called after
-            // onLost removed the state from mNetworkMap. This appears to be due
-            // to a bug in disconnectAndDestroyNetwork, which calls
-            // nai.clatd.update() after the onLost callbacks.
-            // TODO: fix the bug and make this method void.
+            // onLost removed the state from mNetworkMap. This is due to a bug
+            // in disconnectAndDestroyNetwork, which calls nai.clatd.update()
+            // after the onLost callbacks. This was fixed in S.
+            // TODO: make this method void when R is no longer supported.
             return null;
         }
 
diff --git a/framework/Android.bp b/framework/Android.bp
index 4fa9ccb..93ef3bf 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -22,6 +22,7 @@
 java_library {
     name: "framework-connectivity-protos",
     sdk_version: "module_current",
+    min_sdk_version: "30",
     proto: {
         type: "nano",
     },
@@ -77,11 +78,13 @@
 
 java_sdk_library {
     name: "framework-connectivity",
-    api_only: true,
+    sdk_version: "module_current",
+    min_sdk_version: "30",
     defaults: ["framework-module-defaults"],
     installable: true,
     srcs: [
         ":framework-connectivity-sources",
+        ":net-utils-framework-common-srcs",
     ],
     aidl: {
         include_dirs: [
@@ -93,10 +96,43 @@
             "frameworks/native/aidl/binder", // For PersistableBundle.aidl
         ],
     },
+    impl_only_libs: [
+        // TODO (b/183097033) remove once module_current includes core_platform
+        "stable.core.platform.api.stubs",
+        "framework-tethering.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
+        "net-utils-device-common",
+    ],
     libs: [
         "unsupportedappusage",
     ],
+    static_libs: [
+        "framework-connectivity-protos",
+    ],
+    jarjar_rules: "jarjar-rules.txt",
     permitted_packages: ["android.net"],
+    impl_library_visibility: [
+        "//packages/modules/Connectivity/Tethering/apex",
+        // In preparation for future move
+        "//packages/modules/Connectivity/apex",
+        "//packages/modules/Connectivity/service",
+        "//frameworks/base/packages/Connectivity/service",
+        "//frameworks/base",
+
+        // Tests using hidden APIs
+        "//external/sl4a:__subpackages__",
+        "//frameworks/base/tests/net:__subpackages__",
+        "//frameworks/libs/net/common/testutils",
+        "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/opt/telephony/tests/telephonytests",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/NetworkStack/tests:__subpackages__",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
 }
 
 cc_defaults {
@@ -105,6 +141,10 @@
         "-Wall",
         "-Werror",
         "-Wno-unused-parameter",
+        // Don't warn about S API usage even with
+        // min_sdk 30: the library is only loaded
+        // on S+ devices
+        "-Wno-unguarded-availability",
         "-Wthread-safety",
     ],
     shared_libs: [
@@ -131,6 +171,7 @@
 
 cc_library_shared {
     name: "libframework-connectivity-jni",
+    min_sdk_version: "30",
     defaults: ["libframework-connectivity-defaults"],
     srcs: [
         "jni/android_net_NetworkUtils.cpp",
@@ -139,36 +180,6 @@
     shared_libs: ["libandroid"],
     stl: "libc++_static",
     apex_available: [
-        "//apex_available:platform",
         "com.android.tethering",
     ],
 }
-
-java_library {
-    name: "framework-connectivity.impl",
-    sdk_version: "module_current",
-    srcs: [
-        ":framework-connectivity-sources",
-    ],
-    aidl: {
-        include_dirs: [
-            "frameworks/base/core/java", // For framework parcelables
-            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
-        ],
-    },
-    libs: [
-        // TODO (b/183097033) remove once module_current includes core_current
-        "stable.core.platform.api.stubs",
-        "framework-tethering",
-        "framework-wifi",
-        "unsupportedappusage",
-    ],
-    static_libs: [
-        "framework-connectivity-protos",
-        "net-utils-device-common",
-    ],
-    jarjar_rules: "jarjar-rules.txt",
-    apex_available: ["com.android.tethering"],
-    installable: true,
-    permitted_packages: ["android.net"],
-}
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 27bf114..d1d51da 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -232,12 +232,14 @@
     method @NonNull public android.net.Network register();
     method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
     method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+    method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
     method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
     method public final void sendQosCallbackError(int, int);
     method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
     method public final void sendQosSessionLost(int, int, int);
     method public final void sendSocketKeepaliveEvent(int, int);
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
+    method public void setLingerDuration(@NonNull java.time.Duration);
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
     method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
@@ -316,9 +318,16 @@
     method public int getProviderId();
     method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest);
     method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback);
     field public static final int ID_NONE = -1; // 0xffffffff
   }
 
+  public static interface NetworkProvider.NetworkOfferCallback {
+    method public void onNetworkNeeded(@NonNull android.net.NetworkRequest);
+    method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest);
+  }
+
   public class NetworkReleasedException extends java.lang.Exception {
   }
 
@@ -334,15 +343,23 @@
 
   public final class NetworkScore implements android.os.Parcelable {
     method public int describeContents();
+    method public int getKeepConnectedReason();
     method public int getLegacyInt();
+    method public boolean isExiting();
+    method public boolean isTransportPrimary();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
+    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
+    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
   }
 
   public static final class NetworkScore.Builder {
     ctor public NetworkScore.Builder();
     method @NonNull public android.net.NetworkScore build();
+    method @NonNull public android.net.NetworkScore.Builder setExiting(boolean);
+    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
     method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
+    method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean);
   }
 
   public final class OemNetworkPreferences implements android.os.Parcelable {
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
new file mode 100644
index 0000000..099202f
--- /dev/null
+++ b/framework/lint-baseline.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.ParseException`"
+        errorLine1="                ParseException pe = new ParseException(e.reason, e.getCause());"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
+            line="301"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback`"
+        errorLine1="    protected class ActiveDataSubscriptionIdListener extends TelephonyCallback"
+        errorLine2="                                                             ~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="96"
+            column="62"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener`"
+        errorLine1="            implements TelephonyCallback.ActiveDataSubscriptionIdListener {"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="97"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#registerTelephonyCallback`"
+        errorLine1="        ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback("
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="126"
+            column="54"/>
+    </issue>
+
+</issues>
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a64a9e6..20f3853 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -3360,7 +3360,7 @@
      * @param score The prospective score of the network.
      * @param caps The prospective capabilities of the network.
      * @param callback The callback to call when this offer is needed or unneeded.
-     * @hide
+     * @hide exposed via the NetworkProvider class.
      */
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -3383,7 +3383,7 @@
      *
      * @param callback The callback passed at registration time. This must be the same object
      *                 that was passed to {@link #offerNetwork}
-     * @hide
+     * @hide exposed via the NetworkProvider class.
      */
     public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
         try {
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 5f7f539..03c3600 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -1070,7 +1070,7 @@
      */
     @NonNull
     public static Set<Integer> getUidsAllowedOnRestrictedNetworks(@NonNull Context context) {
-        final String uidList = Settings.Secure.getString(
+        final String uidList = Settings.Global.getString(
                 context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS);
         return getUidSetFromString(uidList);
     }
@@ -1084,7 +1084,7 @@
     public static void setUidsAllowedOnRestrictedNetworks(@NonNull Context context,
             @NonNull Set<Integer> uidList) {
         final String uids = getUidStringFromSet(uidList);
-        Settings.Secure.putString(context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS,
+        Settings.Global.putString(context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS,
                 uids);
     }
 }
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 26cb1ed..9a58add 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -42,4 +42,5 @@
     void sendQosSessionLost(int qosCallbackId, in QosSession session);
     void sendQosCallbackError(int qosCallbackId, int exceptionType);
     void sendTeardownDelayMs(int teardownDelayMs);
+    void sendLingerDuration(int durationMs);
 }
diff --git a/framework/src/android/net/INetworkOfferCallback.aidl b/framework/src/android/net/INetworkOfferCallback.aidl
index a6de173..ecfba21 100644
--- a/framework/src/android/net/INetworkOfferCallback.aidl
+++ b/framework/src/android/net/INetworkOfferCallback.aidl
@@ -51,10 +51,8 @@
     /**
      * Called when a network for this offer is needed to fulfill this request.
      * @param networkRequest the request to satisfy
-     * @param providerId the ID of the provider currently satisfying
-     *          this request, or NetworkProvider.ID_NONE if none.
      */
-    void onNetworkNeeded(in NetworkRequest networkRequest, int providerId);
+    void onNetworkNeeded(in NetworkRequest networkRequest);
 
     /**
      * Informs the registrant that the offer is no longer valuable to fulfill this request.
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index f65acdd..adcf338 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.os.Build;
@@ -106,6 +107,9 @@
     private final String LOG_TAG;
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
+    /** @hide */
+    @TestApi
+    public static final int MIN_LINGER_TIMER_MS = 2000;
     private final ArrayList<RegistryAction> mPreConnectedQueue = new ArrayList<>();
     private volatile long mLastBwRefreshTime = 0;
     private static final long BW_REFRESH_MIN_WIN_MS = 500;
@@ -391,6 +395,15 @@
      */
     public static final int CMD_NETWORK_DESTROYED = BASE + 23;
 
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to set the linger duration for this network
+     * agent.
+     * arg1 = the linger duration, represents by {@link Duration}.
+     *
+     * @hide
+     */
+    public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24;
+
     private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
         final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
                 config.legacyTypeName, config.legacySubTypeName);
@@ -956,7 +969,6 @@
      * Must be called by the agent to update the score of this network.
      *
      * @param score the new score.
-     * @hide TODO : unhide when impl is complete
      */
     public final void sendNetworkScore(@NonNull NetworkScore score) {
         Objects.requireNonNull(score);
@@ -1288,6 +1300,22 @@
         queueOrSendMessage(ra -> ra.sendQosCallbackError(qosCallbackId, exceptionType));
     }
 
+    /**
+     * Set the linger duration for this network agent.
+     * @param duration the delay between the moment the network becomes unneeded and the
+     *                 moment the network is disconnected or moved into the background.
+     *                 Note that If this duration has greater than millisecond precision, then
+     *                 the internal implementation will drop any excess precision.
+     */
+    public void setLingerDuration(@NonNull final Duration duration) {
+        Objects.requireNonNull(duration);
+        final long durationMs = duration.toMillis();
+        if (durationMs < MIN_LINGER_TIMER_MS || durationMs > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Duration must be within ["
+                    + MIN_LINGER_TIMER_MS + "," + Integer.MAX_VALUE + "]ms");
+        }
+        queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
+    }
 
     /** @hide */
     protected void log(final String s) {
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index be9426e..3a07db3 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -817,6 +817,7 @@
         final int originalOwnerUid = getOwnerUid();
         final int[] originalAdministratorUids = getAdministratorUids();
         final TransportInfo originalTransportInfo = getTransportInfo();
+        final Set<Integer> originalSubIds = getSubscriptionIds();
         clearAll();
         if (0 != (originalCapabilities & NET_CAPABILITY_NOT_RESTRICTED)) {
             // If the test network is not restricted, then it is only allowed to declare some
@@ -825,6 +826,9 @@
             mTransportTypes =
                     (originalTransportTypes & UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS)
                             | (1 << TRANSPORT_TEST);
+
+            // SubIds are only allowed for Test Networks that only declare TRANSPORT_TEST.
+            setSubscriptionIds(originalSubIds);
         } else {
             // If the test transport is restricted, then it may declare any transport.
             mTransportTypes = (originalTransportTypes | (1 << TRANSPORT_TEST));
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
index d859022..0665af5 100644
--- a/framework/src/android/net/NetworkProvider.java
+++ b/framework/src/android/net/NetworkProvider.java
@@ -167,13 +167,25 @@
         ConnectivityManager.from(mContext).declareNetworkRequestUnfulfillable(request);
     }
 
-    /** @hide */
-    // TODO : make @SystemApi when the impl is complete
+    /**
+     * A callback for parties registering a NetworkOffer.
+     *
+     * This is used with {@link ConnectivityManager#offerNetwork}. When offering a network,
+     * the system will use this callback to inform the caller that a network corresponding to
+     * this offer is needed or unneeded.
+     *
+     * @hide
+     */
+    @SystemApi
     public interface NetworkOfferCallback {
-        /** Called by the system when a network for this offer is needed to satisfy some
-         *  networking request. */
-        void onNetworkNeeded(@NonNull NetworkRequest request, int providerId);
-        /** Called by the system when this offer is no longer valuable for this request. */
+        /**
+         * Called by the system when a network for this offer is needed to satisfy some
+         * networking request.
+         */
+        void onNetworkNeeded(@NonNull NetworkRequest request);
+        /**
+         * Called by the system when this offer is no longer valuable for this request.
+         */
         void onNetworkUnneeded(@NonNull NetworkRequest request);
     }
 
@@ -188,9 +200,8 @@
         }
 
         @Override
-        public void onNetworkNeeded(final @NonNull NetworkRequest request,
-                final int providerId) {
-            mExecutor.execute(() -> callback.onNetworkNeeded(request, providerId));
+        public void onNetworkNeeded(final @NonNull NetworkRequest request) {
+            mExecutor.execute(() -> callback.onNetworkNeeded(request));
         }
 
         @Override
@@ -254,8 +265,11 @@
      *
      * The capabilities and score act as filters as to what requests the provider will see.
      * They are not promises, but for best performance, the providers should strive to put
-     * as much known information as possible in the offer. For capabilities in particular, it
-     * should put all NetworkAgent-managed capabilities a network may have, even if it doesn't
+     * as much known information as possible in the offer. For the score, it should put as
+     * strong a score as the networks will have, since this will filter what requests the
+     * provider sees – it's not a promise, it only serves to avoid sending requests that
+     * the provider can't ever hope to satisfy better than any current network. For capabilities,
+     * it should put all NetworkAgent-managed capabilities a network may have, even if it doesn't
      * have them at first. This applies to INTERNET, for example ; if a provider thinks the
      * network it can bring up for this offer may offer Internet access it should include the
      * INTERNET bit. It's fine if the brought up network ends up not actually having INTERNET.
@@ -268,9 +282,9 @@
      *
      * @hide
      */
-    // TODO : make @SystemApi when the impl is complete
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
-    public void offerNetwork(@NonNull final NetworkScore score,
+    public void registerNetworkOffer(@NonNull final NetworkScore score,
             @NonNull final NetworkCapabilities caps, @NonNull final Executor executor,
             @NonNull final NetworkOfferCallback callback) {
         // Can't offer a network with a provider that is not yet registered or already unregistered.
@@ -307,9 +321,9 @@
      *
      * @hide
      */
-    // TODO : make @SystemApi when the impl is complete
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
-    public void unofferNetwork(final @NonNull NetworkOfferCallback callback) {
+    public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) {
         final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
         if (null == proxy) return;
         mProxies.remove(proxy);
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 6584993..7be7deb 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcel;
@@ -23,6 +24,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Object representing the quality of a network as perceived by the user.
  *
@@ -36,32 +40,82 @@
     // a migration.
     private final int mLegacyInt;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            KEEP_CONNECTED_NONE,
+            KEEP_CONNECTED_FOR_HANDOVER
+    })
+    public @interface KeepConnectedReason { }
+
+    /**
+     * Do not keep this network connected if there is no outstanding request for it.
+     */
+    public static final int KEEP_CONNECTED_NONE = 0;
+    /**
+     * Keep this network connected even if there is no outstanding request for it, because it
+     * is being considered for handover.
+     */
+    public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+
     // Agent-managed policies
-    // TODO : add them here, starting from 1
+    // This network should lose to a wifi that has ever been validated
+    // NOTE : temporarily this policy is managed by ConnectivityService, because of legacy. The
+    // legacy design has this bit global to the system and tacked on WiFi which means it will affect
+    // networks from carriers who don't want it and non-carrier networks, which is bad for users.
+    // The S design has this on mobile networks only, so this can be fixed eventually ; as CS
+    // doesn't know what carriers need this bit, the initial S implementation will continue to
+    // affect other carriers but will at least leave non-mobile networks alone. Eventually Telephony
+    // should set this on networks from carriers that require it.
     /** @hide */
-    public static final int MIN_AGENT_MANAGED_POLICY = 0;
+    public static final int POLICY_YIELD_TO_BAD_WIFI = 1;
+    // This network is primary for this transport.
     /** @hide */
-    public static final int MAX_AGENT_MANAGED_POLICY = -1;
+    public static final int POLICY_TRANSPORT_PRIMARY = 2;
+    // This network is exiting : it will likely disconnect in a few seconds.
+    /** @hide */
+    public static final int POLICY_EXITING = 3;
+
+    /** @hide */
+    public static final int MIN_AGENT_MANAGED_POLICY = POLICY_YIELD_TO_BAD_WIFI;
+    /** @hide */
+    public static final int MAX_AGENT_MANAGED_POLICY = POLICY_EXITING;
 
     // Bitmask of all the policies applied to this score.
     private final long mPolicies;
 
+    private final int mKeepConnectedReason;
+
     /** @hide */
-    NetworkScore(final int legacyInt, final long policies) {
+    NetworkScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
         mLegacyInt = legacyInt;
         mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
     }
 
     private NetworkScore(@NonNull final Parcel in) {
         mLegacyInt = in.readInt();
         mPolicies = in.readLong();
+        mKeepConnectedReason = in.readInt();
     }
 
+    /**
+     * Get the legacy int score embedded in this NetworkScore.
+     * @see Builder#setLegacyInt(int)
+     */
     public int getLegacyInt() {
         return mLegacyInt;
     }
 
     /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
+    /**
      * @return whether this score has a particular policy.
      *
      * @hide
@@ -71,15 +125,70 @@
         return 0 != (mPolicies & (1L << policy));
     }
 
+    /**
+     * To the exclusive usage of FullScore
+     * @hide
+     */
+    public long getPolicies() {
+        return mPolicies;
+    }
+
+    /**
+     * Whether this network should yield to a previously validated wifi gone bad.
+     *
+     * If this policy is set, other things being equal, the device will prefer a previously
+     * validated WiFi even if this network is validated and the WiFi is not.
+     * If this policy is not set, the device prefers the validated network.
+     *
+     * @hide
+     */
+    // TODO : Unhide this for telephony and have telephony call it on the relevant carriers.
+    // In the mean time this is handled by Connectivity in a backward-compatible manner.
+    public boolean shouldYieldToBadWifi() {
+        return hasPolicy(POLICY_YIELD_TO_BAD_WIFI);
+    }
+
+    /**
+     * Whether this network is primary for this transport.
+     *
+     * When multiple networks of the same transport are active, the device prefers the ones that
+     * are primary. This is meant in particular for DS-DA devices with a user setting to choose the
+     * default SIM card, or for WiFi STA+STA and make-before-break cases.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isTransportPrimary() {
+        return hasPolicy(POLICY_TRANSPORT_PRIMARY);
+    }
+
+    /**
+     * Whether this network is exiting.
+     *
+     * If this policy is set, the device will expect this network to disconnect within seconds.
+     * It will try to migrate to some other network if any is available, policy permitting, to
+     * avoid service disruption.
+     * This is useful in particular when a good cellular network is available and WiFi is getting
+     * weak and risks disconnecting soon. The WiFi network should be marked as exiting so that
+     * the device will prefer the reliable mobile network over this soon-to-be-lost WiFi.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isExiting() {
+        return hasPolicy(POLICY_EXITING);
+    }
+
     @Override
     public String toString() {
-        return "Score(" + mLegacyInt + ")";
+        return "Score(" + mLegacyInt + " ; Policies : " + mPolicies + ")";
     }
 
     @Override
     public void writeToParcel(@NonNull final Parcel dest, final int flags) {
         dest.writeInt(mLegacyInt);
         dest.writeLong(mPolicies);
+        dest.writeInt(mKeepConnectedReason);
     }
 
     @Override
@@ -108,11 +217,15 @@
         private static final long POLICY_NONE = 0L;
         private static final int INVALID_LEGACY_INT = Integer.MIN_VALUE;
         private int mLegacyInt = INVALID_LEGACY_INT;
+        private int mKeepConnectedReason = KEEP_CONNECTED_NONE;
+        private int mPolicies = 0;
 
         /**
          * Sets the legacy int for this score.
          *
-         * Do not rely on this. It will be gone by the time S is released.
+         * This will be used for measurements and logs, but will no longer be used for ranking
+         * networks against each other. Callers that existed before Android S should send what
+         * they used to send as the int score.
          *
          * @param score the legacy int
          * @return this
@@ -123,13 +236,93 @@
             return this;
         }
 
+
+        /**
+         * Set for a network that should never be preferred to a wifi that has ever been validated
+         *
+         * If this policy is set, other things being equal, the device will prefer a previously
+         * validated WiFi even if this network is validated and the WiFi is not.
+         * If this policy is not set, the device prefers the validated network.
+         *
+         * @return this builder
+         * @hide
+         */
+        // TODO : Unhide this for telephony and have telephony call it on the relevant carriers.
+        // In the mean time this is handled by Connectivity in a backward-compatible manner.
+        @NonNull
+        public Builder setShouldYieldToBadWifi(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_YIELD_TO_BAD_WIFI);
+            } else {
+                mPolicies &= ~(1L << POLICY_YIELD_TO_BAD_WIFI);
+            }
+            return this;
+        }
+
+        /**
+         * Set for a network that is primary for this transport.
+         *
+         * When multiple networks of the same transport are active, the device prefers the ones that
+         * are primary. This is meant in particular for DS-DA devices with a user setting to choose
+         * the default SIM card, or for WiFi STA+STA and make-before-break cases.
+         *
+         * @return this builder
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setTransportPrimary(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_TRANSPORT_PRIMARY);
+            } else {
+                mPolicies &= ~(1L << POLICY_TRANSPORT_PRIMARY);
+            }
+            return this;
+        }
+
+        /**
+         * Set for a network that will likely disconnect in a few seconds.
+         *
+         * If this policy is set, the device will expect this network to disconnect within seconds.
+         * It will try to migrate to some other network if any is available, policy permitting, to
+         * avoid service disruption.
+         * This is useful in particular when a good cellular network is available and WiFi is
+         * getting weak and risks disconnecting soon. The WiFi network should be marked as exiting
+         * so that the device will prefer the reliable mobile network over this soon-to-be-lost
+         * WiFi.
+         *
+         * @return this builder
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setExiting(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_EXITING);
+            } else {
+                mPolicies &= ~(1L << POLICY_EXITING);
+            }
+            return this;
+        }
+
+        /**
+         * Set the keep-connected reason.
+         *
+         * This can be reset by calling it again with {@link KEEP_CONNECTED_NONE}.
+         */
+        @NonNull
+        public Builder setKeepConnectedReason(@KeepConnectedReason final int reason) {
+            mKeepConnectedReason = reason;
+            return this;
+        }
+
         /**
          * Builds this NetworkScore.
          * @return The built NetworkScore object.
          */
         @NonNull
         public NetworkScore build() {
-            return new NetworkScore(mLegacyInt, POLICY_NONE);
+            return new NetworkScore(mLegacyInt, mPolicies, mKeepConnectedReason);
         }
     }
 }
diff --git a/service/Android.bp b/service/Android.bp
index 8c46dea..841e189 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -21,7 +21,7 @@
 
 cc_library_shared {
     name: "libservice-connectivity",
-    // TODO: build against the NDK (sdk_version: "30" for example)
+    min_sdk_version: "30",
     cflags: [
         "-Wall",
         "-Werror",
@@ -33,13 +33,12 @@
         "jni/onload.cpp",
     ],
     stl: "libc++_static",
+    header_libs: [
+        "libbase_headers",
+    ],
     shared_libs: [
-        "libbase",
         "liblog",
         "libnativehelper",
-        // TODO: remove dependency on ifc_[add/del]_address by having Java code to add/delete
-        // addresses, and remove dependency on libnetutils.
-        "libnetutils",
     ],
     apex_available: [
         "com.android.tethering",
@@ -48,33 +47,46 @@
 
 java_library {
     name: "service-connectivity-pre-jarjar",
+    sdk_version: "system_server_current",
+    min_sdk_version: "30",
     srcs: [
         "src/**/*.java",
         ":framework-connectivity-shared-srcs",
+        ":services-connectivity-shared-srcs",
+        // TODO: move to net-utils-device-common, enable shrink optimization to avoid extra classes
+        ":net-module-utils-srcs",
     ],
     libs: [
-        "android.net.ipsec.ike",
-        "services.core",
-        "services.net",
+        // TODO (b/183097033) remove once system_server_current includes core_current
+        "stable.core.platform.api.stubs",
+        "android_system_server_stubs_current",
+        "framework-annotations-lib",
+        "framework-connectivity.impl",
+        "framework-tethering.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
         "unsupportedappusage",
         "ServiceConnectivityResources",
     ],
     static_libs: [
+        "dnsresolver_aidl_interface-V7-java",
         "modules-utils-os",
         "net-utils-device-common",
         "net-utils-framework-common",
         "netd-client",
+        "netlink-client",
+        "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
     ],
     apex_available: [
-        "//apex_available:platform",
         "com.android.tethering",
     ],
 }
 
 java_library {
     name: "service-connectivity-protos",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
     proto: {
         type: "nano",
     },
@@ -83,20 +95,20 @@
     ],
     libs: ["libprotobuf-java-nano"],
     apex_available: [
-        "//apex_available:platform",
         "com.android.tethering",
     ],
 }
 
 java_library {
     name: "service-connectivity",
+    sdk_version: "system_server_current",
+    min_sdk_version: "30",
     installable: true,
     static_libs: [
         "service-connectivity-pre-jarjar",
     ],
     jarjar_rules: "jarjar-rules.txt",
     apex_available: [
-        "//apex_available:platform",
         "com.android.tethering",
     ],
 }
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 912d99f..f491cc7 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -22,6 +22,7 @@
 android_app {
     name: "ServiceConnectivityResources",
     sdk_version: "module_30",
+    min_sdk_version: "30",
     resource_dirs: [
         "res",
     ],
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 36a6fde..e7a40e5 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -35,8 +35,6 @@
 
 #include <log/log.h>
 
-#include "netutils/ifc.h"
-
 #include "jni.h"
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
@@ -48,9 +46,8 @@
 //------------------------------------------------------------------------------
 
 static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
-    const std::string& msg =
-        android::base::StringPrintf("Error %s %s: %s", action, iface, strerror(error));
-
+    const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) +  ": "
+                + std::string(strerror(error));
     jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
 }
 
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
new file mode 100644
index 0000000..df57c22
--- /dev/null
+++ b/service/lint-baseline.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#isDataCapable`"
+        errorLine1="            if (tm.isDataCapable()) {"
+        errorLine2="                   ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="791"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.content.Context#sendStickyBroadcast`"
+        errorLine1="                mUserAllContext.sendStickyBroadcast(intent, options);"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2693"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.content.pm.PackageManager#getTargetSdkVersion`"
+        errorLine1="            final int callingVersion = pm.getTargetSdkVersion(callingPackageName);"
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="5894"
+            column="43"/>
+    </issue>
+
+</issues>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0692d40..b9076e9 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -77,6 +77,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
 import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired;
 import static android.os.Process.INVALID_UID;
@@ -397,6 +398,32 @@
     }
 
     /**
+     * The priority value is used when issue uid ranges rules to netd. Netd will use the priority
+     * value and uid ranges to generate corresponding ip rules specific to the given preference.
+     * Thus, any device originated data traffic of the applied uids can be routed to the altered
+     * default network which has highest priority.
+     *
+     * Note: The priority value should be in 0~1000. Larger value means lower priority, see
+     *       {@link NativeUidRangeConfig}.
+     */
+    // This is default priority value for those NetworkRequests which doesn't have preference to
+    // alter default network and use the global one.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_NONE = 0;
+    // Used by automotive devices to set the network preferences used to direct traffic at an
+    // application level. See {@link #setOemNetworkPreference}.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_OEM = 10;
+    // Request that a user profile is put by default on a network matching a given preference.
+    // See {@link #setProfileNetworkPreference}.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_PROFILE = 20;
+    // Set by MOBILE_DATA_PREFERRED_UIDS setting. Use mobile data in preference even when
+    // higher-priority networks are connected.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED = 30;
+
+    /**
      * used internally to clear a wakelock when transitioning
      * from one net to another.  Clear happens when we get a new
      * network - EVENT_EXPIRE_NET_TRANSITION_WAKELOCK happens
@@ -617,6 +644,11 @@
     private static final int EVENT_UNREGISTER_NETWORK_OFFER = 53;
 
     /**
+     * Used internally when MOBILE_DATA_PREFERRED_UIDS setting changed.
+     */
+    private static final int EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED = 54;
+
+    /**
      * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
      * should be shown.
      */
@@ -1378,7 +1410,7 @@
         mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit);
 
         mMultinetworkPolicyTracker = mDeps.makeMultinetworkPolicyTracker(
-                mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
+                mContext, mHandler, () -> updateAvoidBadWifi());
         mMultinetworkPolicyTracker.start();
 
         mDnsManager = new DnsManager(mContext, mDnsResolver);
@@ -1394,8 +1426,8 @@
                 new NetworkInfo(TYPE_NONE, 0, "", ""),
                 new LinkProperties(), new NetworkCapabilities(),
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
-                new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, mQosCallbackTracker,
-                mDeps);
+                new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
+                mLingerDelayMs, mQosCallbackTracker, mDeps);
     }
 
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
@@ -1460,6 +1492,11 @@
         mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
     }
 
+    @VisibleForTesting
+    void updateMobileDataPreferredUids() {
+        mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
+    }
+
     private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, int id) {
         final boolean enable = mContext.getResources().getBoolean(id);
         handleAlwaysOnNetworkRequest(networkRequest, enable);
@@ -1504,6 +1541,8 @@
                 vehicleAlwaysRequested || legacyAlwaysRequested);
     }
 
+    // Note that registering observer for setting do not get initial callback when registering,
+    // callers must fetch the initial value of the setting themselves if needed.
     private void registerSettingsCallbacks() {
         // Watch for global HTTP proxy changes.
         mSettingsObserver.observe(
@@ -1519,6 +1558,11 @@
         mSettingsObserver.observe(
                 Settings.Global.getUriFor(ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED),
                 EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+
+        // Watch for mobile data preferred uids changes.
+        mSettingsObserver.observe(
+                Settings.Secure.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_PREFERRED_UIDS),
+                EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
     }
 
     private void registerPrivateDnsSettingsCallbacks() {
@@ -2205,7 +2249,7 @@
     @NonNull
     public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() {
         // This contains IMSI details, so make sure the caller is privileged.
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackOrSettingsPermission();
 
         final ArrayList<NetworkStateSnapshot> result = new ArrayList<>();
         for (Network network : getAllNetworks()) {
@@ -2717,6 +2761,13 @@
 
         // Create network requests for always-on networks.
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS));
+
+        // Update mobile data preference if necessary.
+        // Note that empty uid list can be skip here only because no uid rules applied before system
+        // ready. Normally, the empty uid list means to clear the uids rules on netd.
+        if (!ConnectivitySettingsManager.getMobileDataPreferredUids(mContext).isEmpty()) {
+            updateMobileDataPreferredUids();
+        }
     }
 
     /**
@@ -3209,6 +3260,11 @@
                     } else {
                         logwtf(nai.toShortString() + " set invalid teardown delay " + msg.arg1);
                     }
+                    break;
+                }
+                case NetworkAgent.EVENT_LINGER_DURATION_CHANGED: {
+                    nai.setLingerDuration((int) arg.second);
+                    break;
                 }
             }
         }
@@ -3335,8 +3391,6 @@
                 nai.lastValidated = valid;
                 nai.everValidated |= valid;
                 updateCapabilities(oldScore, nai, nai.networkCapabilities);
-                // If score has changed, rebroadcast to NetworkProviders. b/17726566
-                if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
                 if (valid) {
                     handleFreshlyValidatedNetwork(nai);
                     // Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and
@@ -3757,9 +3811,12 @@
             if (currentNetwork != null
                     && currentNetwork.network.getNetId() == nai.network.getNetId()) {
                 // uid rules for this network will be removed in destroyNativeNetwork(nai).
+                // TODO : setting the satisfier is in fact the job of the rematch. Teach the
+                // rematch not to keep disconnected agents instead of setting it here ; this
+                // will also allow removing updating the offers below.
                 nri.setSatisfier(null, null);
-                if (request.isRequest()) {
-                    sendUpdatedScoreToFactories(request, null);
+                for (final NetworkOfferInfo noi : mNetworkOffers) {
+                    informOffer(nri, noi.offer, mNetworkRanker);
                 }
 
                 if (mDefaultRequest == nri) {
@@ -3915,16 +3972,13 @@
         }
 
         rematchAllNetworksAndRequests();
-        for (final NetworkRequestInfo nri : nris) {
-            // If the nri is satisfied, return as its score has already been sent if needed.
-            if (nri.isBeingSatisfied()) {
-                return;
-            }
 
-            // As this request was not satisfied on rematch and thus never had any scores sent to
-            // the factories, send null now for each request of type REQUEST.
-            for (final NetworkRequest req : nri.mRequests) {
-                if (req.isRequest()) sendUpdatedScoreToFactories(req, null);
+        // Requests that have not been matched to a network will not have been sent to the
+        // providers, because the old satisfier and the new satisfier are the same (null in this
+        // case). Send these requests to the providers.
+        for (final NetworkRequestInfo nri : nris) {
+            for (final NetworkOfferInfo noi : mNetworkOffers) {
+                informOffer(nri, noi.offer, mNetworkRanker);
             }
         }
     }
@@ -3952,6 +4006,12 @@
     //   then it should be lingered.
     private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
         ensureRunningOnConnectivityServiceThread();
+
+        if (!nai.everConnected || nai.isVPN() || nai.isInactive()
+                || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) {
+            return false;
+        }
+
         final int numRequests;
         switch (reason) {
             case TEARDOWN:
@@ -3965,9 +4025,8 @@
                 return true;
         }
 
-        if (!nai.everConnected || nai.isVPN() || nai.isInactive() || numRequests > 0) {
-            return false;
-        }
+        if (numRequests > 0) return false;
+
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
             if (reason == UnneededFor.LINGER
                     && !nri.isMultilayerRequest()
@@ -4010,17 +4069,16 @@
                 // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request
                 // is important so as to not evaluate lower priority requests further in
                 // nri.mRequests.
-                final boolean isNetworkNeeded = candidate.isSatisfyingRequest(req.requestId)
-                        // Note that this catches two important cases:
-                        // 1. Unvalidated cellular will not be reaped when unvalidated WiFi
-                        //    is currently satisfying the request.  This is desirable when
-                        //    cellular ends up validating but WiFi does not.
-                        // 2. Unvalidated WiFi will not be reaped when validated cellular
-                        //    is currently satisfying the request.  This is desirable when
-                        //    WiFi ends up validating and out scoring cellular.
-                        || nri.getSatisfier().getCurrentScore()
-                        < candidate.getCurrentScoreAsValidated();
-                return isNetworkNeeded;
+                final NetworkAgentInfo champion = req.equals(nri.getActiveRequest())
+                        ? nri.getSatisfier() : null;
+                // Note that this catches two important cases:
+                // 1. Unvalidated cellular will not be reaped when unvalidated WiFi
+                //    is currently satisfying the request.  This is desirable when
+                //    cellular ends up validating but WiFi does not.
+                // 2. Unvalidated WiFi will not be reaped when validated cellular
+                //    is currently satisfying the request.  This is desirable when
+                //    WiFi ends up validating and out scoring cellular.
+                return mNetworkRanker.mightBeat(req, champion, candidate.getValidatedScoreable());
             }
         }
 
@@ -4114,8 +4172,10 @@
             final NetworkAgentInfo satisfier = nri.getSatisfier();
             if (null != satisfier) {
                 try {
+                    // TODO: Passing default network priority to netd.
                     mNetd.networkRemoveUidRanges(satisfier.network.getNetId(),
-                            toUidRangeStableParcels(nri.getUids()));
+                            toUidRangeStableParcels(nri.getUids())
+                            /* nri.getDefaultNetworkPriority() */);
                 } catch (RemoteException e) {
                     loge("Exception setting network preference default network", e);
                 }
@@ -4132,7 +4192,15 @@
             }
         }
 
-        cancelNpiRequests(nri);
+        // For all outstanding offers, cancel any of the layers of this NRI that used to be
+        // needed for this offer.
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            for (final NetworkRequest req : nri.mRequests) {
+                if (req.isRequest() && noi.offer.neededFor(req)) {
+                    noi.offer.onNetworkUnneeded(req);
+                }
+            }
+        }
     }
 
     private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
@@ -4145,20 +4213,6 @@
         }
     }
 
-    private void cancelNpiRequests(@NonNull final NetworkRequestInfo nri) {
-        for (final NetworkRequest req : nri.mRequests) {
-            cancelNpiRequest(req);
-        }
-    }
-
-    private void cancelNpiRequest(@NonNull final NetworkRequest req) {
-        if (req.isRequest()) {
-            for (final NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
-                npi.cancelRequest(req);
-            }
-        }
-    }
-
     private void removeListenRequestFromNetworks(@NonNull final NetworkRequest req) {
         // listens don't have a singular affected Network. Check all networks to see
         // if this listen request applies and remove it.
@@ -4283,9 +4337,8 @@
             // network, we should respect the user's option and don't need to popup the
             // PARTIAL_CONNECTIVITY notification to user again.
             nai.networkAgentConfig.acceptPartialConnectivity = accept;
-            nai.updateScoreForNetworkAgentConfigUpdate();
+            nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
-            sendUpdatedScoreToFactories(nai);
         }
 
         if (always) {
@@ -4352,8 +4405,8 @@
         }
         if (!nai.avoidUnvalidated) {
             nai.avoidUnvalidated = true;
+            nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
-            sendUpdatedScoreToFactories(nai);
         }
     }
 
@@ -4458,14 +4511,11 @@
         return avoidBadWifi();
     }
 
-
-    private void rematchForAvoidBadWifiUpdate() {
-        rematchAllNetworksAndRequests();
-        for (NetworkAgentInfo nai: mNetworkAgentInfos) {
-            if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
-                sendUpdatedScoreToFactories(nai);
-            }
+    private void updateAvoidBadWifi() {
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            nai.updateScoreForNetworkAgentUpdate();
         }
+        rematchAllNetworksAndRequests();
     }
 
     // TODO: Evaluate whether this is of interest to other consumers of
@@ -4801,6 +4851,9 @@
                 case EVENT_REPORT_NETWORK_ACTIVITY:
                     mNetworkActivityTracker.handleReportNetworkActivity();
                     break;
+                case EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED:
+                    handleMobileDataPreferredUidsChanged();
+                    break;
             }
         }
     }
@@ -5487,24 +5540,6 @@
             }
         }
 
-        void sendMessageToNetworkProvider(int what, int arg1, int arg2, Object obj) {
-            try {
-                messenger.send(Message.obtain(null /* handler */, what, arg1, arg2, obj));
-            } catch (RemoteException e) {
-                // Remote process died. Ignore; the death recipient will remove this
-                // NetworkProviderInfo from mNetworkProviderInfos.
-            }
-        }
-
-        void requestNetwork(NetworkRequest request, int score, int servingProviderId) {
-            sendMessageToNetworkProvider(NetworkProvider.CMD_REQUEST_NETWORK, score,
-                            servingProviderId, request);
-        }
-
-        void cancelRequest(NetworkRequest request) {
-            sendMessageToNetworkProvider(NetworkProvider.CMD_CANCEL_REQUEST, 0, 0, request);
-        }
-
         void connect(Context context, Handler handler) {
             try {
                 messenger.getBinder().linkToDeath(mDeathRecipient, 0);
@@ -5585,6 +5620,13 @@
         // maximum limit of registered callbacks per UID.
         final int mAsUid;
 
+        // Default network priority of this request.
+        private final int mDefaultNetworkPriority;
+
+        int getDefaultNetworkPriority() {
+            return mDefaultNetworkPriority;
+        }
+
         // In order to preserve the mapping of NetworkRequest-to-callback when apps register
         // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
         // maintained for keying off of. This is only a concern when the original nri
@@ -5605,7 +5647,7 @@
          * Get the list of UIDs this nri applies to.
          */
         @NonNull
-        private Set<UidRange> getUids() {
+        Set<UidRange> getUids() {
             // networkCapabilities.getUids() returns a defensive copy.
             // multilayer requests will all have the same uids so return the first one.
             final Set<UidRange> uids = mRequests.get(0).networkCapabilities.getUidRanges();
@@ -5614,12 +5656,13 @@
 
         NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r,
                 @Nullable final PendingIntent pi, @Nullable String callingAttributionTag) {
-            this(asUid, Collections.singletonList(r), r, pi, callingAttributionTag);
+            this(asUid, Collections.singletonList(r), r, pi, callingAttributionTag,
+                    DEFAULT_NETWORK_PRIORITY_NONE);
         }
 
         NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
                 @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
-                @Nullable String callingAttributionTag) {
+                @Nullable String callingAttributionTag, final int defaultNetworkPriority) {
             ensureAllNetworkRequestsHaveType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = requestForCallback;
@@ -5637,6 +5680,7 @@
              */
             mCallbackFlags = NetworkCallback.FLAG_NONE;
             mCallingAttributionTag = callingAttributionTag;
+            mDefaultNetworkPriority = defaultNetworkPriority;
         }
 
         NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
@@ -5666,6 +5710,7 @@
             mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = callbackFlags;
             mCallingAttributionTag = callingAttributionTag;
+            mDefaultNetworkPriority = DEFAULT_NETWORK_PRIORITY_NONE;
             linkDeathRecipient();
         }
 
@@ -5705,15 +5750,18 @@
             mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = nri.mCallbackFlags;
             mCallingAttributionTag = nri.mCallingAttributionTag;
+            mDefaultNetworkPriority = DEFAULT_NETWORK_PRIORITY_NONE;
             linkDeathRecipient();
         }
 
         NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r) {
-            this(asUid, Collections.singletonList(r));
+            this(asUid, Collections.singletonList(r), DEFAULT_NETWORK_PRIORITY_NONE);
         }
 
-        NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r) {
-            this(asUid, r, r.get(0), null /* pi */, null /* callingAttributionTag */);
+        NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+                final int defaultNetworkPriority) {
+            this(asUid, r, r.get(0), null /* pi */, null /* callingAttributionTag */,
+                    defaultNetworkPriority);
         }
 
         // True if this NRI is being satisfied. It also accounts for if the nri has its satisifer
@@ -6213,7 +6261,6 @@
         if (DBG) log("Got NetworkProvider Messenger for " + npi.name);
         mNetworkProviderInfos.put(npi.messenger, npi);
         npi.connect(mContext, mTrackerHandler);
-        sendAllRequestsToProvider(npi);
     }
 
     @Override
@@ -6236,6 +6283,9 @@
     public void offerNetwork(final int providerId,
             @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps,
             @NonNull final INetworkOfferCallback callback) {
+        Objects.requireNonNull(score);
+        Objects.requireNonNull(caps);
+        Objects.requireNonNull(callback);
         final NetworkOffer offer = new NetworkOffer(
                 FullScore.makeProspectiveScore(score, caps), caps, callback, providerId);
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_OFFER, offer));
@@ -6260,7 +6310,7 @@
                 toRemove.add(noi);
             }
         }
-        for (NetworkOfferInfo noi : toRemove) {
+        for (final NetworkOfferInfo noi : toRemove) {
             handleUnregisterNetworkOffer(noi);
         }
         if (DBG) log("unregisterNetworkProvider for " + npi.name);
@@ -6290,8 +6340,7 @@
     // there may not be a strict 1:1 correlation between the two.
     private final NetIdManager mNetIdManager;
 
-    // NetworkAgentInfo keyed off its connecting messenger
-    // TODO - eval if we can reduce the number of lists/hashmaps/sparsearrays
+    // Tracks all NetworkAgents that are currently registered.
     // NOTE: Only should be accessed on ConnectivityServiceThread, except dump().
     private final ArraySet<NetworkAgentInfo> mNetworkAgentInfos = new ArraySet<>();
 
@@ -6319,6 +6368,11 @@
     @NonNull
     private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences();
 
+    // A set of UIDs that should use mobile data preferentially if available. This object follows
+    // the same threading rules as the OEM network preferences above.
+    @NonNull
+    private Set<Integer> mMobileDataPreferredUids = new ArraySet<>();
+
     // OemNetworkPreferences activity String log entries.
     private static final int MAX_OEM_NETWORK_PREFERENCE_LOGS = 20;
     @NonNull
@@ -6588,7 +6642,8 @@
         final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                 new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc,
                 currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
-                this, mNetd, mDnsResolver, providerId, uid, mQosCallbackTracker, mDeps);
+                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
+                mQosCallbackTracker, mDeps);
 
         // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says.
         processCapabilitiesFromAgent(nai, nc);
@@ -6675,7 +6730,7 @@
             return;
         }
         mNetworkOffers.add(noi);
-        // TODO : send requests to the provider.
+        issueNetworkNeeds(noi);
     }
 
     private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
@@ -6688,7 +6743,7 @@
             @NonNull final INetworkOfferCallback callback) {
         ensureRunningOnConnectivityServiceThread();
         for (final NetworkOfferInfo noi : mNetworkOffers) {
-            if (noi.offer.callback.equals(callback)) return noi;
+            if (noi.offer.callback.asBinder().equals(callback.asBinder())) return noi;
         }
         return null;
     }
@@ -7246,6 +7301,7 @@
         final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc);
 
         updateUids(nai, prevNc, newNc);
+        nai.updateScoreForNetworkAgentUpdate();
 
         if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
             // If the requestable capabilities haven't changed, and the score hasn't changed, then
@@ -7364,9 +7420,13 @@
         maybeCloseSockets(nai, ranges, exemptUids);
         try {
             if (add) {
-                mNetd.networkAddUidRanges(nai.network.netId, ranges);
+                // TODO: Passing default network priority to netd.
+                mNetd.networkAddUidRanges(nai.network.netId, ranges
+                        /* DEFAULT_NETWORK_PRIORITY_NONE */);
             } else {
-                mNetd.networkRemoveUidRanges(nai.network.netId, ranges);
+                // TODO: Passing default network priority to netd.
+                mNetd.networkRemoveUidRanges(nai.network.netId, ranges
+                        /* DEFAULT_NETWORK_PRIORITY_NONE */);
             }
         } catch (Exception e) {
             loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
@@ -7462,7 +7522,7 @@
     public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
         ensureRunningOnConnectivityServiceThread();
 
-        if (getNetworkAgentInfoForNetId(nai.network.getNetId()) != nai) {
+        if (!mNetworkAgentInfos.contains(nai)) {
             // Ignore updates for disconnected networks
             return;
         }
@@ -7476,100 +7536,6 @@
         updateLinkProperties(nai, newLp, new LinkProperties(nai.linkProperties));
     }
 
-    private void sendUpdatedScoreToFactories(NetworkAgentInfo nai) {
-        for (int i = 0; i < nai.numNetworkRequests(); i++) {
-            NetworkRequest nr = nai.requestAt(i);
-            // Don't send listening or track default request to factories. b/17393458
-            if (!nr.isRequest()) continue;
-            sendUpdatedScoreToFactories(nr, nai);
-        }
-    }
-
-    private void sendUpdatedScoreToFactories(
-            @NonNull final NetworkReassignment.RequestReassignment event) {
-        // If a request of type REQUEST is now being satisfied by a new network.
-        if (null != event.mNewNetworkRequest && event.mNewNetworkRequest.isRequest()) {
-            sendUpdatedScoreToFactories(event.mNewNetworkRequest, event.mNewNetwork);
-        }
-
-        // If a previously satisfied request of type REQUEST is no longer being satisfied.
-        if (null != event.mOldNetworkRequest && event.mOldNetworkRequest.isRequest()
-                && event.mOldNetworkRequest != event.mNewNetworkRequest) {
-            sendUpdatedScoreToFactories(event.mOldNetworkRequest, null);
-        }
-
-        cancelMultilayerLowerPriorityNpiRequests(event.mNetworkRequestInfo);
-    }
-
-    /**
-     *  Cancel with all NPIs the given NRI's multilayer requests that are a lower priority than
-     *  its currently satisfied active request.
-     * @param nri the NRI to cancel lower priority requests for.
-     */
-    private void cancelMultilayerLowerPriorityNpiRequests(
-            @NonNull final NetworkRequestInfo nri) {
-        if (!nri.isMultilayerRequest() || null == nri.mActiveRequest) {
-            return;
-        }
-
-        final int indexOfNewRequest = nri.mRequests.indexOf(nri.mActiveRequest);
-        for (int i = indexOfNewRequest + 1; i < nri.mRequests.size(); i++) {
-            cancelNpiRequest(nri.mRequests.get(i));
-        }
-    }
-
-    private void sendUpdatedScoreToFactories(@NonNull NetworkRequest networkRequest,
-            @Nullable NetworkAgentInfo nai) {
-        final int score;
-        final int serial;
-        if (nai != null) {
-            score = nai.getCurrentScore();
-            serial = nai.factorySerialNumber;
-        } else {
-            score = 0;
-            serial = 0;
-        }
-        if (VDBG || DDBG){
-            log("sending new Min Network Score(" + score + "): " + networkRequest.toString());
-        }
-        for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
-            npi.requestNetwork(networkRequest, score, serial);
-        }
-    }
-
-    /** Sends all current NetworkRequests to the specified factory. */
-    private void sendAllRequestsToProvider(@NonNull final NetworkProviderInfo npi) {
-        ensureRunningOnConnectivityServiceThread();
-        for (final NetworkRequestInfo nri : getNrisFromGlobalRequests()) {
-            for (final NetworkRequest req : nri.mRequests) {
-                if (!req.isRequest() && nri.getActiveRequest() == req) {
-                    break;
-                }
-                if (!req.isRequest()) {
-                    continue;
-                }
-                // Only set the nai for the request it is satisfying.
-                final NetworkAgentInfo nai =
-                        nri.getActiveRequest() == req ? nri.getSatisfier() : null;
-                final int score;
-                final int serial;
-                if (null != nai) {
-                    score = nai.getCurrentScore();
-                    serial = nai.factorySerialNumber;
-                } else {
-                    score = 0;
-                    serial = NetworkProvider.ID_NONE;
-                }
-                npi.requestNetwork(req, score, serial);
-                // For multilayer requests, don't send lower priority requests if a higher priority
-                // request is already satisfied.
-                if (null != nai) {
-                    break;
-                }
-            }
-        }
-    }
-
     private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent,
             int notificationType) {
         if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
@@ -7795,14 +7761,18 @@
                         + " any applications to set as the default." + nri);
             }
             if (null != newDefaultNetwork) {
+                // TODO: Passing default network priority to netd.
                 mNetd.networkAddUidRanges(
                         newDefaultNetwork.network.getNetId(),
-                        toUidRangeStableParcels(nri.getUids()));
+                        toUidRangeStableParcels(nri.getUids())
+                        /* nri.getDefaultNetworkPriority() */);
             }
             if (null != oldDefaultNetwork) {
+                // TODO: Passing default network priority to netd.
                 mNetd.networkRemoveUidRanges(
                         oldDefaultNetwork.network.getNetId(),
-                        toUidRangeStableParcels(nri.getUids()));
+                        toUidRangeStableParcels(nri.getUids())
+                        /* nri.getDefaultNetworkPriority() */);
             }
         } catch (RemoteException | ServiceSpecificException e) {
             loge("Exception setting app default network", e);
@@ -7951,7 +7921,7 @@
                     log("   accepting network in place of " + previousSatisfier.toShortString());
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
-                previousSatisfier.lingerRequest(previousRequest.requestId, now, mLingerDelayMs);
+                previousSatisfier.lingerRequest(previousRequest.requestId, now);
             } else {
                 if (VDBG || DDBG) log("   accepting network in place of null");
             }
@@ -7961,6 +7931,7 @@
             // all networks except in the case of an underlying network for a VCN.
             if (newSatisfier.isNascent()) {
                 newSatisfier.unlingerRequest(NetworkRequest.REQUEST_ID_NONE);
+                newSatisfier.unsetInactive();
             }
 
             // if newSatisfier is not null, then newRequest may not be null.
@@ -7996,7 +7967,7 @@
             @NonNull final Collection<NetworkRequestInfo> networkRequests) {
         final NetworkReassignment changes = new NetworkReassignment();
 
-        // Gather the list of all relevant agents and sort them by score.
+        // Gather the list of all relevant agents.
         final ArrayList<NetworkAgentInfo> nais = new ArrayList<>();
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
             if (!nai.everConnected) {
@@ -8013,7 +7984,7 @@
             NetworkAgentInfo bestNetwork = null;
             NetworkRequest bestRequest = null;
             for (final NetworkRequest req : nri.mRequests) {
-                bestNetwork = mNetworkRanker.getBestNetwork(req, nais);
+                bestNetwork = mNetworkRanker.getBestNetwork(req, nais, nri.getSatisfier());
                 // Stop evaluating as the highest possible priority request is satisfied.
                 if (null != bestNetwork) {
                     bestRequest = req;
@@ -8061,6 +8032,7 @@
             log(changes.toString()); // Shorter form, only one line of log
         }
         applyNetworkReassignment(changes, now);
+        issueNetworkNeeds();
     }
 
     private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
@@ -8092,12 +8064,6 @@
         // before LegacyTypeTracker sends legacy broadcasts
         for (final NetworkReassignment.RequestReassignment event :
                 changes.getRequestReassignments()) {
-            // Tell NetworkProviders about the new score, so they can stop
-            // trying to connect if they know they cannot match it.
-            // TODO - this could get expensive if there are a lot of outstanding requests for this
-            // network. Think of a way to reduce this. Push netid->request mapping to each factory?
-            sendUpdatedScoreToFactories(event);
-
             if (null != event.mNewNetwork) {
                 notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
             } else {
@@ -8234,6 +8200,106 @@
         }
     }
 
+    private void issueNetworkNeeds() {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            issueNetworkNeeds(noi);
+        }
+    }
+
+    private void issueNetworkNeeds(@NonNull final NetworkOfferInfo noi) {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+            informOffer(nri, noi.offer, mNetworkRanker);
+        }
+    }
+
+    /**
+     * Inform a NetworkOffer about any new situation of a request.
+     *
+     * This function handles updates to offers. A number of events may happen that require
+     * updating the registrant for this offer about the situation :
+     * • The offer itself was updated. This may lead the offer to no longer being able
+     *     to satisfy a request or beat a satisfier (and therefore be no longer needed),
+     *     or conversely being strengthened enough to beat the satisfier (and therefore
+     *     start being needed)
+     * • The network satisfying a request changed (including cases where the request
+     *     starts or stops being satisfied). The new network may be a stronger or weaker
+     *     match than the old one, possibly affecting whether the offer is needed.
+     * • The network satisfying a request updated their score. This may lead the offer
+     *     to no longer be able to beat it if the current satisfier got better, or
+     *     conversely start being a good choice if the current satisfier got weaker.
+     *
+     * @param nri The request
+     * @param offer The offer. This may be an updated offer.
+     */
+    private static void informOffer(@NonNull NetworkRequestInfo nri,
+            @NonNull final NetworkOffer offer, @NonNull final NetworkRanker networkRanker) {
+        final NetworkRequest activeRequest = nri.isBeingSatisfied() ? nri.getActiveRequest() : null;
+        final NetworkAgentInfo satisfier = null != activeRequest ? nri.getSatisfier() : null;
+
+        // Multi-layer requests have a currently active request, the one being satisfied.
+        // Since the system will try to bring up a better network than is currently satisfying
+        // the request, NetworkProviders need to be told the offers matching the requests *above*
+        // the currently satisfied one are needed, that the ones *below* the satisfied one are
+        // not needed, and the offer is needed for the active request iff the offer can beat
+        // the satisfier.
+        // For non-multilayer requests, the logic above gracefully degenerates to only the
+        // last case.
+        // To achieve this, the loop below will proceed in three steps. In a first phase, inform
+        // providers that the offer is needed for this request, until the active request is found.
+        // In a second phase, deal with the currently active request. In a third phase, inform
+        // the providers that offer is unneeded for the remaining requests.
+
+        // First phase : inform providers of all requests above the active request.
+        int i;
+        for (i = 0; nri.mRequests.size() > i; ++i) {
+            final NetworkRequest request = nri.mRequests.get(i);
+            if (activeRequest == request) break; // Found the active request : go to phase 2
+            if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers
+            // Since this request is higher-priority than the one currently satisfied, if the
+            // offer can satisfy it, the provider should try and bring up the network for sure ;
+            // no need to even ask the ranker – an offer that can satisfy is always better than
+            // no network. Hence tell the provider so unless it already knew.
+            if (request.canBeSatisfiedBy(offer.caps) && !offer.neededFor(request)) {
+                offer.onNetworkNeeded(request);
+            }
+        }
+
+        // Second phase : deal with the active request (if any)
+        if (null != activeRequest && activeRequest.isRequest()) {
+            final boolean oldNeeded = offer.neededFor(activeRequest);
+            // An offer is needed if it is currently served by this provider or if this offer
+            // can beat the current satisfier.
+            final boolean currentlyServing = satisfier != null
+                    && satisfier.factorySerialNumber == offer.providerId;
+            final boolean newNeeded = (currentlyServing
+                    || (activeRequest.canBeSatisfiedBy(offer.caps)
+                            && networkRanker.mightBeat(activeRequest, satisfier, offer)));
+            if (newNeeded != oldNeeded) {
+                if (newNeeded) {
+                    offer.onNetworkNeeded(activeRequest);
+                } else {
+                    // The offer used to be able to beat the satisfier. Now it can't.
+                    offer.onNetworkUnneeded(activeRequest);
+                }
+            }
+        }
+
+        // Third phase : inform the providers that the offer isn't needed for any request
+        // below the active one.
+        for (++i /* skip the active request */; nri.mRequests.size() > i; ++i) {
+            final NetworkRequest request = nri.mRequests.get(i);
+            if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers
+            // Since this request is lower-priority than the one currently satisfied, if the
+            // offer can satisfy it, the provider should not try and bring up the network.
+            // Hence tell the provider so unless it already knew.
+            if (offer.neededFor(request)) {
+                offer.onNetworkUnneeded(request);
+            }
+        }
+    }
+
     private void addNetworkToLegacyTypeTracker(@NonNull final NetworkAgentInfo nai) {
         for (int i = 0; i < nai.numNetworkRequests(); i++) {
             NetworkRequest nr = nai.requestAt(i);
@@ -8370,6 +8436,7 @@
             // But it will be removed as soon as the network satisfies a request for the first time.
             networkAgent.lingerRequest(NetworkRequest.REQUEST_ID_NONE,
                     SystemClock.elapsedRealtime(), mNascentDelayMs);
+            networkAgent.setInactive();
 
             // Consider network even though it is not yet validated.
             rematchAllNetworksAndRequests();
@@ -8399,7 +8466,6 @@
         if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + score);
         nai.setScore(score);
         rematchAllNetworksAndRequests();
-        sendUpdatedScoreToFactories(nai);
     }
 
     // Notify only this one new request of the current state. Transfer all the
@@ -9204,6 +9270,12 @@
         return results;
     }
 
+    private boolean isLocationPermissionRequiredForConnectivityDiagnostics(
+            @NonNull NetworkAgentInfo nai) {
+        // TODO(b/188483916): replace with a transport-agnostic location-aware check
+        return nai.networkCapabilities.hasTransport(TRANSPORT_WIFI);
+    }
+
     private boolean hasLocationPermission(String packageName, int uid) {
         // LocationPermissionChecker#checkLocationPermission can throw SecurityException if the uid
         // and package name don't match. Throwing on the CS thread is not acceptable, so wrap the
@@ -9246,7 +9318,8 @@
             return false;
         }
 
-        return hasLocationPermission(callbackPackageName, callbackUid);
+        return !isLocationPermissionRequiredForConnectivityDiagnostics(nai)
+                || hasLocationPermission(callbackPackageName, callbackUid);
     }
 
     @Override
@@ -9712,7 +9785,8 @@
         // safe - it's just possible the value is slightly outdated. For the final check,
         // see #handleSetProfileNetworkPreference. But if this can be caught here it is a
         // lot easier to understand, so opportunistically check it.
-        if (!mOemNetworkPreferences.isEmpty()) {
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
             throwConcurrentPreferenceException();
         }
         final NetworkCapabilities nc;
@@ -9756,7 +9830,8 @@
             nrs.add(createDefaultInternetRequestForTransport(
                     TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
             setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids()));
-            final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs);
+            final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs,
+                    DEFAULT_NETWORK_PRIORITY_PROFILE);
             result.add(nri);
         }
         return result;
@@ -9771,7 +9846,8 @@
         // The binder call has already checked this, but as mOemNetworkPreferences is only
         // touched on the handler thread, it's theoretically not impossible that it has changed
         // since.
-        if (!mOemNetworkPreferences.isEmpty()) {
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
             // This may happen on a device with an OEM preference set when a user is removed.
             // In this case, it's safe to ignore. In particular this happens in the tests.
             loge("handleSetProfileNetworkPreference, but OEM network preferences not empty");
@@ -9800,6 +9876,57 @@
         }
     }
 
+    @VisibleForTesting
+    @NonNull
+    ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
+            @NonNull final Set<Integer> uids) {
+        final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
+        if (uids.size() == 0) {
+            // Should not create NetworkRequestInfo if no preferences. Without uid range in
+            // NetworkRequestInfo, makeDefaultForApps() would treat it as a illegal NRI.
+            if (DBG) log("Don't create NetworkRequestInfo because no preferences");
+            return nris;
+        }
+
+        final List<NetworkRequest> requests = new ArrayList<>();
+        // The NRI should be comprised of two layers:
+        // - The request for the mobile network preferred.
+        // - The request for the default network, for fallback.
+        requests.add(createDefaultInternetRequestForTransport(
+                TRANSPORT_CELLULAR, NetworkRequest.Type.LISTEN));
+        requests.add(createDefaultInternetRequestForTransport(
+                TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+        final Set<UidRange> ranges = new ArraySet<>();
+        for (final int uid : uids) {
+            ranges.add(new UidRange(uid, uid));
+        }
+        setNetworkRequestUids(requests, ranges);
+        nris.add(new NetworkRequestInfo(Process.myUid(), requests,
+                DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED));
+        return nris;
+    }
+
+    private void handleMobileDataPreferredUidsChanged() {
+        // Ignore update preference because it's not clear what preference should win in case both
+        // apply to the same app.
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mProfileNetworkPreferences.isEmpty()) {
+            loge("Ignore mobile data preference change because other preferences are not empty");
+            return;
+        }
+
+        mMobileDataPreferredUids = ConnectivitySettingsManager.getMobileDataPreferredUids(mContext);
+        mSystemNetworkRequestCounter.transact(
+                mDeps.getCallingUid(), 1 /* numOfNewRequests */,
+                () -> {
+                    final ArraySet<NetworkRequestInfo> nris =
+                            createNrisFromMobileDataPreferredUids(mMobileDataPreferredUids);
+                    replaceDefaultNetworkRequestsForPreference(nris);
+                });
+        // Finally, rematch.
+        rematchAllNetworksAndRequests();
+    }
+
     private void enforceAutomotiveDevice() {
         final boolean isAutomotiveDevice =
                 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
@@ -9829,7 +9956,8 @@
         enforceAutomotiveDevice();
         enforceOemNetworkPreferencesPermission();
 
-        if (!mProfileNetworkPreferences.isEmpty()) {
+        // TODO: Have a priority for each preference.
+        if (!mProfileNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
             // Strictly speaking, mProfileNetworkPreferences should only be touched on the
             // handler thread. However it is an immutable object, so reading the reference is
             // safe - it's just possible the value is slightly outdated. For the final check,
@@ -9867,7 +9995,8 @@
         // The binder call has already checked this, but as mOemNetworkPreferences is only
         // touched on the handler thread, it's theoretically not impossible that it has changed
         // since.
-        if (!mProfileNetworkPreferences.isEmpty()) {
+        // TODO: Have a priority for each preference.
+        if (!mProfileNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
             logwtf("handleSetOemPreference, but per-profile network preferences not empty");
             return;
         }
@@ -10017,7 +10146,7 @@
 
         private SparseArray<Set<Integer>> createUidsFromOemNetworkPreferences(
                 @NonNull final OemNetworkPreferences preference) {
-            final SparseArray<Set<Integer>> uids = new SparseArray<>();
+            final SparseArray<Set<Integer>> prefToUids = new SparseArray<>();
             final PackageManager pm = mContext.getPackageManager();
             final List<UserHandle> users =
                     mContext.getSystemService(UserManager.class).getUserHandles(true);
@@ -10025,29 +10154,29 @@
                 if (VDBG || DDBG) {
                     log("No users currently available for setting the OEM network preference.");
                 }
-                return uids;
+                return prefToUids;
             }
             for (final Map.Entry<String, Integer> entry :
                     preference.getNetworkPreferences().entrySet()) {
                 @OemNetworkPreferences.OemNetworkPreference final int pref = entry.getValue();
-                try {
-                    final int uid = pm.getApplicationInfo(entry.getKey(), 0).uid;
-                    if (!uids.contains(pref)) {
-                        uids.put(pref, new ArraySet<>());
+                // Add the rules for all users as this policy is device wide.
+                for (final UserHandle user : users) {
+                    try {
+                        final int uid = pm.getApplicationInfoAsUser(entry.getKey(), 0, user).uid;
+                        if (!prefToUids.contains(pref)) {
+                            prefToUids.put(pref, new ArraySet<>());
+                        }
+                        prefToUids.get(pref).add(uid);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        // Although this may seem like an error scenario, it is ok that uninstalled
+                        // packages are sent on a network preference as the system will watch for
+                        // package installations associated with this network preference and update
+                        // accordingly. This is done to minimize race conditions on app install.
+                        continue;
                     }
-                    for (final UserHandle ui : users) {
-                        // Add the rules for all users as this policy is device wide.
-                        uids.get(pref).add(ui.getUid(uid));
-                    }
-                } catch (PackageManager.NameNotFoundException e) {
-                    // Although this may seem like an error scenario, it is ok that uninstalled
-                    // packages are sent on a network preference as the system will watch for
-                    // package installations associated with this network preference and update
-                    // accordingly. This is done so as to minimize race conditions on app install.
-                    continue;
                 }
             }
-            return uids;
+            return prefToUids;
         }
 
         private NetworkRequestInfo createNriFromOemNetworkPreferences(
@@ -10083,7 +10212,8 @@
                 ranges.add(new UidRange(uid, uid));
             }
             setNetworkRequestUids(requests, ranges);
-            return new NetworkRequestInfo(Process.myUid(), requests);
+            return new NetworkRequestInfo(
+                    Process.myUid(), requests, DEFAULT_NETWORK_PRIORITY_OEM);
         }
 
         private NetworkRequest createUnmeteredNetworkRequest() {
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 09873f4..fffd2be 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -48,7 +48,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
-import com.android.net.module.util.PermissionUtils;
 
 import java.io.UncheckedIOException;
 import java.net.Inet4Address;
@@ -123,6 +122,8 @@
                         addr.getPrefixLength());
             }
 
+            NetdUtils.setInterfaceUp(mNetd, iface);
+
             return new TestNetworkInterface(tunIntf, iface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -324,14 +325,6 @@
         }
 
         try {
-            final long token = Binder.clearCallingIdentity();
-            try {
-                PermissionUtils.enforceNetworkStackPermission(mContext);
-                NetdUtils.setInterfaceUp(mNetd, iface);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-
             // Synchronize all accesses to mTestNetworkTracker to prevent the case where:
             // 1. TestNetworkAgent successfully binds to death of binder
             // 2. Before it is added to the mTestNetworkTracker, binder dies, binderDied() is called
diff --git a/service/src/com/android/server/connectivity/ConnectivityConstants.java b/service/src/com/android/server/connectivity/ConnectivityConstants.java
deleted file mode 100644
index 325a2cd..0000000
--- a/service/src/com/android/server/connectivity/ConnectivityConstants.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.connectivity;
-
-/**
- * A class encapsulating various constants used by Connectivity.
- * TODO : remove this class.
- * @hide
- */
-public class ConnectivityConstants {
-    // VPNs typically have priority over other networks. Give them a score that will
-    // let them win every single time.
-    public static final int VPN_DEFAULT_SCORE = 101;
-}
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index 9326d69..14cec09 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -17,14 +17,20 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkScore.KEEP_CONNECTED_NONE;
+import static android.net.NetworkScore.POLICY_EXITING;
+import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
+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;
 import android.net.NetworkScore;
+import android.net.NetworkScore.KeepConnectedReason;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -50,7 +56,8 @@
             POLICY_IS_VALIDATED,
             POLICY_IS_VPN,
             POLICY_EVER_USER_SELECTED,
-            POLICY_ACCEPT_UNVALIDATED
+            POLICY_ACCEPT_UNVALIDATED,
+            POLICY_IS_UNMETERED
     })
     public @interface Policy {
     }
@@ -75,12 +82,36 @@
     /** @hide */
     public static final int POLICY_ACCEPT_UNVALIDATED = 60;
 
+    // This network is unmetered. {@see NetworkCapabilities.NET_CAPABILITY_NOT_METERED}.
+    /** @hide */
+    public static final int POLICY_IS_UNMETERED = 59;
+
+    // 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;
+
     // To help iterate when printing
     @VisibleForTesting
-    static final int MIN_CS_MANAGED_POLICY = POLICY_ACCEPT_UNVALIDATED;
+    static final int MIN_CS_MANAGED_POLICY = POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
     @VisibleForTesting
     static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED;
 
+    // Mask for policies in NetworkScore. This should have all bits managed by NetworkScore set
+    // and all bits managed by FullScore unset. As bits are handled from 0 up in NetworkScore and
+    // from 63 down in FullScore, cut at the 32nd bit for simplicity, but change this if some day
+    // there are more than 32 bits handled on either side.
+    // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService.
+    private static final long EXTERNAL_POLICIES_MASK =
+            0x00000000FFFFFFFFL & ~(1L << POLICY_YIELD_TO_BAD_WIFI);
+
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
         switch (policy) {
@@ -88,6 +119,12 @@
             case POLICY_IS_VPN: return "IS_VPN";
             case POLICY_EVER_USER_SELECTED: return "EVER_USER_SELECTED";
             case POLICY_ACCEPT_UNVALIDATED: return "ACCEPT_UNVALIDATED";
+            case POLICY_IS_UNMETERED: return "IS_UNMETERED";
+            case POLICY_YIELD_TO_BAD_WIFI: return "YIELD_TO_BAD_WIFI";
+            case POLICY_TRANSPORT_PRIMARY: return "TRANSPORT_PRIMARY";
+            case POLICY_EXITING: return "EXITING";
+            case POLICY_IS_INVINCIBLE: return "INVINCIBLE";
+            case POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD: return "EVER_VALIDATED";
         }
         throw new IllegalArgumentException("Unknown policy : " + policy);
     }
@@ -95,9 +132,13 @@
     // Bitmask of all the policies applied to this score.
     private final long mPolicies;
 
-    FullScore(final int legacyInt, final long policies) {
+    private final int mKeepConnectedReason;
+
+    FullScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
         mLegacyInt = legacyInt;
         mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
     }
 
     /**
@@ -106,18 +147,30 @@
      * @param score the score supplied by the agent
      * @param caps the NetworkCapabilities of the network
      * @param config the NetworkAgentConfig of the network
-     * @return an FullScore that is appropriate to use for ranking.
+     * @param everValidated whether this network has ever validated
+     * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad
+     * @return a FullScore that is appropriate to use for ranking.
      */
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
     public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
-            @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config) {
-        return withPolicies(score.getLegacyInt(), caps.hasCapability(NET_CAPABILITY_VALIDATED),
+            @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config,
+            final boolean everValidated, final boolean yieldToBadWiFi) {
+        return withPolicies(score.getLegacyInt(), score.getPolicies(),
+                score.getKeepConnectedReason(),
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
                 config.explicitlySelected,
-                config.acceptUnvalidated);
+                config.acceptUnvalidated,
+                yieldToBadWiFi,
+                false /* invincible */); // only prospective scores can be invincible
     }
 
     /**
-     * Given a score supplied by the NetworkAgent, produce a prospective score for an offer.
+     * Given a score supplied by a NetworkProvider, produce a prospective score for an offer.
      *
      * NetworkOffers have score filters that are compared to the scores of actual networks
      * to see if they could possibly beat the current satisfier. Some things the agent can't
@@ -135,12 +188,23 @@
         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;
         // 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;
-        return withPolicies(score.getLegacyInt(), mayValidate, vpn, everUserSelected,
-                acceptUnvalidated);
+        // Don't assume clinging to bad wifi
+        final boolean yieldToBadWiFi = 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.getLegacyInt(), score.getPolicies(), KEEP_CONNECTED_NONE,
+                mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated,
+                yieldToBadWiFi, invincible);
     }
 
     /**
@@ -150,24 +214,56 @@
      * @param config the NetworkAgentConfig of the network
      * @return a score with the policies from the arguments reset
      */
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
     public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
-            @NonNull final NetworkAgentConfig config) {
-        return withPolicies(mLegacyInt, caps.hasCapability(NET_CAPABILITY_VALIDATED),
+            @NonNull final NetworkAgentConfig config,
+            final boolean everValidated,
+            final boolean yieldToBadWifi) {
+        return withPolicies(mLegacyInt, mPolicies, mKeepConnectedReason,
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
                 config.explicitlySelected,
-                config.acceptUnvalidated);
+                config.acceptUnvalidated,
+                yieldToBadWifi,
+                false /* invincible */); // only prospective scores can be invincible
     }
 
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
     private static FullScore withPolicies(@NonNull final int legacyInt,
+            final long externalPolicies,
+            @KeepConnectedReason final int keepConnectedReason,
             final boolean isValidated,
             final boolean isVpn,
+            final boolean isUnmetered,
+            final boolean everValidated,
             final boolean everUserSelected,
-            final boolean acceptUnvalidated) {
-        return new FullScore(legacyInt,
-                (isValidated         ? 1L << POLICY_IS_VALIDATED : 0)
+            final boolean acceptUnvalidated,
+            final boolean yieldToBadWiFi,
+            final boolean invincible) {
+        return new FullScore(legacyInt, (externalPolicies & EXTERNAL_POLICIES_MASK)
+                | (isValidated       ? 1L << POLICY_IS_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));
+                | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0)
+                | (yieldToBadWiFi    ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0)
+                | (invincible        ? 1L << POLICY_IS_INVINCIBLE : 0),
+                keepConnectedReason);
+    }
+
+    /**
+     * Returns this score but validated.
+     */
+    public FullScore asValidated() {
+        return new FullScore(mLegacyInt, mPolicies | (1L << POLICY_IS_VALIDATED),
+                mKeepConnectedReason);
     }
 
     /**
@@ -219,13 +315,21 @@
         return 0 != (mPolicies & (1L << policy));
     }
 
+    /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
     // Example output :
     // Score(50 ; Policies : EVER_USER_SELECTED&IS_VALIDATED)
     @Override
     public String toString() {
         final StringJoiner sj = new StringJoiner(
                 "&", // delimiter
-                "Score(" + mLegacyInt + " ; Policies : ", // prefix
+                "Score(" + mLegacyInt + " ; KeepConnected : " + mKeepConnectedReason
+                        + " ; Policies : ", // prefix
                 ")"); // suffix
         for (int i = NetworkScore.MIN_AGENT_MANAGED_POLICY;
                 i <= NetworkScore.MAX_AGENT_MANAGED_POLICY; ++i) {
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index ee32fbf..18becd4 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
 import android.annotation.NonNull;
@@ -58,6 +59,7 @@
 import com.android.server.ConnectivityService;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.NoSuchElementException;
@@ -142,7 +144,7 @@
 // the network is no longer considered "lingering". After the linger timer expires, if the network
 // is satisfying one or more background NetworkRequests it is kept up in the background. If it is
 // not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.
-public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
+public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRanker.Scoreable {
 
     @NonNull public NetworkInfo networkInfo;
     // This Network object should always be used if possible, so as to encourage reuse of the
@@ -280,6 +282,9 @@
      */
     public static final int ARG_AGENT_SUCCESS = 1;
 
+    // How long this network should linger for.
+    private int mLingerDurationMs;
+
     // All inactivity timers for this network, sorted by expiry time. A timer is added whenever
     // a request is moved to a network with a better score, regardless of whether the network is or
     // was lingering or not. An inactivity timer is also added when a network connects
@@ -348,7 +353,8 @@
             @NonNull NetworkScore score, Context context,
             Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
             IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
-            QosCallbackTracker qosCallbackTracker, ConnectivityService.Dependencies deps) {
+            int lingerDurationMs, QosCallbackTracker qosCallbackTracker,
+            ConnectivityService.Dependencies deps) {
         Objects.requireNonNull(net);
         Objects.requireNonNull(info);
         Objects.requireNonNull(lp);
@@ -362,13 +368,14 @@
         linkProperties = lp;
         networkCapabilities = nc;
         networkAgentConfig = config;
-        setScore(score); // uses members networkCapabilities and networkAgentConfig
-        clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
         mConnService = connService;
+        setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
+        clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
         mContext = context;
         mHandler = handler;
         this.factorySerialNumber = factorySerialNumber;
         this.creatorUid = creatorUid;
+        mLingerDurationMs = lingerDurationMs;
         mQosCallbackTracker = qosCallbackTracker;
     }
 
@@ -684,6 +691,12 @@
             mHandler.obtainMessage(NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED,
                     teardownDelayMs, 0, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
         }
+
+        @Override
+        public void sendLingerDuration(final int durationMs) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
+        }
     }
 
     /**
@@ -706,7 +719,8 @@
             @NonNull final NetworkCapabilities nc) {
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig);
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
+                yieldToBadWiFi());
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -714,6 +728,11 @@
         return oldNc;
     }
 
+    private boolean yieldToBadWiFi() {
+        // Only cellular networks yield to bad wifi
+        return networkCapabilities.hasTransport(TRANSPORT_CELLULAR) && !mConnService.avoidBadWifi();
+    }
+
     public ConnectivityService connService() {
         return mConnService;
     }
@@ -884,13 +903,16 @@
         return isVPN();
     }
 
-    // Return true on devices configured to ignore score penalty for wifi networks
-    // that become unvalidated (b/31075769).
-    private boolean ignoreWifiUnvalidationPenalty() {
-        boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
-                networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        boolean avoidBadWifi = mConnService.avoidBadWifi() || avoidUnvalidated;
-        return isWifi && !avoidBadWifi && everValidated;
+    // Caller must not mutate. This method is called frequently and making a defensive copy
+    // would be too expensive. This is used by NetworkRanker.Scoreable, so it can be compared
+    // against other scoreables.
+    @Override public NetworkCapabilities getCapsNoCopy() {
+        return networkCapabilities;
+    }
+
+    // NetworkRanker.Scoreable
+    @Override public FullScore getScore() {
+        return mScore;
     }
 
     // Get the current score for this Network.  This may be modified from what the
@@ -909,7 +931,8 @@
      * Mix-in the ConnectivityService-managed bits in the score.
      */
     public void setScore(final NetworkScore score) {
-        mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig);
+        mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
+                everValidatedForYield(), yieldToBadWiFi());
     }
 
     /**
@@ -917,8 +940,33 @@
      *
      * Call this after updating the network agent config.
      */
-    public void updateScoreForNetworkAgentConfigUpdate() {
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig);
+    public void updateScoreForNetworkAgentUpdate() {
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
+                everValidatedForYield(), yieldToBadWiFi());
+    }
+
+    private boolean everValidatedForYield() {
+        return everValidated && !avoidUnvalidated;
+    }
+
+    /**
+     * Returns a Scoreable identical to this NAI, but validated.
+     *
+     * This is useful to probe what scoring would be if this network validated, to know
+     * whether to provisionally keep a network that may or may not validate.
+     *
+     * @return a Scoreable identical to this NAI, but validated.
+     */
+    public NetworkRanker.Scoreable getValidatedScoreable() {
+        return new NetworkRanker.Scoreable() {
+            @Override public FullScore getScore() {
+                return mScore.asValidated();
+            }
+
+            @Override public NetworkCapabilities getCapsNoCopy() {
+                return networkCapabilities;
+            }
+        };
     }
 
     /**
@@ -938,13 +986,14 @@
 
     /**
      * Sets the specified requestId to linger on this network for the specified time. Called by
-     * ConnectivityService when the request is moved to another network with a higher score, or
+     * ConnectivityService when any request is moved to another network with a higher score, or
      * when a network is newly created.
      *
      * @param requestId The requestId of the request that no longer need to be served by this
      *                  network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the
-     *                  {@code LingerTimer} for a newly created network.
+     *                  {@code InactivityTimer} for a newly created network.
      */
+    // TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent.
     public void lingerRequest(int requestId, long now, long duration) {
         if (mInactivityTimerForRequest.get(requestId) != null) {
             // Cannot happen. Once a request is lingering on a particular network, we cannot
@@ -960,6 +1009,19 @@
     }
 
     /**
+     * Sets the specified requestId to linger on this network for the timeout set when
+     * initializing or modified by {@link #setLingerDuration(int)}. Called by
+     * ConnectivityService when any request is moved to another network with a higher score.
+     *
+     * @param requestId The requestId of the request that no longer need to be served by this
+     *                  network.
+     * @param now current system timestamp obtained by {@code SystemClock.elapsedRealtime}.
+     */
+    public void lingerRequest(int requestId, long now) {
+        lingerRequest(requestId, now, mLingerDurationMs);
+    }
+
+    /**
      * Cancel lingering. Called by ConnectivityService when a request is added to this network.
      * Returns true if the given requestId was lingering on this network, false otherwise.
      */
@@ -996,6 +1058,7 @@
         }
 
         if (newExpiry > 0) {
+            // If the newExpiry timestamp is in the past, the wakeup message will fire immediately.
             mInactivityMessage = new WakeupMessage(
                     mContext, mHandler,
                     "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */,
@@ -1025,8 +1088,33 @@
     }
 
     /**
-     * Return whether the network is just connected and about to be torn down because of not
-     * satisfying any request.
+     * Set the linger duration for this NAI.
+     * @param durationMs The new linger duration, in milliseconds.
+     */
+    public void setLingerDuration(final int durationMs) {
+        final long diff = durationMs - mLingerDurationMs;
+        final ArrayList<InactivityTimer> newTimers = new ArrayList<>();
+        for (final InactivityTimer timer : mInactivityTimers) {
+            if (timer.requestId == NetworkRequest.REQUEST_ID_NONE) {
+                // Don't touch nascent timer, re-add as is.
+                newTimers.add(timer);
+            } else {
+                newTimers.add(new InactivityTimer(timer.requestId, timer.expiryMs + diff));
+            }
+        }
+        mInactivityTimers.clear();
+        mInactivityTimers.addAll(newTimers);
+        updateInactivityTimer();
+        mLingerDurationMs = durationMs;
+    }
+
+    /**
+     * Return whether the network satisfies no request, but is still being kept up
+     * because it has just connected less than
+     * {@code ConnectivityService#DEFAULT_NASCENT_DELAY_MS}ms ago and is thus still considered
+     * nascent. Note that nascent mechanism uses inactivity timer which isn't
+     * associated with a request. Thus, use {@link NetworkRequest#REQUEST_ID_NONE} to identify it.
+     *
      */
     public boolean isNascent() {
         return mInactive && mInactivityTimers.size() == 1
@@ -1081,7 +1169,7 @@
         return "NetworkAgentInfo{"
                 + "network{" + network + "}  handle{" + network.getNetworkHandle() + "}  ni{"
                 + networkInfo.toShortString() + "} "
-                + "  Score{" + getCurrentScore() + "} "
+                + mScore + " "
                 + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
                 + (everValidated ? " everValidated" : "")
                 + (lastValidated ? " lastValidated" : "")
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index 548db6b..8285e7a 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -17,13 +17,14 @@
 package com.android.server.connectivity;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.net.INetworkOfferCallback;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.os.RemoteException;
 
+import java.util.HashSet;
 import java.util.Objects;
-
+import java.util.Set;
 
 /**
  * Represents an offer made by a NetworkProvider to create a network if a need arises.
@@ -39,51 +40,105 @@
  *
  * @hide
  */
-public class NetworkOffer {
+public class NetworkOffer implements NetworkRanker.Scoreable {
     @NonNull public final FullScore score;
     @NonNull public final NetworkCapabilities caps;
     @NonNull public final INetworkOfferCallback callback;
     @NonNull public final int providerId;
+    // While this could, in principle, be deduced from the old values of the satisfying networks,
+    // doing so would add a lot of complexity and performance penalties. For each request, the
+    // ranker would have to run again to figure out if this offer used to be able to beat the
+    // previous satisfier to know if there is a change in whether this offer is now needed ;
+    // besides, there would be a need to handle an edge case when a new request comes online,
+    // where it's not satisfied before the first rematch, where starting to satisfy a request
+    // should not result in sending unneeded to this offer. This boolean, while requiring that
+    // the offers are only ever manipulated on the CS thread, is by far a simpler and
+    // economical solution.
+    private final Set<NetworkRequest> mCurrentlyNeeded = new HashSet<>();
 
-    private static NetworkCapabilities emptyCaps() {
-        final NetworkCapabilities nc = new NetworkCapabilities();
-        return nc;
-    }
-
-    // Ideally the caps argument would be non-null, but null has historically meant no filter
-    // and telephony passes null. Keep backward compatibility.
     public NetworkOffer(@NonNull final FullScore score,
-            @Nullable final NetworkCapabilities caps,
+            @NonNull final NetworkCapabilities caps,
             @NonNull final INetworkOfferCallback callback,
             @NonNull final int providerId) {
         this.score = Objects.requireNonNull(score);
-        this.caps = null != caps ? caps : emptyCaps();
+        this.caps = Objects.requireNonNull(caps);
         this.callback = Objects.requireNonNull(callback);
         this.providerId = providerId;
     }
 
     /**
+     * Get the score filter of this offer
+     */
+    @Override @NonNull public FullScore getScore() {
+        return score;
+    }
+
+    /**
+     * Get the capabilities filter of this offer
+     */
+    @Override @NonNull public NetworkCapabilities getCapsNoCopy() {
+        return caps;
+    }
+
+    /**
+     * Tell the provider for this offer that the network is needed for a request.
+     * @param request the request for which the offer is needed
+     */
+    public void onNetworkNeeded(@NonNull final NetworkRequest request) {
+        if (mCurrentlyNeeded.contains(request)) {
+            throw new IllegalStateException("Network already needed");
+        }
+        mCurrentlyNeeded.add(request);
+        try {
+            callback.onNetworkNeeded(request);
+        } catch (final RemoteException e) {
+            // The provider is dead. It will be removed by the death recipient.
+        }
+    }
+
+    /**
+     * Tell the provider for this offer that the network is no longer needed for this request.
+     *
+     * onNetworkNeeded will have been called with the same request before.
+     *
+     * @param request the request
+     */
+    public void onNetworkUnneeded(@NonNull final NetworkRequest request) {
+        if (!mCurrentlyNeeded.contains(request)) {
+            throw new IllegalStateException("Network already unneeded");
+        }
+        mCurrentlyNeeded.remove(request);
+        try {
+            callback.onNetworkUnneeded(request);
+        } catch (final RemoteException e) {
+            // The provider is dead. It will be removed by the death recipient.
+        }
+    }
+
+    /**
+     * Returns whether this offer is currently needed for this request.
+     * @param request the request
+     * @return whether the offer is currently considered needed
+     */
+    public boolean neededFor(@NonNull final NetworkRequest request) {
+        return mCurrentlyNeeded.contains(request);
+    }
+
+    /**
      * Migrate from, and take over, a previous offer.
      *
      * When an updated offer is sent from a provider, call this method on the new offer, passing
      * the old one, to take over the state.
      *
-     * @param previousOffer
+     * @param previousOffer the previous offer
      */
     public void migrateFrom(@NonNull final NetworkOffer previousOffer) {
-        if (!callback.equals(previousOffer.callback)) {
+        if (!callback.asBinder().equals(previousOffer.callback.asBinder())) {
             throw new IllegalArgumentException("Can only migrate from a previous version of"
                     + " the same offer");
         }
-    }
-
-    /**
-     * Returns whether an offer can satisfy a NetworkRequest, according to its capabilities.
-     * @param request The request to test against.
-     * @return Whether this offer can satisfy the request.
-     */
-    public final boolean canSatisfy(@NonNull final NetworkRequest request) {
-        return request.networkCapabilities.satisfiedByNetworkCapabilities(caps);
+        mCurrentlyNeeded.clear();
+        mCurrentlyNeeded.addAll(previousOffer.mCurrentlyNeeded);
     }
 
     @Override
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index d0aabf9..e839837 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -16,35 +16,284 @@
 
 package com.android.server.connectivity;
 
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkScore.POLICY_EXITING;
+import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
+import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
+
+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_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_IS_INVINCIBLE;
+import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
+import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * A class that knows how to find the best network matching a request out of a list of networks.
  */
 public class NetworkRanker {
+    // Historically the legacy ints have been 0~100 in principle (though the highest score in
+    // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101.
+    public static final int LEGACY_INT_MAX = 100;
+
+    /**
+     * A class that can be scored against other scoreables.
+     */
+    public interface Scoreable {
+        /** Get score of this scoreable */
+        FullScore getScore();
+        /** Get capabilities of this scoreable */
+        NetworkCapabilities getCapsNoCopy();
+    }
+
+    private static final boolean USE_POLICY_RANKING = true;
+
     public NetworkRanker() { }
 
     /**
      * Find the best network satisfying this request among the list of passed networks.
      */
-    // Almost equivalent to Collections.max(nais), but allows returning null if no network
-    // satisfies the request.
     @Nullable
     public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
+            @NonNull final Collection<NetworkAgentInfo> nais,
+            @Nullable final NetworkAgentInfo currentSatisfier) {
+        final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request));
+        if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier
+        if (candidates.size() <= 0) return null; // No network can satisfy this request
+        if (USE_POLICY_RANKING) {
+            return getBestNetworkByPolicy(candidates, currentSatisfier);
+        } else {
+            return getBestNetworkByLegacyInt(candidates);
+        }
+    }
+
+    // Transport preference order, if it comes down to that.
+    private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
+            TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };
+
+    // Function used to partition a list into two working areas depending on whether they
+    // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
+    // items that don't will be put in |negative|.
+    // This is useful in this file because many of the ranking checks will retain only networks that
+    // satisfy a predicate if any of them do, but keep them all if all of them do. Having working
+    // areas is uncustomary in Java, but this function is called in a fairly intensive manner
+    // and doing allocation quite that often might affect performance quite badly.
+    private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test,
+            @NonNull final List<T> positive, @NonNull final List<T> negative) {
+        positive.clear();
+        negative.clear();
+        for (final T item : source) {
+            if (test.test(item)) {
+                positive.add(item);
+            } else {
+                negative.add(item);
+            }
+        }
+    }
+
+    @Nullable private <T extends Scoreable> T getBestNetworkByPolicy(
+            @NonNull List<T> candidates,
+            @Nullable final T currentSatisfier) {
+        // Used as working areas.
+        final ArrayList<T> accepted =
+                new ArrayList<>(candidates.size() /* initialCapacity */);
+        final ArrayList<T> rejected =
+                new ArrayList<>(candidates.size() /* initialCapacity */);
+
+        // The following tests will search for a network matching a given criterion. They all
+        // function the same way : if any network matches the criterion, drop from consideration
+        // all networks that don't. To achieve this, the tests below :
+        // 1. partition the list of remaining candidates into accepted and rejected networks.
+        // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0]
+        // 3. if multiple remain, keep only the accepted networks and go on to the next criterion.
+        //    Because the working areas will be wiped, a copy of the accepted networks needs to be
+        //    made.
+        // 4. if none remain, the criterion did not help discriminate so keep them all. As an
+        //    optimization, skip creating a new array and go on to the next criterion.
+
+        // If a network is invincible, use it.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If there is a connected VPN, use it.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // Selected & Accept-unvalidated policy : if any network has both of these, then don't
+        // choose one that doesn't.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
+                        && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // Yield to bad wifi policy : if any wifi has ever been validated (even if it's now
+        // unvalidated), and unless it's been explicitly avoided when bad in UI, then keep only
+        // networks that don't yield to such a wifi network.
+        final boolean anyWiFiEverValidated = CollectionUtils.any(candidates,
+                nai -> nai.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
+                        && nai.getCapsNoCopy().hasTransport(TRANSPORT_WIFI));
+        if (anyWiFiEverValidated) {
+            partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
+                    accepted, rejected);
+            if (accepted.size() == 1) return accepted.get(0);
+            if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+        }
+
+        // If any network is validated (or should be accepted even if it's not validated), then
+        // don't choose one that isn't.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED)
+                        || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If any network is not exiting, don't choose one that is.
+        partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // TODO : If any network is unmetered, don't choose a metered network.
+        // This can't be implemented immediately because prospective networks are always
+        // considered unmetered because factories don't know if the network will be metered.
+        // Saying an unmetered network always beats a metered one would mean that when metered wifi
+        // is connected, the offer for telephony would beat WiFi but the actual metered network
+        // would lose, so we'd have an infinite loop where telephony would continually bring up
+        // a network that is immediately torn down.
+        // Fix this by getting the agent to tell connectivity whether the network they will
+        // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and
+        // can revise it if the network later turns out to be metered.
+        // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED),
+        //         accepted, rejected);
+        // if (accepted.size() == 1) return accepted.get(0);
+        // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If any network is for the default subscription, don't choose a network for another
+        // subscription with the same transport.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
+                accepted, rejected);
+        for (final Scoreable defaultSubNai : accepted) {
+            // Remove all networks without the DEFAULT_SUBSCRIPTION policy and the same transports
+            // as a network that has it.
+            final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
+            candidates.removeIf(nai -> !nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY)
+                    && Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
+        }
+        if (1 == candidates.size()) return candidates.get(0);
+        // It's guaranteed candidates.size() > 0 because there is at least one with the
+        // TRANSPORT_PRIMARY policy and only those without it were removed.
+
+        // If some of the networks have a better transport than others, keep only the ones with
+        // the best transports.
+        for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
+            partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
+                    accepted, rejected);
+            if (accepted.size() == 1) return accepted.get(0);
+            if (accepted.size() > 0 && rejected.size() > 0) {
+                candidates = new ArrayList<>(accepted);
+                break;
+            }
+        }
+
+        // At this point there are still multiple networks passing all the tests above. If any
+        // of them is the previous satisfier, keep it.
+        if (candidates.contains(currentSatisfier)) return currentSatisfier;
+
+        // If there are still multiple options at this point but none of them is any of the
+        // transports above, it doesn't matter which is returned. They are all the same.
+        return candidates.get(0);
+    }
+
+    // TODO : switch to the policy implementation and remove
+    // Almost equivalent to Collections.max(nais), but allows returning null if no network
+    // satisfies the request.
+    private NetworkAgentInfo getBestNetworkByLegacyInt(
             @NonNull final Collection<NetworkAgentInfo> nais) {
         NetworkAgentInfo bestNetwork = null;
         int bestScore = Integer.MIN_VALUE;
         for (final NetworkAgentInfo nai : nais) {
-            if (!nai.satisfies(request)) continue;
-            if (nai.getCurrentScore() > bestScore) {
+            final int naiScore = nai.getCurrentScore();
+            if (naiScore > bestScore) {
                 bestNetwork = nai;
-                bestScore = nai.getCurrentScore();
+                bestScore = naiScore;
             }
         }
         return bestNetwork;
     }
+
+    /**
+     * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request.
+     *
+     * Offers are sent by network providers when they think they might be able to make a network
+     * with the characteristics contained in the offer. If the offer has no chance to beat
+     * the currently best network for a given request, there is no point in the provider spending
+     * power trying to find and bring up such a network.
+     *
+     * Note that having an offer up does not constitute a commitment from the provider part
+     * to be able to bring up a network with these characteristics, or a network at all for
+     * that matter. This is only used to save power by letting providers know when they can't
+     * beat a current champion.
+     *
+     * @param request The request to evaluate against.
+     * @param champion The currently best network for this request.
+     * @param contestant The offer.
+     * @return Whether the offer stands a chance to beat the champion.
+     */
+    public boolean mightBeat(@NonNull final NetworkRequest request,
+            @Nullable final NetworkAgentInfo champion,
+            @NonNull final Scoreable contestant) {
+        // If this network can't even satisfy the request then it can't beat anything, not
+        // even an absence of network. It can't satisfy it anyway.
+        if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false;
+        // If there is no satisfying network, then this network can beat, because some network
+        // is always better than no network.
+        if (null == champion) return true;
+        if (USE_POLICY_RANKING) {
+            // If there is no champion, the offer can always beat.
+            // Otherwise rank them.
+            final ArrayList<Scoreable> candidates = new ArrayList<>();
+            candidates.add(champion);
+            candidates.add(contestant);
+            return contestant == getBestNetworkByPolicy(candidates, champion);
+        } else {
+            return mightBeatByLegacyInt(champion.getScore(), contestant);
+        }
+    }
+
+    /**
+     * Returns whether a contestant might beat a champion according to the legacy int.
+     */
+    private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore,
+            @NonNull final Scoreable contestant) {
+        final int offerIntScore;
+        if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            // If the offer might have Internet access, then it might validate.
+            offerIntScore = contestant.getScore().getLegacyIntAsValidated();
+        } else {
+            offerIntScore = contestant.getScore().getLegacyInt();
+        }
+        return championScore.getLegacyInt() < offerIntScore;
+    }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 49b43f8..32e06e5 100644
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -197,7 +197,7 @@
         // Register UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer
         mDeps.registerContentObserver(
                 userAllContext,
-                Settings.Secure.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS),
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS),
                 false /* notifyForDescendants */,
                 new ContentObserver(null) {
                     @Override
@@ -265,7 +265,7 @@
     }
 
     @VisibleForTesting
-    void updateUidsAllowedOnRestrictedNetworks(final Set<Integer> uids) {
+    synchronized void updateUidsAllowedOnRestrictedNetworks(final Set<Integer> uids) {
         mUidsAllowedOnRestrictedNetworks.clear();
         mUidsAllowedOnRestrictedNetworks.addAll(uids);
     }
@@ -285,7 +285,7 @@
     }
 
     @VisibleForTesting
-    boolean isUidAllowedOnRestrictedNetworks(final ApplicationInfo appInfo) {
+    synchronized boolean isUidAllowedOnRestrictedNetworks(final ApplicationInfo appInfo) {
         if (appInfo == null) return false;
         // Check whether package's uid is in allowed on restricted networks uid list. If so, this
         // uid can have netd system permission.
@@ -389,6 +389,14 @@
         update(users, mApps, false);
     }
 
+    /**
+     * Compare the current network permission and the given package's permission to find out highest
+     * permission for the uid.
+     *
+     * @param currentPermission Current uid network permission
+     * @param name The package has same uid that need compare its permission to update uid network
+     *             permission.
+     */
     @VisibleForTesting
     protected Boolean highestPermissionForUid(Boolean currentPermission, String name) {
         if (currentPermission == SYSTEM) {
@@ -472,6 +480,8 @@
         final String[] packages = mPackageManager.getPackagesForUid(uid);
         if (!CollectionUtils.isEmpty(packages)) {
             for (String name : packages) {
+                // If multiple packages have the same UID, give the UID all permissions that
+                // any package in that UID has.
                 permission = highestPermissionForUid(permission, name);
                 if (permission == SYSTEM) {
                     break;
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index dc66870..e8963b9 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -23,7 +23,11 @@
 
 java_library {
     name: "FrameworksNetCommonTests",
-    srcs: ["java/**/*.java", "java/**/*.kt"],
+    defaults: ["framework-connectivity-test-defaults"],
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
     static_libs: [
         "androidx.core_core",
         "androidx.test.rules",
@@ -38,3 +42,22 @@
         "android.test.base.stubs",
     ],
 }
+
+// defaults for tests that need to build against framework-connectivity's @hide APIs
+// Only usable from targets that have visibility on framework-connectivity.impl.
+// Instead of using this, consider avoiding to depend on hidden connectivity APIs in
+// tests.
+java_defaults {
+    name: "framework-connectivity-test-defaults",
+    sdk_version: "core_platform", // tests can use @CorePlatformApi's
+    libs: [
+        // order matters: classes in framework-connectivity are resolved before framework,
+        // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+        // stubs in framework
+        "framework-connectivity.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
+}
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index 2b45b3d..afaae1c 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -59,6 +59,7 @@
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testBuilder() {
+        val testExtraInfo = "mylegacyExtraInfo"
         val config = NetworkAgentConfig.Builder().apply {
             setExplicitlySelected(true)
             setLegacyType(ConnectivityManager.TYPE_ETHERNET)
@@ -67,6 +68,7 @@
             setUnvalidatedConnectivityAcceptable(true)
             setLegacyTypeName("TEST_NETWORK")
             if (isAtLeastS()) {
+                setLegacyExtraInfo(testExtraInfo)
                 setNat64DetectionEnabled(false)
                 setProvisioningNotificationEnabled(false)
                 setBypassableVpn(true)
@@ -80,6 +82,7 @@
         assertTrue(config.isUnvalidatedConnectivityAcceptable())
         assertEquals("TEST_NETWORK", config.getLegacyTypeName())
         if (isAtLeastS()) {
+            assertEquals(testExtraInfo, config.getLegacyExtraInfo())
             assertFalse(config.isNat64DetectionEnabled())
             assertFalse(config.isProvisioningNotificationEnabled())
             assertTrue(config.isBypassableVpn())
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index 340e6f9..7424157 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -198,4 +198,4 @@
         cb.expectCallback<OnUnavailable>() { nr.getNetworkSpecifier() == specifier }
         mCm.unregisterNetworkProvider(provider)
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 1afbfb0..7d30056 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -106,6 +106,8 @@
 
     private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
 
+    private static final String EMPTY_STRING = "";
+
     protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
     protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
 
@@ -206,6 +208,8 @@
                 final String resultData = getResultData();
                 if (resultData == null) {
                     Log.e(TAG, "Received null data from ordered intent");
+                    // Offer an empty string so that the code waiting for the result can return.
+                    result.offer(EMPTY_STRING);
                     return;
                 }
                 result.offer(resultData);
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index cd69b13..e28c33f 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -38,6 +38,7 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
+        ":ike-aes-xcbc",
     ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index a889c41..f993aed 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -20,6 +20,7 @@
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_DEVICE_CONFIG
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WATCH
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -57,6 +58,7 @@
 import junit.framework.AssertionFailedError
 import org.junit.After
 import org.junit.Assume.assumeTrue
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.runner.RunWith
 import java.util.concurrent.CompletableFuture
@@ -128,6 +130,7 @@
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
         assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+        assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
         utils.ensureWifiConnected()
         val cellNetwork = utils.connectToCell()
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 33f704f..1d84a1b 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -74,9 +74,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -107,9 +109,12 @@
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStateSnapshot;
 import android.net.NetworkUtils;
 import android.net.ProxyInfo;
 import android.net.SocketKeepalive;
+import android.net.TelephonyNetworkSpecifier;
 import android.net.TestNetworkInterface;
 import android.net.TestNetworkManager;
 import android.net.TetheringManager;
@@ -128,6 +133,7 @@
 import android.os.VintfRuntimeInfo;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArraySet;
@@ -145,6 +151,7 @@
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.testutils.CompatUtil;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.SkipPresubmit;
@@ -178,6 +185,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -439,6 +447,68 @@
         }
     }
 
+    private String getSubscriberIdForCellNetwork(Network cellNetwork) {
+        final NetworkCapabilities cellCaps = mCm.getNetworkCapabilities(cellNetwork);
+        final NetworkSpecifier specifier = cellCaps.getNetworkSpecifier();
+        assertTrue(specifier instanceof TelephonyNetworkSpecifier);
+        // Get subscription from Telephony network specifier.
+        final int subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        assertNotEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId);
+
+        // Get subscriber Id from telephony manager.
+        final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        return runWithShellPermissionIdentity(() -> tm.getSubscriberId(subId),
+                android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testGetAllNetworkStateSnapshots()
+            throws InterruptedException {
+        // Make sure cell is active to retrieve IMSI for verification in later step.
+        final Network cellNetwork = mCtsNetUtils.connectToCell();
+        final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
+        assertFalse(TextUtils.isEmpty(subscriberId));
+
+        // Verify the API cannot be called without proper permission.
+        assertThrows(SecurityException.class, () -> mCm.getAllNetworkStateSnapshots());
+
+        // Get all networks, verify the result of getAllNetworkStateSnapshots matches the result
+        // got from other APIs.
+        final Network[] networks = mCm.getAllNetworks();
+        assertGreaterOrEqual(networks.length, 1);
+        final List<NetworkStateSnapshot> snapshots = runWithShellPermissionIdentity(
+                () -> mCm.getAllNetworkStateSnapshots(), NETWORK_SETTINGS);
+        assertEquals(networks.length, snapshots.size());
+        for (final Network network : networks) {
+            // Can't use a lambda because it will cause the test to crash on R with
+            // NoClassDefFoundError.
+            NetworkStateSnapshot snapshot = null;
+            for (NetworkStateSnapshot item : snapshots) {
+                if (item.getNetwork().equals(network)) {
+                    snapshot = item;
+                    break;
+                }
+            }
+            assertNotNull(snapshot);
+            final NetworkCapabilities caps =
+                    Objects.requireNonNull(mCm.getNetworkCapabilities(network));
+            // Redact specifier of the capabilities of the snapshot before comparing since
+            // the result returned from getNetworkCapabilities always get redacted.
+            final NetworkSpecifier redactedSnapshotCapSpecifier =
+                    snapshot.getNetworkCapabilities().getNetworkSpecifier().redact();
+            assertEquals("", caps.describeImmutableDifferences(
+                    snapshot.getNetworkCapabilities()
+                            .setNetworkSpecifier(redactedSnapshotCapSpecifier)));
+            assertEquals(mCm.getLinkProperties(network), snapshot.getLinkProperties());
+            assertEquals(mCm.getNetworkInfo(network).getType(), snapshot.getLegacyType());
+
+            if (network.equals(cellNetwork)) {
+                assertEquals(subscriberId, snapshot.getSubscriberId());
+            }
+        }
+    }
+
     /**
      * Tests that connections can be opened on WiFi and cellphone networks,
      * and that they are made from different IP addresses.
@@ -591,6 +661,31 @@
                 .build();
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testIsPrivateDnsBroken() throws InterruptedException {
+        final String invalidPrivateDnsServer = "invalidhostname.example.com";
+        final String goodPrivateDnsServer = "dns.google";
+        mCtsNetUtils.storePrivateDnsSetting();
+        final TestableNetworkCallback cb = new TestableNetworkCallback();
+        mCm.registerNetworkCallback(makeWifiNetworkRequest(), cb);
+        try {
+            // Verifying the good private DNS sever
+            mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
+            final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+
+            // Verifying the broken private DNS sever
+            mCtsNetUtils.setPrivateDnsStrictMode(invalidPrivateDnsServer);
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> (((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+        } finally {
+            mCtsNetUtils.restorePrivateDnsSetting();
+        }
+    }
+
     /**
      * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to
      * see if we get a callback for the TRANSPORT_WIFI transport type being available.
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index c6d8d65..6e9f0cd 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -39,13 +39,11 @@
 import android.net.ConnectivityManager;
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
-import android.net.LinkAddress;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.ProxyInfo;
 import android.net.TestNetworkInterface;
-import android.net.TestNetworkManager;
 import android.net.VpnManager;
 import android.net.cts.util.CtsNetUtils;
 import android.os.Build;
@@ -439,41 +437,37 @@
         assertEquals(vpnNetwork, cb.lastLostNetwork);
     }
 
-    private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception {
-        // Non-final; these variables ensure we clean up properly after our test if we have
-        // allocated test network resources
-        final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
-        TestNetworkInterface testIface = null;
-        TestNetworkCallback tunNetworkCallback = null;
+    private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
+        private final boolean mTestIpv6Only;
 
-        try {
-            // Build underlying test network
-            testIface = tnm.createTunInterface(
-                    new LinkAddress[] {
-                            new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
-                            new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)});
+        /**
+         * Constructs the test
+         *
+         * @param testIpv6Only if true, builds a IPv6-only test; otherwise builds a IPv4-only test
+         */
+        VerifyStartStopVpnProfileTest(boolean testIpv6Only) {
+            mTestIpv6Only = testIpv6Only;
+        }
 
-            // Hold on to this callback to ensure network does not get reaped.
-            tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(
-                    testIface.getInterfaceName());
+        @Override
+        public void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6);
-        } finally {
-            // Make sure to stop the VPN profile. This is safe to call multiple times.
+            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only);
+        }
+
+        @Override
+        public void cleanupTest() {
             sVpnMgr.stopProvisionedVpnProfile();
+        }
 
-            if (testIface != null) {
-                testIface.getFileDescriptor().close();
-            }
-
-            if (tunNetworkCallback != null) {
-                sCM.unregisterNetworkCallback(tunNetworkCallback);
-            }
-
-            final Network testNetwork = tunNetworkCallback.currentNetwork;
-            if (testNetwork != null) {
-                tnm.teardownTestNetwork(testNetwork);
+        @Override
+        public InetAddress[] getTestNetworkAddresses() {
+            if (mTestIpv6Only) {
+                return new InetAddress[] {LOCAL_OUTER_6};
+            } else {
+                return new InetAddress[] {LOCAL_OUTER_4};
             }
         }
     }
@@ -483,9 +477,8 @@
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
         // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(() -> {
-            doTestStartStopVpnProfile(false);
-        });
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false)));
     }
 
     @Test
@@ -493,9 +486,8 @@
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
         // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(() -> {
-            doTestStartStopVpnProfile(true);
-        });
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true)));
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java
new file mode 100644
index 0000000..080f7f9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.IpSecAlgorithm.AUTH_AES_XCBC;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CTR;
+import static android.net.cts.PacketUtils.AES_CTR;
+import static android.net.cts.PacketUtils.AES_CTR_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CTR_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_20;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_28;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_36;
+import static android.net.cts.PacketUtils.AES_CTR_SALT_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC;
+import static android.net.cts.PacketUtils.AES_XCBC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_BLK_SIZE;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_ICV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_IV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_SALT_LEN;
+import static android.net.cts.PacketUtils.ESP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.cts.PacketUtils.BytePayload;
+import android.net.cts.PacketUtils.EspAeadCipher;
+import android.net.cts.PacketUtils.EspAuth;
+import android.net.cts.PacketUtils.EspAuthNull;
+import android.net.cts.PacketUtils.EspCipher;
+import android.net.cts.PacketUtils.EspCipherNull;
+import android.net.cts.PacketUtils.EspCryptCipher;
+import android.net.cts.PacketUtils.EspHeader;
+import android.net.cts.PacketUtils.IpHeader;
+import android.net.cts.PacketUtils.UdpHeader;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class IpSecAlgorithmImplTest extends IpSecBaseTest {
+    private static final InetAddress LOCAL_ADDRESS =
+            InetAddress.parseNumericAddress("2001:db8:1::1");
+    private static final InetAddress REMOTE_ADDRESS =
+            InetAddress.parseNumericAddress("2001:db8:1::2");
+
+    private static final int REMOTE_PORT = 12345;
+    private static final IpSecManager IPSEC_MANAGER =
+            InstrumentationRegistry.getContext().getSystemService(IpSecManager.class);
+
+    private static class CheckCryptoImplTest implements TestNetworkRunnable.Test {
+        private final IpSecAlgorithm mIpsecEncryptAlgo;
+        private final IpSecAlgorithm mIpsecAuthAlgo;
+        private final IpSecAlgorithm mIpsecAeadAlgo;
+        private final EspCipher mEspCipher;
+        private final EspAuth mEspAuth;
+
+        public CheckCryptoImplTest(
+                IpSecAlgorithm ipsecEncryptAlgo,
+                IpSecAlgorithm ipsecAuthAlgo,
+                IpSecAlgorithm ipsecAeadAlgo,
+                EspCipher espCipher,
+                EspAuth espAuth) {
+            mIpsecEncryptAlgo = ipsecEncryptAlgo;
+            mIpsecAuthAlgo = ipsecAuthAlgo;
+            mIpsecAeadAlgo = ipsecAeadAlgo;
+            mEspCipher = espCipher;
+            mEspAuth = espAuth;
+        }
+
+        private static byte[] buildTransportModeEspPayload(
+                int srcPort, int dstPort, int spi, EspCipher espCipher, EspAuth espAuth)
+                throws Exception {
+            final UdpHeader udpPayload =
+                    new UdpHeader(srcPort, dstPort, new BytePayload(TEST_DATA));
+            final IpHeader preEspIpHeader =
+                    getIpHeader(
+                            udpPayload.getProtocolId(), LOCAL_ADDRESS, REMOTE_ADDRESS, udpPayload);
+
+            final PacketUtils.EspHeader espPayload =
+                    new EspHeader(
+                            udpPayload.getProtocolId(),
+                            spi,
+                            1 /* sequence number */,
+                            udpPayload.getPacketBytes(preEspIpHeader),
+                            espCipher,
+                            espAuth);
+            return espPayload.getPacketBytes(preEspIpHeader);
+        }
+
+        @Override
+        public void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception {
+            final TunUtils tunUtils = new TunUtils(testIface.getFileDescriptor());
+            tunNetworkCallback.waitForAvailable();
+            final Network testNetwork = tunNetworkCallback.currentNetwork;
+
+            final IpSecTransform.Builder transformBuilder =
+                    new IpSecTransform.Builder(InstrumentationRegistry.getContext());
+            if (mIpsecAeadAlgo != null) {
+                transformBuilder.setAuthenticatedEncryption(mIpsecAeadAlgo);
+            } else {
+                if (mIpsecEncryptAlgo != null) {
+                    transformBuilder.setEncryption(mIpsecEncryptAlgo);
+                }
+                if (mIpsecAuthAlgo != null) {
+                    transformBuilder.setAuthentication(mIpsecAuthAlgo);
+                }
+            }
+
+            try (final IpSecManager.SecurityParameterIndex outSpi =
+                            IPSEC_MANAGER.allocateSecurityParameterIndex(REMOTE_ADDRESS);
+                    final IpSecManager.SecurityParameterIndex inSpi =
+                            IPSEC_MANAGER.allocateSecurityParameterIndex(LOCAL_ADDRESS);
+                    final IpSecTransform outTransform =
+                            transformBuilder.buildTransportModeTransform(LOCAL_ADDRESS, outSpi);
+                    final IpSecTransform inTransform =
+                            transformBuilder.buildTransportModeTransform(REMOTE_ADDRESS, inSpi);
+                    // Bind localSocket to a random available port.
+                    final DatagramSocket localSocket = new DatagramSocket(0)) {
+                IPSEC_MANAGER.applyTransportModeTransform(
+                        localSocket, IpSecManager.DIRECTION_IN, inTransform);
+                IPSEC_MANAGER.applyTransportModeTransform(
+                        localSocket, IpSecManager.DIRECTION_OUT, outTransform);
+
+                // Send ESP packet
+                final DatagramPacket outPacket =
+                        new DatagramPacket(
+                                TEST_DATA, 0, TEST_DATA.length, REMOTE_ADDRESS, REMOTE_PORT);
+                testNetwork.bindSocket(localSocket);
+                localSocket.send(outPacket);
+                final byte[] outEspPacket =
+                        tunUtils.awaitEspPacket(outSpi.getSpi(), false /* useEncap */);
+
+                // Remove transform for good hygiene
+                IPSEC_MANAGER.removeTransportModeTransforms(localSocket);
+
+                // Get the kernel-generated ESP payload
+                final byte[] outEspPayload = new byte[outEspPacket.length - IP6_HDRLEN];
+                System.arraycopy(outEspPacket, IP6_HDRLEN, outEspPayload, 0, outEspPayload.length);
+
+                // Get the IV of the kernel-generated ESP payload
+                final byte[] iv =
+                        Arrays.copyOfRange(
+                                outEspPayload, ESP_HDRLEN, ESP_HDRLEN + mEspCipher.ivLen);
+
+                // Build ESP payload using the kernel-generated IV and the user space crypto
+                // implementations
+                mEspCipher.updateIv(iv);
+                final byte[] expectedEspPayload =
+                        buildTransportModeEspPayload(
+                                localSocket.getLocalPort(),
+                                REMOTE_PORT,
+                                outSpi.getSpi(),
+                                mEspCipher,
+                                mEspAuth);
+
+                // Compare user-space-generated and kernel-generated ESP payload
+                assertArrayEquals(expectedEspPayload, outEspPayload);
+            }
+        }
+
+        @Override
+        public void cleanupTest() {
+            // Do nothing
+        }
+
+        @Override
+        public InetAddress[] getTestNetworkAddresses() {
+            return new InetAddress[] {LOCAL_ADDRESS};
+        }
+    }
+
+    private void checkAesCtr(int keyLen) throws Exception {
+        final byte[] cryptKey = getKeyBytes(keyLen);
+
+        final IpSecAlgorithm ipsecEncryptAlgo =
+                new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CTR, cryptKey);
+        final EspCipher espCipher =
+                new EspCryptCipher(
+                        AES_CTR, AES_CTR_BLK_SIZE, cryptKey, AES_CTR_IV_LEN, AES_CTR_SALT_LEN);
+
+        runWithShellPermissionIdentity(new TestNetworkRunnable(new CheckCryptoImplTest(
+                ipsecEncryptAlgo, null /* ipsecAuthAlgo */, null /* ipsecAeadAlgo */,
+                espCipher, EspAuthNull.getInstance())));
+    }
+
+    @Test
+    public void testAesCtr160() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_20);
+    }
+
+    @Test
+    public void testAesCtr224() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_28);
+    }
+
+    @Test
+    public void testAesCtr288() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_36);
+    }
+
+    @Test
+    public void testAesXcbc() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final byte[] authKey = getKeyBytes(AES_XCBC_KEY_LEN);
+        final IpSecAlgorithm ipsecAuthAlgo =
+                new IpSecAlgorithm(IpSecAlgorithm.AUTH_AES_XCBC, authKey, AES_XCBC_ICV_LEN * 8);
+        final EspAuth espAuth = new EspAuth(AES_XCBC, authKey, AES_XCBC_ICV_LEN);
+
+        runWithShellPermissionIdentity(new TestNetworkRunnable(new CheckCryptoImplTest(
+                null /* ipsecEncryptAlgo */, ipsecAuthAlgo, null /* ipsecAeadAlgo */,
+                EspCipherNull.getInstance(), espAuth)));
+    }
+
+    @Test
+    public void testChaCha20Poly1305() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final byte[] cryptKey = getKeyBytes(CHACHA20_POLY1305_KEY_LEN);
+        final IpSecAlgorithm ipsecAeadAlgo =
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        cryptKey,
+                        CHACHA20_POLY1305_ICV_LEN * 8);
+        final EspAeadCipher espAead =
+                new EspAeadCipher(
+                        CHACHA20_POLY1305,
+                        CHACHA20_POLY1305_BLK_SIZE,
+                        cryptKey,
+                        CHACHA20_POLY1305_IV_LEN,
+                        CHACHA20_POLY1305_ICV_LEN,
+                        CHACHA20_POLY1305_SALT_LEN);
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(
+                        new CheckCryptoImplTest(
+                                null /* ipsecEncryptAlgo */,
+                                null /* ipsecAuthAlgo */,
+                                ipsecAeadAlgo,
+                                espAead,
+                                EspAuthNull.getInstance())));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 10e43e7..c54ee91 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -16,6 +16,14 @@
 
 package android.net.cts;
 
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_AES_GCM;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_MD5;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA1;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA256;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+
 import static org.junit.Assert.assertArrayEquals;
 
 import android.content.Context;
@@ -31,6 +39,12 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -42,12 +56,10 @@
 import java.net.Socket;
 import java.net.SocketException;
 import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 @RunWith(AndroidJUnit4.class)
 public class IpSecBaseTest {
 
@@ -71,6 +83,18 @@
         0x20, 0x21, 0x22, 0x23
     };
 
+    private static final Set<String> MANDATORY_IPSEC_ALGOS_SINCE_P = new HashSet<>();
+
+    static {
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(CRYPT_AES_CBC);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_MD5);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA1);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA256);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA384);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA512);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_CRYPT_AES_GCM);
+    }
+
     protected static final byte[] AUTH_KEY = getKey(256);
     protected static final byte[] CRYPT_KEY = getKey(256);
 
@@ -89,8 +113,24 @@
                                 .getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
+    /** Checks if an IPsec algorithm is enabled on the device */
+    protected static boolean hasIpSecAlgorithm(String algorithm) {
+        if (SdkLevel.isAtLeastS()) {
+            return IpSecAlgorithm.getSupportedAlgorithms().contains(algorithm);
+        } else {
+            return MANDATORY_IPSEC_ALGOS_SINCE_P.contains(algorithm);
+        }
+    }
+
+    protected static byte[] getKeyBytes(int byteLength) {
+        return Arrays.copyOf(KEY_DATA, byteLength);
+    }
+
     protected static byte[] getKey(int bitLength) {
-        return Arrays.copyOf(KEY_DATA, bitLength / 8);
+        if (bitLength % 8 != 0) {
+            throw new IllegalArgumentException("Invalid key length in bits" + bitLength);
+        }
+        return getKeyBytes(bitLength / 8);
     }
 
     protected static int getDomain(InetAddress address) {
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index d08f6e9..5f79a3e 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -16,10 +16,33 @@
 
 package android.net.cts;
 
+import static android.net.IpSecAlgorithm.AUTH_AES_CMAC;
+import static android.net.IpSecAlgorithm.AUTH_AES_XCBC;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_AES_GCM;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_MD5;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA1;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA256;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CTR;
 import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
 import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CMAC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_CMAC_KEY_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CTR_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_20;
 import static android.net.cts.PacketUtils.AES_GCM_BLK_SIZE;
 import static android.net.cts.PacketUtils.AES_GCM_IV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_BLK_SIZE;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_ICV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_IV_LEN;
+import static android.net.cts.PacketUtils.HMAC_SHA512_ICV_LEN;
+import static android.net.cts.PacketUtils.HMAC_SHA512_KEY_LEN;
 import static android.net.cts.PacketUtils.IP4_HDRLEN;
 import static android.net.cts.PacketUtils.IP6_HDRLEN;
 import static android.net.cts.PacketUtils.TCP_HDRLEN_WITH_TIMESTAMP_OPT;
@@ -27,15 +50,20 @@
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
+import static com.android.compatibility.common.util.PropertyUtil.getVendorApiLevel;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.net.IpSecAlgorithm;
 import android.net.IpSecManager;
 import android.net.IpSecTransform;
 import android.net.TrafficStats;
+import android.os.Build;
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -44,8 +72,11 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.SkipPresubmit;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -56,10 +87,15 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 public class IpSecManagerTest extends IpSecBaseTest {
+    @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
     private static final String TAG = IpSecManagerTest.class.getSimpleName();
 
@@ -417,8 +453,12 @@
         switch (cryptOrAead.getName()) {
             case IpSecAlgorithm.CRYPT_AES_CBC:
                 return AES_CBC_IV_LEN;
+            case IpSecAlgorithm.CRYPT_AES_CTR:
+                return AES_CTR_IV_LEN;
             case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
                 return AES_GCM_IV_LEN;
+            case IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305:
+                return CHACHA20_POLY1305_IV_LEN;
             default:
                 throw new IllegalArgumentException(
                         "IV length unknown for algorithm" + cryptOrAead.getName());
@@ -433,8 +473,12 @@
         switch (cryptOrAead.getName()) {
             case IpSecAlgorithm.CRYPT_AES_CBC:
                 return AES_CBC_BLK_SIZE;
+            case IpSecAlgorithm.CRYPT_AES_CTR:
+                return AES_CTR_BLK_SIZE;
             case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
                 return AES_GCM_BLK_SIZE;
+            case IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305:
+                return CHACHA20_POLY1305_BLK_SIZE;
             default:
                 throw new IllegalArgumentException(
                         "Blk size unknown for algorithm" + cryptOrAead.getName());
@@ -516,7 +560,6 @@
             int blkSize,
             int truncLenBits)
             throws Exception {
-
         int innerPacketSize = TEST_DATA.length + transportHdrLen + ipHdrLen;
         int outerPacketSize =
                 PacketUtils.calculateEspPacketSize(
@@ -663,6 +706,41 @@
     //     checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, true, 1000);
     // }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testGetSupportedAlgorithms() throws Exception {
+        final Map<String, Integer> algoToRequiredMinSdk = new HashMap<>();
+        algoToRequiredMinSdk.put(CRYPT_AES_CBC, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_MD5, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA1, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA256, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA384, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA512, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_CRYPT_AES_GCM, Build.VERSION_CODES.P);
+
+        // TODO: b/170424293 Use Build.VERSION_CODES.S when is finalized
+        algoToRequiredMinSdk.put(CRYPT_AES_CTR, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_AES_CMAC, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_AES_XCBC, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_CRYPT_CHACHA20_POLY1305, Build.VERSION_CODES.R + 1);
+
+        final Set<String> supportedAlgos = IpSecAlgorithm.getSupportedAlgorithms();
+
+        // Verify all supported algorithms are valid
+        for (String algo : supportedAlgos) {
+            assertTrue("Found invalid algo " + algo, algoToRequiredMinSdk.keySet().contains(algo));
+        }
+
+        // Verify all mandatory algorithms are supported
+        for (Entry<String, Integer> entry : algoToRequiredMinSdk.entrySet()) {
+            if (Math.min(getFirstApiLevel(), getVendorApiLevel()) >= entry.getValue()) {
+                assertTrue(
+                        "Fail to support " + entry.getKey(),
+                        supportedAlgos.contains(entry.getKey()));
+            }
+        }
+    }
+
     @Test
     public void testInterfaceCountersUdp4() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
@@ -849,6 +927,152 @@
         checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
     }
 
+    private static IpSecAlgorithm buildCryptAesCtr() throws Exception {
+        return new IpSecAlgorithm(CRYPT_AES_CTR, getKeyBytes(AES_CTR_KEY_LEN_20));
+    }
+
+    private static IpSecAlgorithm buildAuthHmacSha512() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_HMAC_SHA512, getKeyBytes(HMAC_SHA512_KEY_LEN), HMAC_SHA512_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Tcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+    public void testAesCtrHmacSha512Tcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    private static IpSecAlgorithm buildCryptAesCbc() throws Exception {
+        return new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+    }
+
+    private static IpSecAlgorithm buildAuthAesXcbc() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_AES_XCBC, getKeyBytes(AES_XCBC_KEY_LEN), AES_XCBC_ICV_LEN * 8);
+    }
+
+    private static IpSecAlgorithm buildAuthAesCmac() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_AES_CMAC, getKeyBytes(AES_CMAC_KEY_LEN), AES_CMAC_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcTcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+    public void testAesCbcAesXCbcTcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacTcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+    public void testAesCbcAesCmacTcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
     @Test
     public void testAesGcm64Tcp4() throws Exception {
         IpSecAlgorithm authCrypt =
@@ -948,6 +1172,48 @@
         checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
     }
 
+    private static IpSecAlgorithm buildAuthCryptChaCha20Poly1305() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_CRYPT_CHACHA20_POLY1305, AEAD_KEY, CHACHA20_POLY1305_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Tcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec")
+    public void testChaCha20Poly1305Tcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
     @Test
     public void testAesCbcHmacMd5Tcp4UdpEncap() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
@@ -1029,6 +1295,66 @@
     }
 
     @Test
+    public void testAesCtrHmacSha512Tcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcTcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_XCBC, getKey(128), 96);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_XCBC, getKey(128), 96);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacTcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_CMAC, getKey(128), 96);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_CMAC, getKey(128), 96);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
     public void testAesGcm64Tcp4UdpEncap() throws Exception {
         IpSecAlgorithm authCrypt =
                 new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
@@ -1077,6 +1403,24 @@
     }
 
     @Test
+    public void testChaCha20Poly1305Tcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+    }
+
+    @Test
     public void testCryptUdp4() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
         checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index dac2e5c..9017f1b 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,6 +15,7 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.app.Instrumentation
 import android.content.Context
 import android.net.ConnectivityManager
@@ -68,14 +69,19 @@
 import android.os.HandlerThread
 import android.os.Looper
 import android.os.Message
+import android.os.SystemClock
+import android.telephony.TelephonyManager
 import android.util.DebugUtils.valueToString
 import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.ThrowingSupplier
 import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.CompatUtil
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.junit.After
@@ -114,6 +120,7 @@
 // requests filed by the test and should never match normal internet requests. 70 is the default
 // score of Ethernet networks, it's as good a value as any other.
 private const val TEST_NETWORK_SCORE = 70
+private const val WORSE_NETWORK_SCORE = 65
 private const val BETTER_NETWORK_SCORE = 75
 private const val FAKE_NET_ID = 1098
 private val instrumentation: Instrumentation
@@ -329,7 +336,8 @@
         context: Context = realContext,
         name: String? = null,
         initialNc: NetworkCapabilities? = null,
-        initialLp: LinkProperties? = null
+        initialLp: LinkProperties? = null,
+        initialConfig: NetworkAgentConfig? = null
     ): TestableNetworkAgent {
         val nc = initialNc ?: NetworkCapabilities().apply {
             addTransportType(TRANSPORT_TEST)
@@ -349,21 +357,25 @@
             addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
         }
-        val config = NetworkAgentConfig.Builder().build()
+        val config = initialConfig ?: NetworkAgentConfig.Builder().build()
         return TestableNetworkAgent(context, mHandlerThread.looper, nc, lp, config).also {
             agentsToCleanUp.add(it)
         }
     }
 
-    private fun createConnectedNetworkAgent(context: Context = realContext, name: String? = null):
-            Pair<TestableNetworkAgent, TestableNetworkCallback> {
+    private fun createConnectedNetworkAgent(
+        context: Context = realContext,
+        name: String? = null,
+        initialConfig: NetworkAgentConfig? = null
+    ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val request: NetworkRequest = NetworkRequest.Builder()
                 .clearCapabilities()
                 .addTransportType(TRANSPORT_TEST)
                 .build()
         val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
         requestNetwork(request, callback)
-        val agent = createNetworkAgent(context, name)
+        val config = initialConfig ?: NetworkAgentConfig.Builder().build()
+        val agent = createNetworkAgent(context, name, initialConfig = config)
         agent.setTeardownDelayMillis(0)
         agent.register()
         agent.markConnected()
@@ -376,6 +388,49 @@
     }
 
     @Test
+    fun testSetSubtypeNameAndExtraInfoByAgentConfig() {
+        val subtypeLTE = TelephonyManager.NETWORK_TYPE_LTE
+        val subtypeNameLTE = "LTE"
+        val legacyExtraInfo = "mylegacyExtraInfo"
+        val config = NetworkAgentConfig.Builder()
+                .setLegacySubType(subtypeLTE)
+                .setLegacySubTypeName(subtypeNameLTE)
+                .setLegacyExtraInfo(legacyExtraInfo).build()
+        val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
+        val networkInfo = mCM.getNetworkInfo(agent.network)
+        assertEquals(subtypeLTE, networkInfo.getSubtype())
+        assertEquals(subtypeNameLTE, networkInfo.getSubtypeName())
+        assertEquals(legacyExtraInfo, config.getLegacyExtraInfo())
+    }
+
+    @Test
+    fun testSetLegacySubtypeInNetworkAgent() {
+        val subtypeLTE = TelephonyManager.NETWORK_TYPE_LTE
+        val subtypeUMTS = TelephonyManager.NETWORK_TYPE_UMTS
+        val subtypeNameLTE = "LTE"
+        val subtypeNameUMTS = "UMTS"
+        val config = NetworkAgentConfig.Builder()
+                .setLegacySubType(subtypeLTE)
+                .setLegacySubTypeName(subtypeNameLTE).build()
+        val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
+            callback.expectAvailableThenValidatedCallbacks(agent.network)
+            agent.setLegacySubtype(subtypeUMTS, subtypeNameUMTS)
+
+            // There is no callback when networkInfo changes,
+            // so use the NetworkCapabilities callback to ensure
+            // that networkInfo is ready for verification.
+            val nc = NetworkCapabilities(agent.nc)
+            nc.addCapability(NET_CAPABILITY_NOT_METERED)
+            agent.sendNetworkCapabilities(nc)
+            callback.expectCapabilitiesThat(agent.network) {
+                it.hasCapability(NET_CAPABILITY_NOT_METERED)
+            }
+            val networkInfo = mCM.getNetworkInfo(agent.network)
+            assertEquals(subtypeUMTS, networkInfo.getSubtype())
+            assertEquals(subtypeNameUMTS, networkInfo.getSubtypeName())
+    }
+
+    @Test
     fun testConnectAndUnregister() {
         val (agent, callback) = createConnectedNetworkAgent()
         callback.expectAvailableThenValidatedCallbacks(agent.network)
@@ -543,16 +598,23 @@
         // Connect the first Network
         createConnectedNetworkAgent(name = name1).let { (agent1, _) ->
             callback.expectAvailableThenValidatedCallbacks(agent1.network)
-            // Upgrade agent1 to a better score so that there is no ambiguity when
-            // agent2 connects that agent1 is still better
-            agent1.sendNetworkScore(BETTER_NETWORK_SCORE - 1)
+            // If using the int ranking, agent1 must be upgraded to a better score so that there is
+            // no ambiguity when agent2 connects that agent1 is still better. If using policy
+            // ranking, this is not necessary.
+            agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+                    .build())
             // Connect the second agent
             createConnectedNetworkAgent(name = name2).let { (agent2, _) ->
                 agent2.markConnected()
-                // The callback should not see anything yet
+                // The callback should not see anything yet. With int ranking, agent1 was upgraded
+                // to a stronger score beforehand. With policy ranking, agent1 is preferred by
+                // virtue of already satisfying the request.
                 callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
-                // Now update the score and expect the callback now prefers agent2
-                agent2.sendNetworkScore(BETTER_NETWORK_SCORE)
+                // Now downgrade the score and expect the callback now prefers agent2
+                agent1.sendNetworkScore(NetworkScore.Builder()
+                        .setLegacyInt(WORSE_NETWORK_SCORE)
+                        .setExiting(true)
+                        .build())
                 callback.expectCallback<Available>(agent2.network)
             }
         }
@@ -768,4 +830,96 @@
 
         // tearDown() will unregister the requests and agents
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testSetLingerDuration() {
+        // This test will create two networks and check that the one with the stronger
+        // score wins out for a request that matches them both. And the weaker agent will
+        // be disconnected after customized linger duration.
+
+        // Connect the first Network
+        val name1 = UUID.randomUUID().toString()
+        val name2 = UUID.randomUUID().toString()
+        val (agent1, callback) = createConnectedNetworkAgent(name = name1)
+        callback.expectAvailableThenValidatedCallbacks(agent1.network!!)
+        // Downgrade agent1 to a worse score so that there is no ambiguity when
+        // agent2 connects.
+        agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(WORSE_NETWORK_SCORE)
+                .setExiting(true).build())
+
+        // Verify invalid linger duration cannot be set.
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(-1))
+        }
+        assertFailsWith<IllegalArgumentException> { agent1.setLingerDuration(Duration.ZERO) }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(Integer.MIN_VALUE.toLong()))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong() + 1))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agent1.setLingerDuration(Duration.ofMillis(
+                    NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+        }
+        // Verify valid linger timer can be set, but it should not take effect since the network
+        // is still needed.
+        agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong()))
+        callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        // Set to the value we want to verify the functionality.
+        agent1.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
+        // Make a listener which can observe agent1 lost later.
+        val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        registerNetworkCallback(NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
+                .build(), callbackWeaker)
+        callbackWeaker.expectAvailableCallbacks(agent1.network!!)
+
+        // Connect the second agent with a score better than agent1. Verify the callback for
+        // agent1 sees the linger expiry while the callback for both sees the winner.
+        // Record linger start timestamp prior to send score to prevent possible race, the actual
+        // timestamp should be slightly late than this since the service handles update
+        // network score asynchronously.
+        val lingerStart = SystemClock.elapsedRealtime()
+        val agent2 = createNetworkAgent(name = name2)
+        agent2.register()
+        agent2.markConnected()
+        callback.expectAvailableCallbacks(agent2.network!!)
+        callbackWeaker.expectCallback<Losing>(agent1.network!!)
+        val expectedRemainingLingerDuration = lingerStart +
+                NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
+        // If the available callback is too late. The remaining duration will be reduced.
+        assertTrue(expectedRemainingLingerDuration > 0,
+                "expected remaining linger duration is $expectedRemainingLingerDuration")
+        callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
+        callbackWeaker.expectCallback<Lost>(agent1.network!!)
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testSetSubscriberId() {
+        val name = "TEST-AGENT"
+        val imsi = UUID.randomUUID().toString()
+        val config = NetworkAgentConfig.Builder().setSubscriberId(imsi).build()
+
+        val request: NetworkRequest = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
+                .build()
+        val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        requestNetwork(request, callback)
+
+        val agent = createNetworkAgent(name = name, initialConfig = config)
+        agent.register()
+        agent.markConnected()
+        callback.expectAvailableThenValidatedCallbacks(agent.network!!)
+        val snapshots = runWithShellPermissionIdentity(ThrowingSupplier {
+                mCM!!.allNetworkStateSnapshots }, NETWORK_SETTINGS)
+        val testNetworkSnapshot = snapshots.findLast { it.network == agent.network }
+        assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 0aedecb..0b3bad4 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -19,6 +19,8 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import com.android.internal.net.ipsec.ike.crypto.AesXCbcImpl;
+
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -27,6 +29,8 @@
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
@@ -43,21 +47,61 @@
     static final int UDP_HDRLEN = 8;
     static final int TCP_HDRLEN = 20;
     static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
+    static final int ESP_HDRLEN = 8;
+    static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
+    static final int ESP_TRAILER_LEN = 2;
 
     // Not defined in OsConstants
     static final int IPPROTO_IPV4 = 4;
     static final int IPPROTO_ESP = 50;
 
     // Encryption parameters
-    static final int AES_GCM_IV_LEN = 8;
     static final int AES_CBC_IV_LEN = 16;
-    static final int AES_GCM_BLK_SIZE = 4;
     static final int AES_CBC_BLK_SIZE = 16;
+    static final int AES_CTR_SALT_LEN = 4;
+
+    static final int AES_CTR_KEY_LEN_20 = 20;
+    static final int AES_CTR_KEY_LEN_28 = 28;
+    static final int AES_CTR_KEY_LEN_36 = 36;
+    static final int AES_CTR_BLK_SIZE = ESP_BLK_SIZE;
+    static final int AES_CTR_IV_LEN = 8;
+
+    // AEAD parameters
+    static final int AES_GCM_IV_LEN = 8;
+    static final int AES_GCM_BLK_SIZE = 4;
+    static final int CHACHA20_POLY1305_KEY_LEN = 36;
+    static final int CHACHA20_POLY1305_BLK_SIZE = ESP_BLK_SIZE;
+    static final int CHACHA20_POLY1305_IV_LEN = 8;
+    static final int CHACHA20_POLY1305_SALT_LEN = 4;
+    static final int CHACHA20_POLY1305_ICV_LEN = 16;
+
+    // Authentication parameters
+    static final int HMAC_SHA256_ICV_LEN = 16;
+    static final int HMAC_SHA512_KEY_LEN = 64;
+    static final int HMAC_SHA512_ICV_LEN = 32;
+    static final int AES_XCBC_KEY_LEN = 16;
+    static final int AES_XCBC_ICV_LEN = 12;
+    static final int AES_CMAC_KEY_LEN = 16;
+    static final int AES_CMAC_ICV_LEN = 12;
+
+    // Block counter field should be 32 bits and starts from value one as per RFC 3686
+    static final byte[] AES_CTR_INITIAL_COUNTER = new byte[] {0x00, 0x00, 0x00, 0x01};
 
     // Encryption algorithms
     static final String AES = "AES";
     static final String AES_CBC = "AES/CBC/NoPadding";
+    static final String AES_CTR = "AES/CTR/NoPadding";
+
+    // AEAD algorithms
+    static final String CHACHA20_POLY1305 = "ChaCha20/Poly1305/NoPadding";
+
+    // Authentication algorithms
+    static final String HMAC_MD5 = "HmacMD5";
+    static final String HMAC_SHA1 = "HmacSHA1";
     static final String HMAC_SHA_256 = "HmacSHA256";
+    static final String HMAC_SHA_384 = "HmacSHA384";
+    static final String HMAC_SHA_512 = "HmacSHA512";
+    static final String AES_XCBC = "AesXCbc";
 
     public interface Payload {
         byte[] getPacketBytes(IpHeader header) throws Exception;
@@ -310,8 +354,9 @@
         public final int nextHeader;
         public final int spi;
         public final int seqNum;
-        public final byte[] key;
         public final byte[] payload;
+        public final EspCipher cipher;
+        public final EspAuth auth;
 
         /**
          * Generic constructor for ESP headers.
@@ -322,11 +367,48 @@
          * calculated using the pre-encryption IP header
          */
         public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) {
+            this(nextHeader, spi, seqNum, payload, getDefaultCipher(key), getDefaultAuth(key));
+        }
+
+        /**
+         * Generic constructor for ESP headers that allows configuring encryption and authentication
+         * algortihms.
+         *
+         * <p>For Tunnel mode, payload will be a full IP header + attached payloads
+         *
+         * <p>For Transport mode, payload will be only the attached payloads, but with the checksum
+         * calculated using the pre-encryption IP header
+         */
+        public EspHeader(
+                int nextHeader,
+                int spi,
+                int seqNum,
+                byte[] payload,
+                EspCipher cipher,
+                EspAuth auth) {
             this.nextHeader = nextHeader;
             this.spi = spi;
             this.seqNum = seqNum;
-            this.key = key;
             this.payload = payload;
+            this.cipher = cipher;
+            this.auth = auth;
+
+            if (cipher instanceof EspCipherNull && auth instanceof EspAuthNull) {
+                throw new IllegalArgumentException("No algorithm is provided");
+            }
+
+            if (cipher instanceof EspAeadCipher && !(auth instanceof EspAuthNull)) {
+                throw new IllegalArgumentException(
+                        "AEAD is provided with an authentication" + " algorithm.");
+            }
+        }
+
+        private static EspCipher getDefaultCipher(byte[] key) {
+            return new EspCryptCipher(AES_CBC, AES_CBC_BLK_SIZE, key, AES_CBC_IV_LEN);
+        }
+
+        private static EspAuth getDefaultAuth(byte[] key) {
+            return new EspAuth(HMAC_SHA_256, key, HMAC_SHA256_ICV_LEN);
         }
 
         public int getProtocolId() {
@@ -334,9 +416,10 @@
         }
 
         public short length() {
-            // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len)
-            return (short)
-                    calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128);
+            final int icvLen =
+                    cipher instanceof EspAeadCipher ? ((EspAeadCipher) cipher).icvLen : auth.icvLen;
+            return calculateEspPacketSize(
+                    payload.length, cipher.ivLen, cipher.blockSize, icvLen * 8);
         }
 
         public byte[] getPacketBytes(IpHeader header) throws Exception {
@@ -350,58 +433,12 @@
             ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
             espPayloadBuffer.putInt(spi);
             espPayloadBuffer.putInt(seqNum);
-            espPayloadBuffer.put(getCiphertext(key));
 
-            espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16);
+            espPayloadBuffer.put(cipher.getCipherText(nextHeader, payload, spi, seqNum));
+            espPayloadBuffer.put(auth.getIcv(getByteArrayFromBuffer(espPayloadBuffer)));
+
             resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer));
         }
-
-        private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
-            Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256);
-            SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256);
-            sha256HMAC.init(authKey);
-
-            return sha256HMAC.doFinal(authenticatedSection);
-        }
-
-        /**
-         * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks
-         *
-         * <p>The ciphertext does NOT include the SPI/Sequence numbers, or the ICV.
-         */
-        private byte[] getCiphertext(byte[] key) throws GeneralSecurityException {
-            int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE);
-            ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
-            paddedPayload.put(payload);
-
-            // Add padding - consecutive integers from 0x01
-            int pad = 1;
-            while (paddedPayload.position() < paddedPayload.limit()) {
-                paddedPayload.put((byte) pad++);
-            }
-
-            paddedPayload.position(paddedPayload.limit() - 2);
-            paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length
-            paddedPayload.put((byte) nextHeader);
-
-            // Generate Initialization Vector
-            byte[] iv = new byte[AES_CBC_IV_LEN];
-            new SecureRandom().nextBytes(iv);
-            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
-            SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES);
-
-            // Encrypt payload
-            Cipher cipher = Cipher.getInstance(AES_CBC);
-            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
-            byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload));
-
-            // Build ciphertext
-            ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length);
-            cipherText.put(iv);
-            cipherText.put(encrypted);
-
-            return getByteArrayFromBuffer(cipherText);
-        }
     }
 
     private static int addAndWrapForChecksum(int currentChecksum, int value) {
@@ -418,15 +455,14 @@
         return (short) ((~val) & 0xffff);
     }
 
-    public static int calculateEspPacketSize(
+    public static short calculateEspPacketSize(
             int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) {
-        final int ESP_HDRLEN = 4 + 4; // SPI + Seq#
         final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length
-        payloadLen += cryptIvLength; // Initialization Vector
 
         // Align to block size of encryption algorithm
         payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize);
-        return payloadLen + ESP_HDRLEN + ICV_LEN;
+        payloadLen += cryptIvLength; // Initialization Vector
+        return (short) (payloadLen + ESP_HDRLEN + ICV_LEN);
     }
 
     private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) {
@@ -457,6 +493,236 @@
         }
     }
 
+    public abstract static class EspCipher {
+        protected static final int SALT_LEN_UNUSED = 0;
+
+        public final String algoName;
+        public final int blockSize;
+        public final byte[] key;
+        public final int ivLen;
+        public final int saltLen;
+        protected byte[] iv;
+
+        public EspCipher(String algoName, int blockSize, byte[] key, int ivLen, int saltLen) {
+            this.algoName = algoName;
+            this.blockSize = blockSize;
+            this.key = key;
+            this.ivLen = ivLen;
+            this.saltLen = saltLen;
+            this.iv = getIv(ivLen);
+        }
+
+        public void updateIv(byte[] iv) {
+            this.iv = iv;
+        }
+
+        public static byte[] getPaddedPayload(int nextHeader, byte[] payload, int blockSize) {
+            final int paddedLen = calculateEspEncryptedLength(payload.length, blockSize);
+            final ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
+            paddedPayload.put(payload);
+
+            // Add padding - consecutive integers from 0x01
+            byte pad = 1;
+            while (paddedPayload.position() < paddedPayload.limit() - ESP_TRAILER_LEN) {
+                paddedPayload.put((byte) pad++);
+            }
+
+            // Add padding length and next header
+            paddedPayload.put((byte) (paddedLen - ESP_TRAILER_LEN - payload.length));
+            paddedPayload.put((byte) nextHeader);
+
+            return getByteArrayFromBuffer(paddedPayload);
+        }
+
+        private static byte[] getIv(int ivLen) {
+            final byte[] iv = new byte[ivLen];
+            new SecureRandom().nextBytes(iv);
+            return iv;
+        }
+
+        public abstract byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException;
+    }
+
+    public static class EspCipherNull extends EspCipher {
+        private static final String CRYPT_NULL = "CRYPT_NULL";
+        private static final int IV_LEN_UNUSED = 0;
+        private static final byte[] KEY_UNUSED = new byte[0];
+
+        private static final EspCipherNull INSTANCE = new EspCipherNull();
+
+        private EspCipherNull() {
+            super(CRYPT_NULL, ESP_BLK_SIZE, KEY_UNUSED, IV_LEN_UNUSED, SALT_LEN_UNUSED);
+        }
+
+        public static EspCipherNull getInstance() {
+            return INSTANCE;
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            return getPaddedPayload(nextHeader, payload, blockSize);
+        }
+    }
+
+    public static class EspCryptCipher extends EspCipher {
+        public EspCryptCipher(String algoName, int blockSize, byte[] key, int ivLen) {
+            this(algoName, blockSize, key, ivLen, SALT_LEN_UNUSED);
+        }
+
+        public EspCryptCipher(String algoName, int blockSize, byte[] key, int ivLen, int saltLen) {
+            super(algoName, blockSize, key, ivLen, saltLen);
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            final IvParameterSpec ivParameterSpec;
+            final SecretKeySpec secretKeySpec;
+
+            if (AES_CBC.equals(algoName)) {
+                ivParameterSpec = new IvParameterSpec(iv);
+                secretKeySpec = new SecretKeySpec(key, algoName);
+            } else if (AES_CTR.equals(algoName)) {
+                // Provided key consists of encryption/decryption key plus 4-byte salt. Salt is used
+                // with ESP payload IV and initial block counter value to build IvParameterSpec.
+                final byte[] secretKey = Arrays.copyOfRange(key, 0, key.length - saltLen);
+                final byte[] salt = Arrays.copyOfRange(key, secretKey.length, key.length);
+                secretKeySpec = new SecretKeySpec(secretKey, algoName);
+
+                final ByteBuffer ivParameterBuffer =
+                        ByteBuffer.allocate(iv.length + saltLen + AES_CTR_INITIAL_COUNTER.length);
+                ivParameterBuffer.put(salt);
+                ivParameterBuffer.put(iv);
+                ivParameterBuffer.put(AES_CTR_INITIAL_COUNTER);
+                ivParameterSpec = new IvParameterSpec(ivParameterBuffer.array());
+            } else {
+                throw new IllegalArgumentException("Invalid algorithm " + algoName);
+            }
+
+            // Encrypt payload
+            final Cipher cipher = Cipher.getInstance(algoName);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            final byte[] encrypted =
+                    cipher.doFinal(getPaddedPayload(nextHeader, payload, blockSize));
+
+            // Build ciphertext
+            final ByteBuffer cipherText = ByteBuffer.allocate(iv.length + encrypted.length);
+            cipherText.put(iv);
+            cipherText.put(encrypted);
+
+            return getByteArrayFromBuffer(cipherText);
+        }
+    }
+
+    public static class EspAeadCipher extends EspCipher {
+        public final int icvLen;
+
+        public EspAeadCipher(
+                String algoName, int blockSize, byte[] key, int ivLen, int icvLen, int saltLen) {
+            super(algoName, blockSize, key, ivLen, saltLen);
+            this.icvLen = icvLen;
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            // Provided key consists of encryption/decryption key plus salt. Salt is used
+            // with ESP payload IV to build IvParameterSpec.
+            final byte[] secretKey = Arrays.copyOfRange(key, 0, key.length - saltLen);
+            final byte[] salt = Arrays.copyOfRange(key, secretKey.length, key.length);
+
+            final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, algoName);
+
+            final ByteBuffer ivParameterBuffer = ByteBuffer.allocate(saltLen + iv.length);
+            ivParameterBuffer.put(salt);
+            ivParameterBuffer.put(iv);
+            final IvParameterSpec ivParameterSpec = new IvParameterSpec(ivParameterBuffer.array());
+
+            final ByteBuffer aadBuffer = ByteBuffer.allocate(ESP_HDRLEN);
+            aadBuffer.putInt(spi);
+            aadBuffer.putInt(seqNum);
+
+            // Encrypt payload
+            final Cipher cipher = Cipher.getInstance(algoName);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            cipher.updateAAD(aadBuffer.array());
+            final byte[] encryptedTextAndIcv =
+                    cipher.doFinal(getPaddedPayload(nextHeader, payload, blockSize));
+
+            // Build ciphertext
+            final ByteBuffer cipherText =
+                    ByteBuffer.allocate(iv.length + encryptedTextAndIcv.length);
+            cipherText.put(iv);
+            cipherText.put(encryptedTextAndIcv);
+
+            return getByteArrayFromBuffer(cipherText);
+        }
+    }
+
+    public static class EspAuth {
+        public final String algoName;
+        public final byte[] key;
+        public final int icvLen;
+
+        private static final Set<String> SUPPORTED_HMAC_ALGOS = new HashSet<>();
+
+        static {
+            SUPPORTED_HMAC_ALGOS.add(HMAC_MD5);
+            SUPPORTED_HMAC_ALGOS.add(HMAC_SHA1);
+            SUPPORTED_HMAC_ALGOS.add(HMAC_SHA_256);
+            SUPPORTED_HMAC_ALGOS.add(HMAC_SHA_384);
+            SUPPORTED_HMAC_ALGOS.add(HMAC_SHA_512);
+        }
+
+        public EspAuth(String algoName, byte[] key, int icvLen) {
+            this.algoName = algoName;
+            this.key = key;
+            this.icvLen = icvLen;
+        }
+
+        public byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+            if (AES_XCBC.equals(algoName)) {
+                final Cipher aesCipher = Cipher.getInstance(AES_CBC);
+                return new AesXCbcImpl().mac(key, authenticatedSection, true /* needTruncation */);
+            } else if (SUPPORTED_HMAC_ALGOS.contains(algoName)) {
+                final Mac mac = Mac.getInstance(algoName);
+                final SecretKeySpec authKey = new SecretKeySpec(key, algoName);
+                mac.init(authKey);
+
+                final ByteBuffer buffer = ByteBuffer.wrap(mac.doFinal(authenticatedSection));
+                final byte[] icv = new byte[icvLen];
+                buffer.get(icv);
+                return icv;
+            } else {
+                throw new IllegalArgumentException("Invalid algorithm: " + algoName);
+            }
+        }
+    }
+
+    public static class EspAuthNull extends EspAuth {
+        private static final String AUTH_NULL = "AUTH_NULL";
+        private static final int ICV_LEN_UNUSED = 0;
+        private static final byte[] KEY_UNUSED = new byte[0];
+        private static final byte[] ICV_EMPTY = new byte[0];
+
+        private static final EspAuthNull INSTANCE = new EspAuthNull();
+
+        private EspAuthNull() {
+            super(AUTH_NULL, KEY_UNUSED, ICV_LEN_UNUSED);
+        }
+
+        public static EspAuthNull getInstance() {
+            return INSTANCE;
+        }
+
+        @Override
+        public byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+            return ICV_EMPTY;
+        }
+    }
+
     /*
      * Debug printing
      */
diff --git a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
new file mode 100644
index 0000000..0eb5644
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.cts.util.CtsNetUtils;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/** This class supports running a test with a test network. */
+public class TestNetworkRunnable implements ThrowingRunnable {
+    private static final int IP4_PREFIX_LEN = 32;
+    private static final int IP6_PREFIX_LEN = 128;
+
+    private static final InetAddress DEFAULT_ADDRESS_4 =
+            InetAddress.parseNumericAddress("192.2.0.2");
+    private static final InetAddress DEFAULT_ADDRESS_6 =
+            InetAddress.parseNumericAddress("2001:db8:1::2");
+
+    private static final Context sContext = InstrumentationRegistry.getContext();
+    private static final ConnectivityManager sCm =
+            sContext.getSystemService(ConnectivityManager.class);
+
+    private final Test mTest;
+
+    public TestNetworkRunnable(Test test) {
+        mTest = test;
+    }
+
+    private void runTest() throws Exception {
+        final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
+
+        // Non-final; these variables ensure we clean up properly after our test if we
+        // have allocated test network resources
+        TestNetworkInterface testIface = null;
+        TestNetworkCallback tunNetworkCallback = null;
+
+        final CtsNetUtils ctsNetUtils = new CtsNetUtils(sContext);
+        final InetAddress[] addresses = mTest.getTestNetworkAddresses();
+        final LinkAddress[] linkAddresses = new LinkAddress[addresses.length];
+        for (int i = 0; i < addresses.length; i++) {
+            InetAddress address = addresses[i];
+            if (address instanceof Inet4Address) {
+                linkAddresses[i] = new LinkAddress(address, IP4_PREFIX_LEN);
+            } else {
+                linkAddresses[i] = new LinkAddress(address, IP6_PREFIX_LEN);
+            }
+        }
+
+        try {
+            // Build underlying test network
+            testIface = tnm.createTunInterface(linkAddresses);
+
+            // Hold on to this callback to ensure network does not get reaped.
+            tunNetworkCallback = ctsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
+
+            mTest.runTest(testIface, tunNetworkCallback);
+        } finally {
+            try {
+                mTest.cleanupTest();
+            } catch (Exception e) {
+                // No action
+            }
+
+            if (testIface != null) {
+                testIface.getFileDescriptor().close();
+            }
+
+            if (tunNetworkCallback != null) {
+                sCm.unregisterNetworkCallback(tunNetworkCallback);
+            }
+
+            final Network testNetwork = tunNetworkCallback.currentNetwork;
+            if (testNetwork != null) {
+                tnm.teardownTestNetwork(testNetwork);
+            }
+        }
+    }
+
+    @Override
+    public void run() throws Exception {
+        if (sContext.checkSelfPermission(MANAGE_TEST_NETWORKS) == PERMISSION_GRANTED) {
+            runTest();
+        } else {
+            runWithShellPermissionIdentity(this::runTest, MANAGE_TEST_NETWORKS);
+        }
+    }
+
+    /** Interface for test caller to configure the test that will be run with a test network */
+    public interface Test {
+        /** Runs the test with a test network */
+        void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception;
+
+        /** Cleans up when the test is finished or interrupted */
+        void cleanupTest();
+
+        /** Returns the IP addresses that will be used by the test network */
+        default InetAddress[] getTestNetworkAddresses() {
+            return new InetAddress[] {DEFAULT_ADDRESS_4, DEFAULT_ADDRESS_6};
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 7887385..d8e39b4 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -147,6 +147,10 @@
         return espPkt; // We've found the packet we're looking for.
     }
 
+    public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
+        return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+    }
+
     private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
         // Check SPI byte by byte.
         return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index 88a2068..b5f1208 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -26,5 +26,6 @@
         "compatibility-device-util-axt",
         "junit",
         "net-tests-utils",
+        "modules-utils-build",
     ],
 }
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 d5a26c4..4abbecc 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
@@ -23,6 +23,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
 
+import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
@@ -55,7 +56,6 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
-import android.os.SystemProperties;
 import android.provider.Settings;
 import android.system.Os;
 import android.system.OsConstants;
@@ -87,6 +87,7 @@
     private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
     private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
     private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+    private static final String PRIVATE_DNS_MODE_STRICT = "hostname";
     public static final int HTTP_PORT = 80;
     public static final String TEST_HOST = "connectivitycheck.gstatic.com";
     public static final String HTTP_REQUEST =
@@ -116,8 +117,7 @@
     /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */
     public boolean hasIpsecTunnelsFeature() {
         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
-                || SystemProperties.getInt("ro.product.first_api_level", 0)
-                        >= Build.VERSION_CODES.Q;
+                || getFirstApiLevel() >= Build.VERSION_CODES.Q;
     }
 
     /**
@@ -524,12 +524,19 @@
     }
 
     public void restorePrivateDnsSetting() throws InterruptedException {
-        if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
-            return;
+        if (mOldPrivateDnsMode == null) {
+            fail("restorePrivateDnsSetting without storing settings first");
         }
         // restore private DNS setting
-        if ("hostname".equals(mOldPrivateDnsMode)) {
+        if (PRIVATE_DNS_MODE_STRICT.equals(mOldPrivateDnsMode)) {
+            // In case of invalid setting, set to opportunistic to avoid a bad state and fail
+            if (mOldPrivateDnsSpecifier == null) {
+                Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE,
+                        PRIVATE_DNS_MODE_OPPORTUNISTIC);
+                fail("Invalid private DNS setting: no hostname specified in strict mode");
+            }
             setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+
             awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
                     mCm.getActiveNetwork(),
                     mOldPrivateDnsSpecifier, true);
@@ -540,13 +547,14 @@
 
     public void setPrivateDnsStrictMode(String server) {
         // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
-        // that if the previous private DNS mode was not "hostname", the system only sees one
+        // that if the previous private DNS mode was not strict, the system only sees one
         // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
         Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
         final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
-        if (!"hostname".equals(mode)) {
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+        // If current private DNS mode is strict, we only need to set PRIVATE_DNS_SPECIFIER.
+        if (!PRIVATE_DNS_MODE_STRICT.equals(mode)) {
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE,
+                    PRIVATE_DNS_MODE_STRICT);
         }
     }
 
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index e809550..17db179 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -41,6 +41,7 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkScore;
 import android.net.NetworkSpecifier;
 import android.net.QosFilter;
 import android.net.SocketKeepalive;
@@ -51,7 +52,6 @@
 import android.util.Range;
 
 import com.android.net.module.util.ArrayTrackRecord;
-import com.android.server.connectivity.ConnectivityConstants;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TestableNetworkCallback;
 
@@ -69,7 +69,7 @@
     private final ConditionVariable mDisconnected = new ConditionVariable();
     private final ConditionVariable mPreventReconnectReceived = new ConditionVariable();
     private final AtomicBoolean mConnected = new AtomicBoolean(false);
-    private int mScore;
+    private NetworkScore mScore;
     private NetworkAgent mNetworkAgent;
     private int mStartKeepaliveError = SocketKeepalive.ERROR_UNSUPPORTED;
     private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE;
@@ -90,23 +90,23 @@
         mNetworkCapabilities.addTransportType(transport);
         switch (transport) {
             case TRANSPORT_ETHERNET:
-                mScore = 70;
+                mScore = new NetworkScore.Builder().setLegacyInt(70).build();
                 break;
             case TRANSPORT_WIFI:
-                mScore = 60;
+                mScore = new NetworkScore.Builder().setLegacyInt(60).build();
                 break;
             case TRANSPORT_CELLULAR:
-                mScore = 50;
+                mScore = new NetworkScore.Builder().setLegacyInt(50).build();
                 break;
             case TRANSPORT_WIFI_AWARE:
-                mScore = 20;
+                mScore = new NetworkScore.Builder().setLegacyInt(20).build();
                 break;
             case TRANSPORT_VPN:
                 mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN);
                 // VPNs deduce the SUSPENDED capability from their underlying networks and there
                 // is no public API to let VPN services set it.
                 mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
-                mScore = ConnectivityConstants.VPN_DEFAULT_SCORE;
+                mScore = new NetworkScore.Builder().setLegacyInt(101).build();
                 break;
             default:
                 throw new UnsupportedOperationException("unimplemented network type");
@@ -199,12 +199,23 @@
         }
     }
 
+    public void setScore(@NonNull final NetworkScore score) {
+        mScore = score;
+        mNetworkAgent.sendNetworkScore(score);
+    }
+
     public void adjustScore(int change) {
-        mScore += change;
+        final int newLegacyScore = mScore.getLegacyInt() + change;
+        final NetworkScore.Builder builder = new NetworkScore.Builder()
+                .setLegacyInt(newLegacyScore);
+        if (mNetworkCapabilities.hasTransport(TRANSPORT_WIFI) && newLegacyScore < 50) {
+            builder.setExiting(true);
+        }
+        mScore = builder.build();
         mNetworkAgent.sendNetworkScore(mScore);
     }
 
-    public int getScore() {
+    public NetworkScore getScore() {
         return mScore;
     }
 
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 9a0c422..6c4bb90 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -47,12 +47,14 @@
 
 android_test {
     name: "FrameworksNetTests",
-    defaults: ["FrameworksNetTests-jni-defaults"],
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "FrameworksNetTests-jni-defaults",
+    ],
     srcs: [
         "java/**/*.java",
         "java/**/*.kt",
     ],
-    platform_apis: true,
     test_suites: ["device-tests"],
     certificate: "platform",
     jarjar_rules: "jarjar-rules.txt",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index d08b2f8..4c60ccf 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -42,7 +42,7 @@
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" />
     <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
-    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
     <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.NETWORK_STACK" />
     <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY" />
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 899295a..6bd2bd5 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -24,8 +24,8 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -52,6 +52,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class NetworkStatsManagerTest {
+    private static final String TEST_SUBSCRIBER_ID = "subid";
 
     private @Mock INetworkStatsService mService;
     private @Mock INetworkStatsSession mStatsSession;
@@ -69,7 +70,6 @@
 
     @Test
     public void testQueryDetails() throws RemoteException {
-        final String subscriberId = "subid";
         final long startTime = 1;
         final long endTime = 100;
         final int uid1 = 10001;
@@ -113,7 +113,7 @@
                 .then((InvocationOnMock inv) -> {
                     NetworkTemplate template = inv.getArgument(0);
                     assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
-                    assertEquals(subscriberId, template.getSubscriberId());
+                    assertEquals(TEST_SUBSCRIBER_ID, template.getSubscriberId());
                     return history1;
                 });
 
@@ -124,13 +124,13 @@
                 .then((InvocationOnMock inv) -> {
                     NetworkTemplate template = inv.getArgument(0);
                     assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
-                    assertEquals(subscriberId, template.getSubscriberId());
+                    assertEquals(TEST_SUBSCRIBER_ID, template.getSubscriberId());
                     return history2;
                 });
 
 
         NetworkStats stats = mManager.queryDetails(
-                ConnectivityManager.TYPE_MOBILE, subscriberId, startTime, endTime);
+                ConnectivityManager.TYPE_MOBILE, TEST_SUBSCRIBER_ID, startTime, endTime);
 
         NetworkStats.Bucket bucket = new NetworkStats.Bucket();
 
@@ -166,35 +166,30 @@
         assertFalse(stats.hasNextBucket());
     }
 
-    @Test
-    public void testQueryDetails_NoSubscriberId() throws RemoteException {
+    private void runQueryDetailsAndCheckTemplate(int networkType, String subscriberId,
+            NetworkTemplate expectedTemplate) throws RemoteException {
         final long startTime = 1;
         final long endTime = 100;
         final int uid1 = 10001;
         final int uid2 = 10002;
 
+        reset(mStatsSession);
         when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
         when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2 });
-
-        NetworkStats stats = mManager.queryDetails(
-                ConnectivityManager.TYPE_MOBILE, null, startTime, endTime);
-
         when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
                 anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyLong()))
                 .thenReturn(new NetworkStatsHistory(10, 0));
+        NetworkStats stats = mManager.queryDetails(
+                networkType, subscriberId, startTime, endTime);
 
         verify(mStatsSession, times(1)).getHistoryIntervalForUid(
-                argThat((NetworkTemplate t) ->
-                        // No subscriberId: MATCH_MOBILE_WILDCARD template
-                        t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD),
+                eq(expectedTemplate),
                 eq(uid1), eq(android.net.NetworkStats.SET_ALL),
                 eq(android.net.NetworkStats.TAG_NONE),
                 eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
 
         verify(mStatsSession, times(1)).getHistoryIntervalForUid(
-                argThat((NetworkTemplate t) ->
-                        // No subscriberId: MATCH_MOBILE_WILDCARD template
-                        t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD),
+                eq(expectedTemplate),
                 eq(uid2), eq(android.net.NetworkStats.SET_ALL),
                 eq(android.net.NetworkStats.TAG_NONE),
                 eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
@@ -202,6 +197,25 @@
         assertFalse(stats.hasNextBucket());
     }
 
+    @Test
+    public void testNetworkTemplateWhenRunningQueryDetails_NoSubscriberId() throws RemoteException {
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_MOBILE,
+                null /* subscriberId */, NetworkTemplate.buildTemplateMobileWildcard());
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                "" /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                null /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+    }
+
+    @Test
+    public void testNetworkTemplateWhenRunningQueryDetails_MergedCarrierWifi()
+            throws RemoteException {
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                TEST_SUBSCRIBER_ID,
+                NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL,
+                        TEST_SUBSCRIBER_ID));
+    }
+
     private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) {
         assertEquals(expected.uid, actual.getUid());
         assertEquals(expected.rxBytes, actual.getRxBytes());
diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
index fc739fb..bc6dbf2 100644
--- a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
+++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
@@ -24,7 +24,9 @@
 import static org.junit.Assert.fail;
 
 import android.net.util.KeepalivePacketDataUtil;
+import android.util.Log;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,8 +40,19 @@
     private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 1};
     private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 5};
 
+    private Log.TerribleFailureHandler mOriginalHandler;
+
     @Before
-    public void setUp() {}
+    public void setUp() {
+        // Terrible failures are logged when using deprecated methods on newer platforms
+        mOriginalHandler = Log.setWtfHandler((tag, what, sys) ->
+                Log.e(tag, "Terrible failure in test", what));
+    }
+
+    @After
+    public void tearDown() {
+        Log.setWtfHandler(mOriginalHandler);
+    }
 
     @Test
     public void testFromTcpKeepaliveStableParcelable() throws Exception {
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3a4cc1d..b0d849a 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -104,6 +104,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkScore.KEEP_CONNECTED_FOR_HANDOVER;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
@@ -117,6 +118,8 @@
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
 
+import static com.android.server.ConnectivityService.DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED;
+import static com.android.server.ConnectivityService.DEFAULT_NETWORK_PRIORITY_OEM;
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -299,8 +302,9 @@
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.module.util.CollectionUtils;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
-import com.android.server.connectivity.ConnectivityConstants;
+import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
 import com.android.server.connectivity.NetworkAgentInfo;
@@ -410,11 +414,13 @@
             UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID);
     private static final String CLAT_PREFIX = "v4-";
     private static final String MOBILE_IFNAME = "test_rmnet_data0";
+    private static final String CLAT_MOBILE_IFNAME = CLAT_PREFIX + MOBILE_IFNAME;
     private static final String WIFI_IFNAME = "test_wlan0";
     private static final String WIFI_WOL_IFNAME = "test_wlan_wol";
     private static final String VPN_IFNAME = "tun10042";
     private static final String TEST_PACKAGE_NAME = "com.android.test.package";
     private static final int TEST_PACKAGE_UID = 123;
+    private static final int TEST_PACKAGE_UID2 = 321;
     private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn";
 
     private static final String INTERFACE_NAME = "interface";
@@ -452,6 +458,7 @@
     private TestNetworkCallback mDefaultNetworkCallback;
     private TestNetworkCallback mSystemDefaultNetworkCallback;
     private TestNetworkCallback mProfileDefaultNetworkCallback;
+    private TestNetworkCallback mTestPackageDefaultNetworkCallback;
 
     // State variables required to emulate NetworkPolicyManagerService behaviour.
     private int mBlockedReasons = BLOCKED_REASON_NONE;
@@ -1027,8 +1034,6 @@
      * operations have been processed and test for them.
      */
     private static class MockNetworkFactory extends NetworkFactory {
-        private final ConditionVariable mNetworkStartedCV = new ConditionVariable();
-        private final ConditionVariable mNetworkStoppedCV = new ConditionVariable();
         private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
 
         static class RequestEntry {
@@ -1040,11 +1045,8 @@
             }
 
             static final class Add extends RequestEntry {
-                public final int factorySerialNumber;
-
-                Add(@NonNull final NetworkRequest request, final int factorySerialNumber) {
+                Add(@NonNull final NetworkRequest request) {
                     super(request);
-                    this.factorySerialNumber = factorySerialNumber;
                 }
             }
 
@@ -1053,6 +1055,11 @@
                     super(request);
                 }
             }
+
+            @Override
+            public String toString() {
+                return "RequestEntry [ " + getClass().getName() + " : " + request + " ]";
+            }
         }
 
         // History of received requests adds and removes.
@@ -1064,7 +1071,6 @@
             return obj;
         }
 
-
         public RequestEntry.Add expectRequestAdd() {
             return failIfNull((RequestEntry.Add) mRequestHistory.poll(TIMEOUT_MS,
                     it -> it instanceof RequestEntry.Add), "Expected request add");
@@ -1104,40 +1110,28 @@
 
         protected void startNetwork() {
             mNetworkStarted.set(true);
-            mNetworkStartedCV.open();
         }
 
         protected void stopNetwork() {
             mNetworkStarted.set(false);
-            mNetworkStoppedCV.open();
         }
 
         public boolean getMyStartRequested() {
             return mNetworkStarted.get();
         }
 
-        public ConditionVariable getNetworkStartedCV() {
-            mNetworkStartedCV.close();
-            return mNetworkStartedCV;
-        }
-
-        public ConditionVariable getNetworkStoppedCV() {
-            mNetworkStoppedCV.close();
-            return mNetworkStoppedCV;
-        }
 
         @Override
-        protected void handleAddRequest(NetworkRequest request, int score,
-                int factorySerialNumber) {
+        protected void needNetworkFor(NetworkRequest request) {
             mNetworkRequests.put(request.requestId, request);
-            super.handleAddRequest(request, score, factorySerialNumber);
-            mRequestHistory.add(new RequestEntry.Add(request, factorySerialNumber));
+            super.needNetworkFor(request);
+            mRequestHistory.add(new RequestEntry.Add(request));
         }
 
         @Override
-        protected void handleRemoveRequest(NetworkRequest request) {
+        protected void releaseNetworkFor(NetworkRequest request) {
             mNetworkRequests.remove(request.requestId);
-            super.handleRemoveRequest(request);
+            super.releaseNetworkFor(request);
             mRequestHistory.add(new RequestEntry.Remove(request));
         }
 
@@ -1171,6 +1165,10 @@
         return ranges;
     }
 
+    private Set<UidRange> uidRangesForUids(Collection<Integer> uids) {
+        return uidRangesForUids(CollectionUtils.toIntArray(uids));
+    }
+
     private static Looper startHandlerThreadAndReturnLooper() {
         final HandlerThread handlerThread = new HandlerThread("MockVpnThread");
         handlerThread.start();
@@ -1533,6 +1531,8 @@
     }
 
     private static final int PRIMARY_USER = 0;
+    private static final int SECONDARY_USER = 10;
+    private static final int TERTIARY_USER = 11;
     private static final UidRange PRIMARY_UIDRANGE =
             UidRange.createForUser(UserHandle.of(PRIMARY_USER));
     private static final int APP1_UID = UserHandle.getUid(PRIMARY_USER, 10100);
@@ -1541,6 +1541,8 @@
     private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER, "",
             UserInfo.FLAG_PRIMARY);
     private static final UserHandle PRIMARY_USER_HANDLE = new UserHandle(PRIMARY_USER);
+    private static final UserHandle SECONDARY_USER_HANDLE = new UserHandle(SECONDARY_USER);
+    private static final UserHandle TERTIARY_USER_HANDLE = new UserHandle(TERTIARY_USER);
 
     private static final int RESTRICTED_USER = 1;
     private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "",
@@ -2692,25 +2694,6 @@
         assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
 
-        // Bring up wifi with a score of 70.
-        // Cell is lingered because it would not satisfy any request, even if it validated.
-        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.adjustScore(50);
-        mWiFiNetworkAgent.connect(false);   // Score: 70
-        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
-        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
-        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
-
-        // Tear down wifi.
-        mWiFiNetworkAgent.disconnect();
-        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
-        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
-        defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
-
         // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but
         // it's arguably correct to linger it, since it was the default network before it validated.
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -3026,11 +3009,11 @@
         callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
-        // BUG: the network will no longer linger, even though it's validated and outscored.
-        // TODO: fix this.
         mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
         mEthernetNetworkAgent.connect(true);
-        callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+        callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent);
         assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         callback.assertNoCallback();
 
@@ -3082,8 +3065,9 @@
         }
 
         NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addTransportType(TRANSPORT_CELLULAR);
         filter.addCapability(capability);
-        // Add NOT_VCN_MANAGED capability into filter unconditionally since some request will add
+        // Add NOT_VCN_MANAGED capability into filter unconditionally since some requests will add
         // NOT_VCN_MANAGED automatically but not for NetworkCapabilities,
         // see {@code NetworkCapabilities#deduceNotVcnManagedCapability} for more details.
         filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
@@ -3091,47 +3075,67 @@
         handlerThread.start();
         final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
                 mServiceContext, "testFactory", filter, mCsHandlerThread);
-        testFactory.setScoreFilter(40);
-        ConditionVariable cv = testFactory.getNetworkStartedCV();
+        testFactory.setScoreFilter(45);
         testFactory.register();
-        testFactory.expectRequestAdd();
-        testFactory.assertRequestCountEquals(1);
-        int expectedRequestCount = 1;
-        NetworkCallback networkCallback = null;
-        // For non-INTERNET capabilities we cannot rely on the default request being present, so
-        // add one.
+
+        final NetworkCallback networkCallback;
         if (capability != NET_CAPABILITY_INTERNET) {
+            // If the capability passed in argument is part of the default request, then the
+            // factory will see the default request. Otherwise the filter will prevent the
+            // factory from seeing it. In that case, add a request so it can be tested.
             assertFalse(testFactory.getMyStartRequested());
             NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build();
             networkCallback = new NetworkCallback();
             mCm.requestNetwork(request, networkCallback);
-            expectedRequestCount++;
-            testFactory.expectRequestAdd();
+        } else {
+            networkCallback = null;
         }
-        waitFor(cv);
-        testFactory.assertRequestCountEquals(expectedRequestCount);
+        testFactory.expectRequestAdd();
+        testFactory.assertRequestCountEquals(1);
         assertTrue(testFactory.getMyStartRequested());
 
         // Now bring in a higher scored network.
         TestNetworkAgentWrapper testAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
-        // Rather than create a validated network which complicates things by registering it's
-        // own NetworkRequest during startup, just bump up the score to cancel out the
-        // unvalidated penalty.
-        testAgent.adjustScore(40);
-        cv = testFactory.getNetworkStoppedCV();
-
-        // When testAgent connects, ConnectivityService will re-send us all current requests with
-        // the new score. There are expectedRequestCount such requests, and we must wait for all of
-        // them.
-        testAgent.connect(false);
+        // When testAgent connects, because of its score (50 legacy int / cell transport)
+        // it will beat or equal the testFactory's offer, so the request will be removed.
+        // Note the agent as validated only if the capability is INTERNET, as it's the only case
+        // where it makes sense.
+        testAgent.connect(NET_CAPABILITY_INTERNET == capability /* validated */);
         testAgent.addCapability(capability);
-        waitFor(cv);
-        testFactory.expectRequestAdds(expectedRequestCount);
-        testFactory.assertRequestCountEquals(expectedRequestCount);
+        testFactory.expectRequestRemove();
+        testFactory.assertRequestCountEquals(0);
         assertFalse(testFactory.getMyStartRequested());
 
+        // Add a request and make sure it's not sent to the factory, because the agent
+        // is satisfying it better.
+        final NetworkCallback cb = new ConnectivityManager.NetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder().addCapability(capability).build(), cb);
+        expectNoRequestChanged(testFactory);
+        testFactory.assertRequestCountEquals(0);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // If using legacy scores, make the test agent weak enough to have the exact same score as
+        // the factory (50 for cell - 5 adjustment). Make sure the factory doesn't see the request.
+        // If not using legacy score, this is a no-op and the "same score removes request" behavior
+        // has already been tested above.
+        testAgent.adjustScore(-5);
+        expectNoRequestChanged(testFactory);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Make the test agent weak enough that the factory will see the two requests (the one that
+        // was just sent, and either the default one or the one sent at the top of this test if
+        // the default won't be seen).
+        testAgent.setScore(new NetworkScore.Builder().setLegacyInt(2).setExiting(true).build());
+        testFactory.expectRequestAdds(2);
+        testFactory.assertRequestCountEquals(2);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Now unregister and make sure the request is removed.
+        mCm.unregisterNetworkCallback(cb);
+        testFactory.expectRequestRemove();
+
         // Bring in a bunch of requests.
-        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
+        assertEquals(1, testFactory.getMyRequestCount());
         ConnectivityManager.NetworkCallback[] networkCallbacks =
                 new ConnectivityManager.NetworkCallback[10];
         for (int i = 0; i< networkCallbacks.length; i++) {
@@ -3141,24 +3145,28 @@
             mCm.requestNetwork(builder.build(), networkCallbacks[i]);
         }
         testFactory.expectRequestAdds(10);
-        testFactory.assertRequestCountEquals(10 + expectedRequestCount);
-        assertFalse(testFactory.getMyStartRequested());
+        testFactory.assertRequestCountEquals(11); // +1 for the default/test specific request
+        assertTrue(testFactory.getMyStartRequested());
 
         // Remove the requests.
         for (int i = 0; i < networkCallbacks.length; i++) {
             mCm.unregisterNetworkCallback(networkCallbacks[i]);
         }
         testFactory.expectRequestRemoves(10);
-        testFactory.assertRequestCountEquals(expectedRequestCount);
+        testFactory.assertRequestCountEquals(1);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Adjust the agent score up again. Expect the request to be withdrawn.
+        testAgent.setScore(new NetworkScore.Builder().setLegacyInt(50).build());
+        testFactory.expectRequestRemove();
+        testFactory.assertRequestCountEquals(0);
         assertFalse(testFactory.getMyStartRequested());
 
         // Drop the higher scored network.
-        cv = testFactory.getNetworkStartedCV();
         testAgent.disconnect();
-        waitFor(cv);
-        testFactory.expectRequestAdds(expectedRequestCount);
-        testFactory.assertRequestCountEquals(expectedRequestCount);
-        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
+        testFactory.expectRequestAdd();
+        testFactory.assertRequestCountEquals(1);
+        assertEquals(1, testFactory.getMyRequestCount());
         assertTrue(testFactory.getMyStartRequested());
 
         testFactory.terminate();
@@ -3190,9 +3198,47 @@
     }
 
     @Test
-    public void testNetworkFactoryUnregister() throws Exception {
+    public void testRegisterIgnoringScore() throws Exception {
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(90).build());
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        // Make sure the factory sees the default network
         final NetworkCapabilities filter = new NetworkCapabilities();
-        filter.clearAll();
+        filter.addTransportType(TRANSPORT_CELLULAR);
+        filter.addCapability(NET_CAPABILITY_INTERNET);
+        filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
+        handlerThread.start();
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.register();
+
+        final MockNetworkFactory testFactoryAll = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactoryAll", filter, mCsHandlerThread);
+        testFactoryAll.registerIgnoringScore();
+
+        // The regular test factory should not see the request, because WiFi is stronger than cell.
+        expectNoRequestChanged(testFactory);
+        // With ignoringScore though the request is seen.
+        testFactoryAll.expectRequestAdd();
+
+        // The legacy int will be ignored anyway, set the only other knob to true
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(110)
+                .setTransportPrimary(true).build());
+
+        expectNoRequestChanged(testFactory); // still not seeing the request
+        expectNoRequestChanged(testFactoryAll); // still seeing the request
+
+        mWiFiNetworkAgent.disconnect();
+    }
+
+    @Test
+    public void testNetworkFactoryUnregister() throws Exception {
+        // Make sure the factory sees the default network
+        final NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addCapability(NET_CAPABILITY_INTERNET);
+        filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
 
         final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
         handlerThread.start();
@@ -4451,6 +4497,7 @@
         testFactory.register();
 
         try {
+            // Expect the factory to receive the default network request.
             testFactory.expectRequestAdd();
             testFactory.assertRequestCountEquals(1);
             assertTrue(testFactory.getMyStartRequested());
@@ -4459,25 +4506,44 @@
             mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
             // Score 60 - 40 penalty for not validated yet, then 60 when it validates
             mWiFiNetworkAgent.connect(true);
-            // Default request and mobile always on request
-            testFactory.expectRequestAdds(2);
+            // The network connects with a low score, so the offer can still beat it and
+            // nothing happens. Then the network validates, and the offer with its filter score
+            // of 40 can no longer beat it and the request is removed.
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+
             assertFalse(testFactory.getMyStartRequested());
 
-            // Turn on mobile data always on. The factory starts looking again.
+            // Turn on mobile data always on. This request will not match the wifi request, so
+            // it will be sent to the test factory whose filters allow to see it.
             setAlwaysOnNetworks(true);
             testFactory.expectRequestAdd();
-            testFactory.assertRequestCountEquals(2);
+            testFactory.assertRequestCountEquals(1);
 
             assertTrue(testFactory.getMyStartRequested());
 
             // Bring up cell data and check that the factory stops looking.
             assertLength(1, mCm.getAllNetworks());
             mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
-            mCellNetworkAgent.connect(true);
-            cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
-            testFactory.expectRequestAdds(2); // Unvalidated and validated
-            testFactory.assertRequestCountEquals(2);
-            // The cell network outscores the factory filter, so start is not requested.
+            mCellNetworkAgent.connect(false);
+            cellNetworkCallback.expectAvailableCallbacks(mCellNetworkAgent, false, false, false,
+                    TEST_CALLBACK_TIMEOUT_MS);
+            // When cell connects, it will satisfy the "mobile always on request" right away
+            // by virtue of being the only network that can satisfy the request. However, its
+            // score is low (50 - 40 = 10) so the test factory can still hope to beat it.
+            expectNoRequestChanged(testFactory);
+
+            // Next, cell validates. This gives it a score of 50 and the test factory can't
+            // hope to beat that according to its filters. It will see the message that its
+            // offer is now unnecessary.
+            mCellNetworkAgent.setNetworkValid(true);
+            // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is
+            // validated – see testPartialConnectivity.
+            mCm.reportNetworkConnectivity(mCellNetworkAgent.getNetwork(), true);
+            cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent);
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+            // Accordingly, the factory shouldn't be started.
             assertFalse(testFactory.getMyStartRequested());
 
             // Check that cell data stays up.
@@ -4485,13 +4551,29 @@
             verifyActiveNetwork(TRANSPORT_WIFI);
             assertLength(2, mCm.getAllNetworks());
 
-            // Turn off mobile data always on and expect the request to disappear...
-            setAlwaysOnNetworks(false);
-            testFactory.expectRequestRemove();
+            // Cell disconnects. There is still the "mobile data always on" request outstanding,
+            // and the test factory should see it now that it isn't hopelessly outscored.
+            mCellNetworkAgent.disconnect();
+            cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            assertLength(1, mCm.getAllNetworks());
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
 
-            // ...  and cell data to be torn down after nascent network timeout.
-            cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
-                    mService.mNascentDelayMs + TEST_CALLBACK_TIMEOUT_MS);
+            // Reconnect cell validated, see the request disappear again. Then withdraw the
+            // mobile always on request. This will tear down cell, and there shouldn't be a
+            // blip where the test factory briefly sees the request or anything.
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(true);
+            cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+            assertLength(2, mCm.getAllNetworks());
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+            setAlwaysOnNetworks(false);
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(0);
+            assertFalse(testFactory.getMyStartRequested());
+            // ...  and cell data to be torn down immediately since it is no longer nascent.
+            cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
             waitForIdle();
             assertLength(1, mCm.getAllNetworks());
         } finally {
@@ -4791,7 +4873,8 @@
         handlerThread.start();
         NetworkCapabilities filter = new NetworkCapabilities()
                 .addTransportType(TRANSPORT_WIFI)
-                .addCapability(NET_CAPABILITY_INTERNET);
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
         final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
                 mServiceContext, "testFactory", filter, mCsHandlerThread);
         testFactory.setScoreFilter(40);
@@ -4800,32 +4883,43 @@
         testFactory.register();
         testFactory.expectRequestAdd();
 
-        // Now file the test request and expect it.
-        mCm.requestNetwork(nr, networkCallback);
-        final NetworkRequest newRequest = testFactory.expectRequestAdd().request;
+        try {
+            // Now file the test request and expect it.
+            mCm.requestNetwork(nr, networkCallback);
+            final NetworkRequest newRequest = testFactory.expectRequestAdd().request;
 
-        if (preUnregister) {
-            mCm.unregisterNetworkCallback(networkCallback);
+            if (preUnregister) {
+                mCm.unregisterNetworkCallback(networkCallback);
 
-            // Simulate the factory releasing the request as unfulfillable: no-op since
-            // the callback has already been unregistered (but a test that no exceptions are
-            // thrown).
-            testFactory.triggerUnfulfillable(newRequest);
-        } else {
-            // Simulate the factory releasing the request as unfulfillable and expect onUnavailable!
-            testFactory.triggerUnfulfillable(newRequest);
+                // The request has been released : the factory should see it removed
+                // immediately.
+                testFactory.expectRequestRemove();
 
-            networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
+                // Simulate the factory releasing the request as unfulfillable: no-op since
+                // the callback has already been unregistered (but a test that no exceptions are
+                // thrown).
+                testFactory.triggerUnfulfillable(newRequest);
+            } else {
+                // Simulate the factory releasing the request as unfulfillable and expect
+                // onUnavailable!
+                testFactory.triggerUnfulfillable(newRequest);
 
-            // unregister network callback - a no-op (since already freed by the
-            // on-unavailable), but should not fail or throw exceptions.
-            mCm.unregisterNetworkCallback(networkCallback);
+                networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
+
+                // Declaring a request unfulfillable releases it automatically.
+                testFactory.expectRequestRemove();
+
+                // unregister network callback - a no-op (since already freed by the
+                // on-unavailable), but should not fail or throw exceptions.
+                mCm.unregisterNetworkCallback(networkCallback);
+
+                // The factory should not see any further removal, as this request has
+                // already been removed.
+            }
+        } finally {
+            testFactory.terminate();
+            handlerThread.quit();
         }
-
-        testFactory.expectRequestRemove();
-
-        testFactory.terminate();
-        handlerThread.quit();
     }
 
     private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
@@ -5982,7 +6076,8 @@
         // called again, it does. For example, connect Ethernet, but with a low score, such that it
         // does not become the default network.
         mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
-        mEthernetNetworkAgent.adjustScore(-40);
+        mEthernetNetworkAgent.setScore(
+                new NetworkScore.Builder().setLegacyInt(30).setExiting(true).build());
         mEthernetNetworkAgent.connect(false);
         waitForIdle();
         verify(mStatsManager).notifyNetworkStatus(any(List.class),
@@ -6857,8 +6952,6 @@
         callback.expectAvailableCallbacksUnvalidated(mMockVpn);
         callback.assertNoCallback();
 
-        assertTrue(mMockVpn.getAgent().getScore() > mEthernetNetworkAgent.getScore());
-        assertEquals(ConnectivityConstants.VPN_DEFAULT_SCORE, mMockVpn.getAgent().getScore());
         assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
 
         NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
@@ -8277,10 +8370,10 @@
     private LinkProperties makeClatLinkProperties(LinkAddress la) {
         LinkAddress clatAddress = la;
         LinkProperties stacked = new LinkProperties();
-        stacked.setInterfaceName(CLAT_PREFIX + MOBILE_IFNAME);
+        stacked.setInterfaceName(CLAT_MOBILE_IFNAME);
         RouteInfo ipv4Default = new RouteInfo(
                 new LinkAddress(Inet4Address.ANY, 0),
-                clatAddress.getAddress(), CLAT_PREFIX + MOBILE_IFNAME);
+                clatAddress.getAddress(), CLAT_MOBILE_IFNAME);
         stacked.addRoute(ipv4Default);
         stacked.addLinkAddress(clatAddress);
         return stacked;
@@ -8305,12 +8398,12 @@
         final String kOtherNat64PrefixString = "64:ff9b::";
         final IpPrefix kOtherNat64Prefix = new IpPrefix(
                 InetAddress.getByName(kOtherNat64PrefixString), 96);
-        final RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, myIpv6.getAddress(),
-                                                     MOBILE_IFNAME);
+        final RouteInfo ipv6Default =
+                new RouteInfo((IpPrefix) null, myIpv6.getAddress(), MOBILE_IFNAME);
         final RouteInfo ipv6Subnet = new RouteInfo(myIpv6, null, MOBILE_IFNAME);
         final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME);
-        final RouteInfo stackedDefault = new RouteInfo((IpPrefix) null, myIpv4.getAddress(),
-                                                       CLAT_PREFIX + MOBILE_IFNAME);
+        final RouteInfo stackedDefault =
+                new RouteInfo((IpPrefix) null, myIpv4.getAddress(), CLAT_MOBILE_IFNAME);
 
         final NetworkRequest networkRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -8323,7 +8416,7 @@
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName(MOBILE_IFNAME);
         cellLp.addLinkAddress(myIpv6);
-        cellLp.addRoute(defaultRoute);
+        cellLp.addRoute(ipv6Default);
         cellLp.addRoute(ipv6Subnet);
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         reset(mMockDnsResolver);
@@ -8331,12 +8424,12 @@
 
         // Connect with ipv6 link properties. Expect prefix discovery to be started.
         mCellNetworkAgent.connect(true);
-        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        int cellNetId = mCellNetworkAgent.getNetwork().netId;
         waitForIdle();
 
         verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId,
                 INetd.PERMISSION_NONE));
-        assertRoutesAdded(cellNetId, ipv6Subnet, defaultRoute);
+        assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
         verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId));
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME);
         verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
@@ -8367,7 +8460,7 @@
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
         reset(mMockDnsResolver);
-        when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME))
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
                 .thenReturn(getClatInterfaceConfigParcel(myIpv4));
 
         // Remove IPv4 address. Expect prefix discovery to be started again.
@@ -8389,13 +8482,13 @@
         verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true);
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
         List<LinkProperties> stackedLps = mCm.getLinkProperties(mCellNetworkAgent.getNetwork())
                 .getStackedLinks();
         assertEquals(makeClatLinkProperties(myIpv4), stackedLps.get(0));
         assertRoutesAdded(cellNetId, stackedDefault);
-        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
         // Change trivial linkproperties and see if stacked link is preserved.
         cellLp.addDnsServer(InetAddress.getByName("8.8.8.8"));
         mCellNetworkAgent.sendLinkProperties(cellLp);
@@ -8418,7 +8511,7 @@
                     new int[] { TRANSPORT_CELLULAR });
         }
         reset(mMockNetd);
-        when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME))
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
                 .thenReturn(getClatInterfaceConfigParcel(myIpv4));
         // Change the NAT64 prefix without first removing it.
         // Expect clatd to be stopped and started with the new prefix.
@@ -8428,16 +8521,16 @@
                 (lp) -> lp.getStackedLinks().size() == 0);
         verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
         assertRoutesRemoved(cellNetId, stackedDefault);
-        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
         verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString());
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix));
-        clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true);
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 1);
         assertRoutesAdded(cellNetId, stackedDefault);
-        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
         reset(mMockNetd);
 
         // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked
@@ -8460,14 +8553,14 @@
         assertRoutesRemoved(cellNetId, stackedDefault);
 
         // The interface removed callback happens but has no effect after stop is called.
-        clat.interfaceRemoved(CLAT_PREFIX + MOBILE_IFNAME);
+        clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
         networkCallback.assertNoCallback();
-        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
         verifyNoMoreInteractions(mMockNetd);
         verifyNoMoreInteractions(mMockDnsResolver);
         reset(mMockNetd);
         reset(mMockDnsResolver);
-        when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME))
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
                 .thenReturn(getClatInterfaceConfigParcel(myIpv4));
 
         // Stopping prefix discovery causes netd to tell us that the NAT64 prefix is gone.
@@ -8490,11 +8583,11 @@
         verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true);
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 1 && lp.getNat64Prefix() != null);
         assertRoutesAdded(cellNetId, stackedDefault);
-        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
         // NAT64 prefix is removed. Expect that clat is stopped.
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
@@ -8507,13 +8600,54 @@
         verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
         networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
                 (lp) -> lp.getStackedLinks().size() == 0);
-        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME);
-        verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME);
-        verifyNoMoreInteractions(mMockNetd);
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_MOBILE_IFNAME);
         // Clean up.
         mCellNetworkAgent.disconnect();
         networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         networkCallback.assertNoCallback();
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        verify(mMockNetd).networkDestroy(cellNetId);
+        verifyNoMoreInteractions(mMockNetd);
+        reset(mMockNetd);
+
+        // Test disconnecting a network that is running 464xlat.
+
+        // Connect a network with a NAT64 prefix.
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
+                .thenReturn(getClatInterfaceConfigParcel(myIpv4));
+        cellLp.setNat64Prefix(kNat64Prefix);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(false /* validated */);
+        networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        cellNetId = mCellNetworkAgent.getNetwork().netId;
+        verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId,
+                INetd.PERMISSION_NONE));
+        assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
+
+        // Clatd is started and clat iface comes up. Expect stacked link to be added.
+        verify(mMockNetd).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+        clat = getNat464Xlat(mCellNetworkAgent);
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 1
+                        && lp.getNat64Prefix().equals(kNat64Prefix));
+        verify(mMockNetd).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        // assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
+        assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
+        reset(mMockNetd);
+
+        // Disconnect the network. clat is stopped and the network is destroyed.
+        mCellNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        networkCallback.assertNoCallback();
+        verify(mMockNetd).clatdStop(MOBILE_IFNAME);
+        verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        verify(mMockNetd).networkDestroy(cellNetId);
+        verifyNoMoreInteractions(mMockNetd);
+
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
@@ -9658,6 +9792,8 @@
     }
 
     private void assertRoutesAdded(int netId, RouteInfo... routes) throws Exception {
+        // TODO: add @JavaDerive(equals=true) to RouteInfoParcel, use eq() directly, and delete
+        // assertRouteInfoParcelMatches above.
         ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class);
         verify(mMockNetd, times(routes.length)).networkAddRouteParcel(eq(netId), captor.capture());
         for (int i = 0; i < routes.length; i++) {
@@ -9724,13 +9860,28 @@
     }
 
     public NetworkAgentInfo fakeMobileNai(NetworkCapabilities nc) {
+        final NetworkCapabilities cellNc = new NetworkCapabilities.Builder(nc)
+                .addTransportType(TRANSPORT_CELLULAR).build();
         final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
                 ConnectivityManager.getNetworkTypeName(TYPE_MOBILE),
                 TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_LTE));
-        return new NetworkAgentInfo(null, new Network(NET_ID), info, new LinkProperties(),
+        return fakeNai(cellNc, info);
+    }
+
+    private NetworkAgentInfo fakeWifiNai(NetworkCapabilities nc) {
+        final NetworkCapabilities wifiNc = new NetworkCapabilities.Builder(nc)
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkInfo info = new NetworkInfo(TYPE_WIFI, 0 /* subtype */,
+                ConnectivityManager.getNetworkTypeName(TYPE_WIFI), "" /* subtypeName */);
+        return fakeNai(wifiNc, info);
+    }
+
+    private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
+        return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
                 nc, new NetworkScore.Builder().setLegacyInt(0).build(),
                 mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
-                INVALID_UID, mQosCallbackTracker, new ConnectivityService.Dependencies());
+                INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
+                new ConnectivityService.Dependencies());
     }
 
     @Test
@@ -9752,7 +9903,7 @@
 
         final NetworkCapabilities nc = new NetworkCapabilities();
         nc.setAdministratorUids(new int[] {wrongUid});
-        final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
+        final NetworkAgentInfo naiWithUid = fakeWifiNai(nc);
 
         mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
 
@@ -9762,18 +9913,37 @@
                         Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
     }
 
+    private void verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(
+            NetworkAgentInfo info, boolean expectPermission) {
+        mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
+
+        assertEquals(
+                "Unexpected ConnDiags permission",
+                expectPermission,
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
+    }
+
     @Test
-    public void testCheckConnectivityDiagnosticsPermissionsNoLocationPermission() throws Exception {
+    public void testCheckConnectivityDiagnosticsPermissionsCellularNoLocationPermission()
+            throws Exception {
         final NetworkCapabilities nc = new NetworkCapabilities();
         nc.setAdministratorUids(new int[] {Process.myUid()});
         final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
 
-        mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
+        verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(naiWithUid,
+                true /* expectPermission */);
+    }
 
-        assertFalse(
-                "ACCESS_FINE_LOCATION permission necessary for Connectivity Diagnostics",
-                mService.checkConnectivityDiagnosticsPermissions(
-                        Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsWifiNoLocationPermission()
+            throws Exception {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setAdministratorUids(new int[] {Process.myUid()});
+        final NetworkAgentInfo naiWithUid = fakeWifiNai(nc);
+
+        verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(naiWithUid,
+                false /* expectPermission */);
     }
 
     @Test
@@ -10055,7 +10225,7 @@
         mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
         waitForIdle();
 
-        final ConnectivityService.NetworkRequestInfo[] nriOutput = mService.requestsSortedById();
+        final NetworkRequestInfo[] nriOutput = mService.requestsSortedById();
 
         assertTrue(nriOutput.length > 1);
         for (int i = 0; i < nriOutput.length - 1; i++) {
@@ -10236,6 +10406,83 @@
         }
     }
 
+    @Test
+    public void testKeepConnected() throws Exception {
+        setAlwaysOnNetworks(false);
+        registerDefaultNetworkCallbacks();
+        final TestNetworkCallback allNetworksCb = new TestNetworkCallback();
+        final NetworkRequest allNetworksRequest = new NetworkRequest.Builder().clearCapabilities()
+                .build();
+        mCm.registerNetworkCallback(allNetworksRequest, allNetworksCb);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        // While the default callback doesn't see the network before it's validated, the listen
+        // sees the network come up and validate later
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        allNetworksCb.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+
+        // The cell network has disconnected (see LOST above) because it was outscored and
+        // had no requests (see setAlwaysOnNetworks(false) above)
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore score = new NetworkScore.Builder().setLegacyInt(30).build();
+        mCellNetworkAgent.setScore(score);
+        mCellNetworkAgent.connect(false /* validated */);
+
+        // The cell network gets torn down right away.
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.assertNoCallback();
+
+        // Now create a cell network with KEEP_CONNECTED_FOR_HANDOVER and make sure it's
+        // not disconnected immediately when outscored.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore scoreKeepup = new NetworkScore.Builder().setLegacyInt(30)
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_HANDOVER).build();
+        mCellNetworkAgent.setScore(scoreKeepup);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mWiFiNetworkAgent.disconnect();
+
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Reconnect a WiFi network and make sure the cell network is still not torn down.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Now remove the reason to keep connected and make sure the network lingers and is
+        // torn down.
+        mCellNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).build());
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(allNetworksCb);
+        // mDefaultNetworkCallback will be unregistered by tearDown()
+    }
+
     private class QosCallbackMockHelper {
         @NonNull public final QosFilter mFilter;
         @NonNull public final IQosCallback mCallback;
@@ -10413,35 +10660,35 @@
         fail("TOO_MANY_REQUESTS never thrown");
     }
 
-    private UidRange createUidRange(int userId) {
-        return UidRange.createForUser(UserHandle.of(userId));
+    private void mockGetApplicationInfo(@NonNull final String packageName, final int uid) {
+        mockGetApplicationInfo(packageName, uid, PRIMARY_USER_HANDLE);
     }
 
-    private void mockGetApplicationInfo(@NonNull final String packageName, @NonNull final int uid) {
+    private void mockGetApplicationInfo(@NonNull final String packageName, final int uid,
+            @NonNull final UserHandle user) {
         final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.uid = uid;
         try {
-            when(mPackageManager.getApplicationInfo(eq(packageName), anyInt()))
+            when(mPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), eq(user)))
                     .thenReturn(applicationInfo);
         } catch (Exception e) {
             fail(e.getMessage());
         }
     }
 
-    private void mockGetApplicationInfoThrowsNameNotFound(@NonNull final String packageName)
+    private void mockGetApplicationInfoThrowsNameNotFound(@NonNull final String packageName,
+            @NonNull final UserHandle user)
             throws Exception {
-        when(mPackageManager.getApplicationInfo(eq(packageName), anyInt()))
+        when(mPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), eq(user)))
                 .thenThrow(new PackageManager.NameNotFoundException(packageName));
     }
 
-    private void mockHasSystemFeature(@NonNull final String featureName,
-            @NonNull final boolean hasFeature) {
+    private void mockHasSystemFeature(@NonNull final String featureName, final boolean hasFeature) {
         when(mPackageManager.hasSystemFeature(eq(featureName)))
                 .thenReturn(hasFeature);
     }
 
-    private Range<Integer> getNriFirstUidRange(
-            @NonNull final ConnectivityService.NetworkRequestInfo nri) {
+    private Range<Integer> getNriFirstUidRange(@NonNull final NetworkRequestInfo nri) {
         return nri.mRequests.get(0).networkCapabilities.getUids().iterator().next();
     }
 
@@ -10480,12 +10727,13 @@
                 OEM_NETWORK_PREFERENCE_OEM_PAID;
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory()
                         .createNrisFromOemNetworkPreferences(
                                 createDefaultOemNetworkPreferences(prefToTest));
-
-        final List<NetworkRequest> mRequests = nris.iterator().next().mRequests;
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
         assertEquals(expectedNumOfNris, nris.size());
         assertEquals(expectedNumOfRequests, mRequests.size());
         assertTrue(mRequests.get(0).isListen());
@@ -10509,12 +10757,13 @@
                 OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory()
                         .createNrisFromOemNetworkPreferences(
                                 createDefaultOemNetworkPreferences(prefToTest));
-
-        final List<NetworkRequest> mRequests = nris.iterator().next().mRequests;
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
         assertEquals(expectedNumOfNris, nris.size());
         assertEquals(expectedNumOfRequests, mRequests.size());
         assertTrue(mRequests.get(0).isListen());
@@ -10535,12 +10784,13 @@
                 OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory()
                         .createNrisFromOemNetworkPreferences(
                                 createDefaultOemNetworkPreferences(prefToTest));
-
-        final List<NetworkRequest> mRequests = nris.iterator().next().mRequests;
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
         assertEquals(expectedNumOfNris, nris.size());
         assertEquals(expectedNumOfRequests, mRequests.size());
         assertTrue(mRequests.get(0).isRequest());
@@ -10558,12 +10808,13 @@
                 OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory()
                         .createNrisFromOemNetworkPreferences(
                                 createDefaultOemNetworkPreferences(prefToTest));
-
-        final List<NetworkRequest> mRequests = nris.iterator().next().mRequests;
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
         assertEquals(expectedNumOfNris, nris.size());
         assertEquals(expectedNumOfRequests, mRequests.size());
         assertTrue(mRequests.get(0).isRequest());
@@ -10591,7 +10842,7 @@
                 .build();
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref);
 
         assertNotNull(nris);
@@ -10616,7 +10867,7 @@
                 .build();
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final List<ConnectivityService.NetworkRequestInfo> nris =
+        final List<NetworkRequestInfo> nris =
                 new ArrayList<>(
                         mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(
                                 pref));
@@ -10630,16 +10881,18 @@
     }
 
     @Test
-    public void testOemNetworkRequestFactoryMultipleUsersCorrectlySetsUids()
+    public void testOemNetworkRequestFactoryMultipleUsersSetsUids()
             throws Exception {
         // Arrange users
-        final int secondUser = 10;
-        final UserHandle secondUserHandle = new UserHandle(secondUser);
+        final int secondUserTestPackageUid = UserHandle.getUid(SECONDARY_USER, TEST_PACKAGE_UID);
+        final int thirdUserTestPackageUid = UserHandle.getUid(TERTIARY_USER, TEST_PACKAGE_UID);
         when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
-                Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle));
+                Arrays.asList(PRIMARY_USER_HANDLE, SECONDARY_USER_HANDLE, TERTIARY_USER_HANDLE));
 
-        // Arrange PackageManager mocks
-        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+        // Arrange PackageManager mocks testing for users who have and don't have a package.
+        mockGetApplicationInfoThrowsNameNotFound(TEST_PACKAGE_NAME, PRIMARY_USER_HANDLE);
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, SECONDARY_USER_HANDLE);
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, thirdUserTestPackageUid, TERTIARY_USER_HANDLE);
 
         // Build OemNetworkPreferences object
         final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID;
@@ -10648,13 +10901,13 @@
                 .build();
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final List<ConnectivityService.NetworkRequestInfo> nris =
+        final List<NetworkRequestInfo> nris =
                 new ArrayList<>(
                         mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(
                                 pref));
 
-        // UIDs for all users and all managed packages should be present.
-        // Two users each with two packages.
+        // UIDs for users with installed packages should be present.
+        // Three users exist, but only two have the test package installed.
         final int expectedUidSize = 2;
         final List<Range<Integer>> uids =
                 new ArrayList<>(nris.get(0).mRequests.get(0).networkCapabilities.getUids());
@@ -10662,11 +10915,10 @@
 
         // Sort by uid to access nris by index
         uids.sort(Comparator.comparingInt(uid -> uid.getLower()));
-        final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID);
-        assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getLower());
-        assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getUpper());
-        assertEquals(secondUserTestPackageUid, (int) uids.get(1).getLower());
-        assertEquals(secondUserTestPackageUid, (int) uids.get(1).getUpper());
+        assertEquals(secondUserTestPackageUid, (int) uids.get(0).getLower());
+        assertEquals(secondUserTestPackageUid, (int) uids.get(0).getUpper());
+        assertEquals(thirdUserTestPackageUid, (int) uids.get(1).getLower());
+        assertEquals(thirdUserTestPackageUid, (int) uids.get(1).getUpper());
     }
 
     @Test
@@ -10690,7 +10942,7 @@
                 .build();
 
         // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
-        final ArraySet<ConnectivityService.NetworkRequestInfo> nris =
+        final ArraySet<NetworkRequestInfo> nris =
                 mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref);
 
         assertEquals(expectedNumOfNris, nris.size());
@@ -10783,8 +11035,7 @@
         // each time to confirm it doesn't change under test.
         final int expectedDefaultNetworkRequestsSize = 2;
         assertEquals(expectedDefaultNetworkRequestsSize, mService.mDefaultNetworkRequests.size());
-        for (final ConnectivityService.NetworkRequestInfo defaultRequest
-                : mService.mDefaultNetworkRequests) {
+        for (final NetworkRequestInfo defaultRequest : mService.mDefaultNetworkRequests) {
             final Network defaultNetwork = defaultRequest.getSatisfier() == null
                     ? null : defaultRequest.getSatisfier().network();
             // If this is the default request.
@@ -10828,16 +11079,24 @@
     }
 
     private void registerDefaultNetworkCallbacks() {
+        if (mSystemDefaultNetworkCallback != null || mDefaultNetworkCallback != null
+                || mProfileDefaultNetworkCallback != null
+                || mTestPackageDefaultNetworkCallback != null) {
+            throw new IllegalStateException("Default network callbacks already registered");
+        }
+
         // Using Manifest.permission.NETWORK_SETTINGS for registerSystemDefaultNetworkCallback()
         mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
         mSystemDefaultNetworkCallback = new TestNetworkCallback();
         mDefaultNetworkCallback = new TestNetworkCallback();
         mProfileDefaultNetworkCallback = new TestNetworkCallback();
+        mTestPackageDefaultNetworkCallback = new TestNetworkCallback();
         mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback,
                 new Handler(ConnectivityThread.getInstanceLooper()));
         mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback);
         registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback,
                 TEST_WORK_PROFILE_APP_UID);
+        registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback, TEST_PACKAGE_UID);
         // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well.
         mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
     }
@@ -10852,6 +11111,9 @@
         if (null != mProfileDefaultNetworkCallback) {
             mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallback);
         }
+        if (null != mTestPackageDefaultNetworkCallback) {
+            mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback);
+        }
     }
 
     private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
@@ -11468,6 +11730,7 @@
         final UidRangeParcel[] uidRanges =
                 toUidRangeStableParcels(
                         uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid));
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, secondUserHandle);
         setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
 
         // Verify the starting state. No networks should be connected.
@@ -11510,6 +11773,7 @@
         final UidRangeParcel[] uidRangesBothUsers =
                 toUidRangeStableParcels(
                         uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid));
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, secondUserHandle);
         setupSetOemNetworkPreferenceForPreferenceTest(
                 networkPref, uidRangesSingleUser, TEST_PACKAGE_NAME);
 
@@ -11566,7 +11830,7 @@
         final UidRangeParcel[] uidRangesSinglePackage =
                 toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
         mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
-        mockGetApplicationInfoThrowsNameNotFound(packageToInstall);
+        mockGetApplicationInfoThrowsNameNotFound(packageToInstall, PRIMARY_USER_HANDLE);
         setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME, packageToInstall);
         grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid(), packageToInstall);
 
@@ -11600,7 +11864,7 @@
                 false /* shouldDestroyNetwork */);
 
         // Set the system to no longer recognize the package to be installed
-        mockGetApplicationInfoThrowsNameNotFound(packageToInstall);
+        mockGetApplicationInfoThrowsNameNotFound(packageToInstall, PRIMARY_USER_HANDLE);
 
         // Send a broadcast indicating a package was removed.
         final Intent removedIntent = new Intent(ACTION_PACKAGE_REMOVED);
@@ -11688,6 +11952,124 @@
         // default callbacks will be unregistered in tearDown
     }
 
+    @Test
+    public void testNetworkFactoryRequestsWithMultilayerRequest()
+            throws Exception {
+        // First use OEM_PAID preference to create a multi-layer request : 1. listen for
+        // unmetered, 2. request network with cap OEM_PAID, 3, request the default network for
+        // fallback.
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        final HandlerThread handlerThread = new HandlerThread("MockFactory");
+        handlerThread.start();
+        NetworkCapabilities internetFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final MockNetworkFactory internetFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "internetFactory", internetFilter, mCsHandlerThread);
+        internetFactory.setScoreFilter(40);
+        internetFactory.register();
+        // Default internet request only. The unmetered request is never sent to factories (it's a
+        // LISTEN, not requestable). The 3rd (fallback) request in OEM_PAID NRI is TRACK_DEFAULT
+        // which is also not sent to factories. Finally, the OEM_PAID request doesn't match the
+        // internetFactory filter.
+        internetFactory.expectRequestAdds(1);
+        internetFactory.assertRequestCountEquals(1);
+
+        NetworkCapabilities oemPaidFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_OEM_PAID)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        final MockNetworkFactory oemPaidFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "oemPaidFactory", oemPaidFilter, mCsHandlerThread);
+        oemPaidFactory.setScoreFilter(40);
+        oemPaidFactory.register();
+        oemPaidFactory.expectRequestAdd(); // Because nobody satisfies the request
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        // A network connected that satisfies the default internet request. For the OEM_PAID
+        // preference, this is not as good as an OEM_PAID network, so even if the score of
+        // the network is better than the factory announced, it still should try to bring up
+        // the network.
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        // The internet factory however is outscored, and should lose its requests.
+        internetFactory.expectRequestRemove();
+        internetFactory.assertRequestCountEquals(0);
+
+        final NetworkCapabilities oemPaidNc = new NetworkCapabilities();
+        oemPaidNc.addCapability(NET_CAPABILITY_OEM_PAID);
+        oemPaidNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        final TestNetworkAgentWrapper oemPaidAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+                new LinkProperties(), oemPaidNc);
+        oemPaidAgent.connect(true);
+
+        // The oemPaidAgent has score 50/cell transport, so it beats what the oemPaidFactory can
+        // provide, therefore it loses the request.
+        oemPaidFactory.expectRequestRemove();
+        oemPaidFactory.assertRequestCountEquals(0);
+        expectNoRequestChanged(internetFactory);
+        internetFactory.assertRequestCountEquals(0);
+
+        oemPaidAgent.setScore(new NetworkScore.Builder().setLegacyInt(20).setExiting(true).build());
+        // Now the that the agent is weak, the oemPaidFactory can beat the existing network for the
+        // OEM_PAID request. The internet factory however can't beat a network that has OEM_PAID
+        // for the preference request, so it doesn't see the request.
+        oemPaidFactory.expectRequestAdd();
+        oemPaidFactory.assertRequestCountEquals(1);
+        expectNoRequestChanged(internetFactory);
+        internetFactory.assertRequestCountEquals(0);
+
+        mCellNetworkAgent.disconnect();
+        // The network satisfying the default internet request has disconnected, so the
+        // internetFactory sees the default request again. However there is a network with OEM_PAID
+        // connected, so the 2nd OEM_PAID req is already satisfied, so the oemPaidFactory doesn't
+        // care about networks that don't have OEM_PAID.
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        internetFactory.expectRequestAdd();
+        internetFactory.assertRequestCountEquals(1);
+
+        // Cell connects again, still with score 50. Back to the previous state.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        internetFactory.expectRequestRemove();
+        internetFactory.assertRequestCountEquals(0);
+
+        // Create a request that holds the upcoming wifi network.
+        final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
+                wifiCallback);
+
+        // Now WiFi connects and it's unmetered, but it's weaker than cell.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).setExiting(true)
+                .build()); // Not the best Internet network, but unmetered
+        mWiFiNetworkAgent.connect(true);
+
+        // The OEM_PAID preference prefers an unmetered network to an OEM_PAID network, so
+        // the oemPaidFactory can't beat wifi no matter how high its score.
+        oemPaidFactory.expectRequestRemove();
+        expectNoRequestChanged(internetFactory);
+
+        mCellNetworkAgent.disconnect();
+        // Now that the best internet network (cell, with its 50 score compared to 30 for WiFi
+        // at this point), the default internet request is satisfied by a network worse than
+        // the internetFactory announced, so it gets the request. However, there is still an
+        // unmetered network, so the oemPaidNetworkFactory still can't beat this.
+        expectNoRequestChanged(oemPaidFactory);
+        internetFactory.expectRequestAdd();
+        mCm.unregisterNetworkCallback(wifiCallback);
+    }
+
     /**
      * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order:
      * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID
@@ -12009,11 +12391,11 @@
         testFactory.setScoreFilter(40);
 
         try {
-            // Register the factory and expect it will see default request, because all requests
-            // are sent to all factories.
+            // Register the factory. It doesn't see the default request because its filter does
+            // not include INTERNET.
             testFactory.register();
-            testFactory.expectRequestAdd();
-            testFactory.assertRequestCountEquals(1);
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(0);
             // The factory won't try to start the network since the default request doesn't
             // match the filter (no INTERNET capability).
             assertFalse(testFactory.getMyStartRequested());
@@ -12026,7 +12408,7 @@
                     bestMatchingCb, mCsHandlerThread.getThreadHandler());
             bestMatchingCb.assertNoCallback();
             expectNoRequestChanged(testFactory);
-            testFactory.assertRequestCountEquals(1);
+            testFactory.assertRequestCountEquals(0);
             assertFalse(testFactory.getMyStartRequested());
 
             // Fire a normal mms request, verify the factory will only see the request.
@@ -12035,13 +12417,13 @@
                     .addCapability(NET_CAPABILITY_MMS).build();
             mCm.requestNetwork(mmsRequest, mmsNetworkCallback);
             testFactory.expectRequestAdd();
-            testFactory.assertRequestCountEquals(2);
+            testFactory.assertRequestCountEquals(1);
             assertTrue(testFactory.getMyStartRequested());
 
             // Unregister best matching callback, verify factory see no change.
             mCm.unregisterNetworkCallback(bestMatchingCb);
             expectNoRequestChanged(testFactory);
-            testFactory.assertRequestCountEquals(2);
+            testFactory.assertRequestCountEquals(1);
             assertTrue(testFactory.getMyStartRequested());
         } finally {
             testFactory.terminate();
@@ -12613,4 +12995,245 @@
             }
         }
     }
+
+    private void assertCreateNrisFromMobileDataPreferredUids(Set<Integer> uids) {
+        final Set<NetworkRequestInfo> nris =
+                mService.createNrisFromMobileDataPreferredUids(uids);
+        final NetworkRequestInfo nri = nris.iterator().next();
+        // Verify that one NRI is created with multilayer requests. Because one NRI can contain
+        // multiple uid ranges, so it only need create one NRI here.
+        assertEquals(1, nris.size());
+        assertTrue(nri.isMultilayerRequest());
+        assertEquals(nri.getUids(), uidRangesForUids(uids));
+        assertEquals(DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED,
+                nri.getDefaultNetworkPriority());
+    }
+
+    /**
+     * Test createNrisFromMobileDataPreferredUids returns correct NetworkRequestInfo.
+     */
+    @Test
+    public void testCreateNrisFromMobileDataPreferredUids() {
+        // Verify that empty uid set should not create any NRI for it.
+        final Set<NetworkRequestInfo> nrisNoUid =
+                mService.createNrisFromMobileDataPreferredUids(new ArraySet<>());
+        assertEquals(0, nrisNoUid.size());
+
+        final int uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID);
+        final int uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2);
+        final int uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID);
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1));
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1, uid3));
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1, uid2));
+    }
+
+    private void setAndUpdateMobileDataPreferredUids(Set<Integer> uids) {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(mServiceContext, uids);
+        mService.updateMobileDataPreferredUids();
+        waitForIdle();
+    }
+
+    /**
+     * Test that MOBILE_DATA_PREFERRED_UIDS changes will send correct net id and uid ranges to netd.
+     */
+    @Test
+    public void testMobileDataPreferredUidsChanged() throws Exception {
+        final InOrder inorder = inOrder(mMockNetd);
+        registerDefaultNetworkCallbacks();
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId, INetd.PERMISSION_NONE));
+
+        // Initial mobile data preferred uids status.
+        setAndUpdateMobileDataPreferredUids(Set.of());
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting and verify that net id and uid ranges send to netd
+        final Set<Integer> uids1 = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids1));
+        setAndUpdateMobileDataPreferredUids(uids1);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges1);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting again and verify that old rules are removed and
+        // new rules are added.
+        final Set<Integer> uids2 = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID),
+                PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2),
+                SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges2 = toUidRangeStableParcels(uidRangesForUids(uids2));
+        setAndUpdateMobileDataPreferredUids(uids2);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(cellNetId, uidRanges1);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges2);
+
+        // Clear MOBILE_DATA_PREFERRED_UIDS setting again and verify that old rules are removed and
+        // new rules are not added.
+        final Set<Integer> uids3 = Set.of();
+        final UidRangeParcel[] uidRanges3 = toUidRangeStableParcels(uidRangesForUids(uids3));
+        setAndUpdateMobileDataPreferredUids(uids3);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(cellNetId, uidRanges2);
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+    }
+
+    /**
+     * Make sure mobile data preferred uids feature behaves as expected when the mobile network
+     * goes up and down while the uids is set. Make sure they behave as expected whether
+     * there is a general default network or not.
+     */
+    @Test
+    public void testMobileDataPreferenceForMobileNetworkUpDown() throws Exception {
+        final InOrder inorder = inOrder(mMockNetd);
+        // File a request for cell to ensure it doesn't go down.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+        cellNetworkCallback.assertNoCallback();
+
+        registerDefaultNetworkCallbacks();
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int wifiNetId = mWiFiNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                wifiNetId, INetd.PERMISSION_NONE));
+
+        // Initial mobile data preferred uids status.
+        setAndUpdateMobileDataPreferredUids(Set.of());
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting and verify that wifi net id and uid ranges send to
+        // netd.
+        final Set<Integer> uids = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges = toUidRangeStableParcels(uidRangesForUids(uids));
+        setAndUpdateMobileDataPreferredUids(uids);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(wifiNetId, uidRanges);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Cellular network connected. mTestPackageDefaultNetworkCallback should receive
+        // callback with cellular network and net id and uid ranges should be updated to netd.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId, INetd.PERMISSION_NONE));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(wifiNetId, uidRanges);
+
+        // Cellular network disconnected. mTestPackageDefaultNetworkCallback should receive
+        // callback with wifi network from fallback request.
+        mCellNetworkAgent.disconnect();
+        mDefaultNetworkCallback.assertNoCallback();
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(wifiNetId, uidRanges);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd).networkDestroy(cellNetId);
+
+        // Cellular network comes back. mTestPackageDefaultNetworkCallback should receive
+        // callback with cellular network.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int cellNetId2 = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId2, INetd.PERMISSION_NONE));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId2, uidRanges);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(wifiNetId, uidRanges);
+
+        // Wifi network disconnected. mTestPackageDefaultNetworkCallback should not receive
+        // any callback.
+        mWiFiNetworkAgent.disconnect();
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.assertNoCallback();
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+        waitForIdle();
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd).networkDestroy(wifiNetId);
+
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+    }
+
+    @Test
+    public void testSetMobileDataPreferredUids_noIssueToFactory() throws Exception {
+        // First set mobile data preferred uid to create a multi-layer requests: 1. listen for
+        // cellular, 2. track the default network for fallback.
+        setAndUpdateMobileDataPreferredUids(
+                Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)));
+
+        final HandlerThread handlerThread = new HandlerThread("MockFactory");
+        handlerThread.start();
+        NetworkCapabilities internetFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final MockNetworkFactory internetFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "internetFactory", internetFilter, mCsHandlerThread);
+        internetFactory.setScoreFilter(40);
+
+        try {
+            internetFactory.register();
+            // Default internet request only. The first request is listen for cellular network,
+            // which is never sent to factories (it's a LISTEN, not requestable). The second
+            // fallback request is TRACK_DEFAULT which is also not sent to factories.
+            internetFactory.expectRequestAdds(1);
+            internetFactory.assertRequestCountEquals(1);
+
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(true);
+
+            // The internet factory however is outscored, and should lose its requests.
+            internetFactory.expectRequestRemove();
+            internetFactory.assertRequestCountEquals(0);
+
+            mCellNetworkAgent.disconnect();
+            // The network satisfying the default internet request has disconnected, so the
+            // internetFactory sees the default request again.
+            internetFactory.expectRequestAdds(1);
+            internetFactory.assertRequestCountEquals(1);
+        } finally {
+            internetFactory.terminate();
+            handlerThread.quitSafely();
+        }
+    }
+
+    /**
+     * Validate request counts are counted accurately on MOBILE_DATA_PREFERRED_UIDS change
+     * on set/replace.
+     */
+    @Test
+    public void testMobileDataPreferredUidsChangedCountsRequestsCorrectlyOnSet() throws Exception {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(mServiceContext,
+                Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)));
+        testRequestCountLimits(() -> {
+            // Set initially to test the limit prior to having existing requests.
+            mService.updateMobileDataPreferredUids();
+            waitForIdle();
+
+            // re-set so as to test the limit as part of replacing existing requests.
+            mService.updateMobileDataPreferredUids();
+            waitForIdle();
+        });
+    }
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index a90fa68..20be5f4 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -57,6 +57,7 @@
 public class NsdServiceTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
+    private static final long CLEANUP_DELAY_MS = 500;
 
     long mTimeoutMs = 100; // non-final so that tests can adjust the value.
 
@@ -86,20 +87,27 @@
     }
 
     @Test
-    public void testClientsCanConnectAndDisconnect() {
+    public void testNoDaemonStartedWhenClientsConnect() {
         when(mSettings.isEnabled()).thenReturn(true);
 
         NsdService service = makeService();
 
+        // Creating an NsdManager will not cause any cmds executed, which means
+        // no daemon is started.
         NsdManager client1 = connectClient(service);
-        verify(mDaemon, timeout(100).times(1)).start();
+        verify(mDaemon, never()).execute(any());
 
+        // Creating another NsdManager will not cause any cmds executed.
         NsdManager client2 = connectClient(service);
+        verify(mDaemon, never()).execute(any());
 
         client1.disconnect();
-        client2.disconnect();
+        // Still 1 client remains, daemon shouldn't be stopped.
+        verify(mDaemon, never()).maybeStop();
 
-        verify(mDaemon, timeout(mTimeoutMs).times(1)).stop();
+        client2.disconnect();
+        // All clients are disconnected, stop the daemon after CLEANUP_DELAY_MS.
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
 
         client1.disconnect();
         client2.disconnect();
@@ -113,7 +121,8 @@
         NsdService service = makeService();
         NsdManager client = connectClient(service);
 
-        verify(mDaemon, timeout(100).times(1)).start();
+        verify(mDaemon, never()).maybeStart();
+        verify(mDaemon, never()).execute(any());
 
         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
         request.setPort(2201);
@@ -121,21 +130,24 @@
         // Client registration request
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
         verifyDaemonCommand("register 2 a_name a_type 2201");
 
         // Client discovery request
         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
         client.discoverServices("a_type", PROTOCOL, listener2);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
         verifyDaemonCommand("discover 3 a_type");
 
         // Client resolve request
         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
         client.resolveService(request, listener3);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
         verifyDaemonCommand("resolve 4 a_name a_type local.");
 
-        // Client disconnects
+        // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
         client.disconnect();
-        verify(mDaemon, timeout(mTimeoutMs).times(1)).stop();
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
 
         // checks that request are cleaned
         verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4");
@@ -143,12 +155,38 @@
         client.disconnect();
     }
 
+    @Test
+    public void testCleanupDelayNoRequestActive() {
+        when(mSettings.isEnabled()).thenReturn(true);
+        when(mDaemon.execute(any())).thenReturn(true);
+
+        NsdService service = makeService();
+        NsdManager client = connectClient(service);
+
+        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
+        request.setPort(2201);
+        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        client.registerService(request, PROTOCOL, listener1);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
+        verifyDaemonCommand("register 2 a_name a_type 2201");
+
+        client.unregisterService(listener1);
+        verifyDaemonCommand("stop-register 2");
+
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        reset(mDaemon);
+        client.disconnect();
+        // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon.
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+    }
+
     NsdService makeService() {
         DaemonConnectionSupplier supplier = (callback) -> {
             mDaemonCallback = callback;
             return mDaemon;
         };
-        NsdService service = new NsdService(mContext, mSettings, mHandler, supplier);
+        NsdService service = new NsdService(mContext, mSettings,
+                mHandler, supplier, CLEANUP_DELAY_MS);
         verify(mDaemon, never()).execute(any(String.class));
         return service;
     }
@@ -157,6 +195,13 @@
         return new NsdManager(mContext, service);
     }
 
+    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) {
+        // Stop daemon shouldn't be called immediately.
+        verify(mDaemon, timeout(mTimeoutMs).times(0)).maybeStop();
+        // Clean up the daemon after CLEANUP_DELAY_MS.
+        verify(mDaemon, timeout(cleanupDelayMs + mTimeoutMs)).maybeStop();
+    }
+
     void verifyDaemonCommands(String... wants) {
         verifyDaemonCommand(String.join(" ", wants), wants.length);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index eb3b4df..45b575a 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -18,6 +18,7 @@
 
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
 import android.text.TextUtils
 import android.util.ArraySet
 import androidx.test.filters.SmallTest
@@ -55,16 +56,16 @@
             if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
             if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
         }.build()
-        return mixInScore(nc, nac)
+        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */)
     }
 
     @Test
     fun testGetLegacyInt() {
-        val ns = FullScore(50, 0L /* policy */)
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
         assertEquals(10, ns.legacyInt) // -40 penalty for not being validated
         assertEquals(50, ns.legacyIntAsValidated)
 
-        val vpnNs = FullScore(101, 0L /* policy */).withPolicies(vpn = true)
+        val vpnNs = FullScore(101, 0L /* policy */, KEEP_CONNECTED_NONE).withPolicies(vpn = true)
         assertEquals(101, vpnNs.legacyInt) // VPNs are not subject to unvalidation penalty
         assertEquals(101, vpnNs.legacyIntAsValidated)
         assertEquals(101, vpnNs.withPolicies(validated = true).legacyInt)
@@ -83,7 +84,7 @@
 
     @Test
     fun testToString() {
-        val string = FullScore(10, 0L /* policy */)
+        val string = FullScore(10, 0L /* policy */, KEEP_CONNECTED_NONE)
                 .withPolicies(vpn = true, acceptUnvalidated = true).toString()
         assertTrue(string.contains("Score(10"), string)
         assertTrue(string.contains("ACCEPT_UNVALIDATED"), string)
@@ -107,7 +108,7 @@
 
     @Test
     fun testHasPolicy() {
-        val ns = FullScore(50, 0L /* policy */)
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
         assertFalse(ns.hasPolicy(POLICY_IS_VALIDATED))
         assertFalse(ns.hasPolicy(POLICY_IS_VPN))
         assertFalse(ns.hasPolicy(POLICY_EVER_USER_SELECTED))
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 116d755..36e229d 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -71,6 +71,8 @@
     static final int LOW_DAILY_LIMIT = 2;
     static final int HIGH_DAILY_LIMIT = 1000;
 
+    private static final int TEST_LINGER_DELAY_MS = 400;
+
     LingerMonitor mMonitor;
 
     @Mock ConnectivityService mConnService;
@@ -366,7 +368,7 @@
         NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
                 new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
                 mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
-                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(),
+                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
                 mQosCallbackTracker, new ConnectivityService.Dependencies());
         nai.everValidated = true;
         return nai;
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
new file mode 100644
index 0000000..409f8c3
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity
+
+import android.net.INetworkOfferCallback
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+const val POLICY_NONE = 0L
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkOfferTest {
+    val mockCallback = mock(INetworkOfferCallback::class.java)
+
+    @Test
+    fun testOfferNeededUnneeded() {
+        val score = FullScore(50, POLICY_NONE, KEEP_CONNECTED_NONE)
+        val offer = NetworkOffer(score, NetworkCapabilities.Builder().build(), mockCallback,
+                1 /* providerId */)
+        val request1 = mock(NetworkRequest::class.java)
+        val request2 = mock(NetworkRequest::class.java)
+        offer.onNetworkNeeded(request1)
+        verify(mockCallback).onNetworkNeeded(eq(request1))
+        assertTrue(offer.neededFor(request1))
+        assertFalse(offer.neededFor(request2))
+
+        offer.onNetworkNeeded(request2)
+        verify(mockCallback).onNetworkNeeded(eq(request2))
+        assertTrue(offer.neededFor(request1))
+        assertTrue(offer.neededFor(request2))
+
+        // Note that the framework never calls onNetworkNeeded multiple times with the same
+        // request without calling onNetworkUnneeded first. It would be incorrect usage and the
+        // behavior would be undefined, so there is nothing to test.
+
+        offer.onNetworkUnneeded(request1)
+        verify(mockCallback).onNetworkUnneeded(eq(request1))
+        assertFalse(offer.neededFor(request1))
+        assertTrue(offer.neededFor(request2))
+
+        offer.onNetworkUnneeded(request2)
+        verify(mockCallback).onNetworkUnneeded(eq(request2))
+        assertFalse(offer.neededFor(request1))
+        assertFalse(offer.neededFor(request2))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 86c9116..551b94c 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.server.connectivity
 
+import android.net.NetworkCapabilities
 import android.net.NetworkRequest
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import org.junit.Test
@@ -32,10 +34,14 @@
 class NetworkRankerTest {
     private val ranker = NetworkRanker()
 
-    private fun makeNai(satisfy: Boolean, score: Int) = mock(NetworkAgentInfo::class.java).also {
-        doReturn(satisfy).`when`(it).satisfies(any())
-        doReturn(score).`when`(it).currentScore
-    }
+    private fun makeNai(satisfy: Boolean, legacyScore: Int) =
+            mock(NetworkAgentInfo::class.java).also {
+                doReturn(satisfy).`when`(it).satisfies(any())
+                val fs = FullScore(legacyScore, 0 /* policies */, KEEP_CONNECTED_NONE)
+                doReturn(fs).`when`(it).getScore()
+                val nc = NetworkCapabilities.Builder().build()
+                doReturn(nc).`when`(it).getCapsNoCopy()
+            }
 
     @Test
     fun testGetBestNetwork() {
@@ -43,7 +49,7 @@
         val nais = scores.map { makeNai(true, it) }
         val bestNetwork = nais[2] // The one with the top score
         val someRequest = mock(NetworkRequest::class.java)
-        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais))
+        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, bestNetwork))
     }
 
     @Test
@@ -52,20 +58,20 @@
                 makeNai(false, 60), makeNai(true, 23), makeNai(false, 68))
         val bestNetwork = nais[1] // Top score that's satisfying
         val someRequest = mock(NetworkRequest::class.java)
-        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais))
+        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, nais[1]))
     }
 
     @Test
     fun testNoMatch() {
         val nais = listOf(makeNai(false, 20), makeNai(false, 50), makeNai(false, 90))
         val someRequest = mock(NetworkRequest::class.java)
-        assertNull(ranker.getBestNetwork(someRequest, nais))
+        assertNull(ranker.getBestNetwork(someRequest, nais, null))
     }
 
     @Test
     fun testEmpty() {
         val someRequest = mock(NetworkRequest::class.java)
-        assertNull(ranker.getBestNetwork(someRequest, emptyList()))
+        assertNull(ranker.getBestNetwork(someRequest, emptyList(), null))
     }
 
     // Make sure the ranker is "stable" (as in stable sort), that is, it always returns the FIRST
@@ -75,10 +81,10 @@
         val nais1 = listOf(makeNai(true, 30), makeNai(true, 30), makeNai(true, 30),
                 makeNai(true, 30), makeNai(true, 30), makeNai(true, 30))
         val someRequest = mock(NetworkRequest::class.java)
-        assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1))
+        assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1, nais1[0]))
 
         val nais2 = listOf(makeNai(true, 30), makeNai(true, 50), makeNai(true, 20),
                 makeNai(true, 50), makeNai(true, 50), makeNai(true, 40))
-        assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2))
+        assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2, nais2[1]))
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index c6e7606..e98f5db 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -73,6 +73,7 @@
 import android.os.SystemConfigManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.SparseIntArray;
 
@@ -515,7 +516,7 @@
 
     @Test
     public void testUserAndPackageAddRemove() throws Exception {
-        final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService);
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
 
         // MOCK_UID1: MOCK_PACKAGE1 only has network permission.
         // SYSTEM_UID: SYSTEM_PACKAGE1 has system permission.
@@ -531,47 +532,47 @@
         // Add SYSTEM_PACKAGE2, expect only have network permission.
         mPermissionMonitor.onUserAdded(MOCK_USER1);
         addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE2, SYSTEM_UID);
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
 
         // Add SYSTEM_PACKAGE1, expect permission escalate.
         addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE1, SYSTEM_UID);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
 
         mPermissionMonitor.onUserAdded(MOCK_USER2);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID});
 
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID});
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{MOCK_UID1});
 
         // Remove MOCK_UID1, expect no permission left for all user.
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
         removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{MOCK_UID1});
 
         // Remove SYSTEM_PACKAGE1, expect permission downgrade.
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{SYSTEM_PACKAGE2});
         removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 SYSTEM_PACKAGE1, SYSTEM_UID);
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID});
 
         mPermissionMonitor.onUserRemoved(MOCK_USER1);
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER2}, new int[]{SYSTEM_UID});
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER2}, new int[]{SYSTEM_UID});
 
         // Remove all packages, expect no permission left.
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{});
         removePackageForUsers(new UserHandle[]{MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID);
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID, MOCK_UID1});
 
         // Remove last user, expect no redundant clearPermission is invoked.
         mPermissionMonitor.onUserRemoved(MOCK_USER2);
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID, MOCK_UID1});
     }
 
@@ -698,7 +699,7 @@
 
     @Test
     public void testPackagePermissionUpdate() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
         // MOCK_UID1: MOCK_PACKAGE1 only has internet permission.
         // MOCK_UID2: MOCK_PACKAGE2 does not have any permission.
         // SYSTEM_UID1: SYSTEM_PACKAGE1 has internet permission and update device stats permission.
@@ -714,29 +715,29 @@
         // Send the permission information to netd, expect permission updated.
         mPermissionMonitor.sendPackagePermissionsToNetd(netdPermissionsAppIds);
 
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET,
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET,
                 new int[]{MOCK_UID1});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{SYSTEM_UID1});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS,
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS,
                 new int[]{SYSTEM_UID2});
 
         // Update permission of MOCK_UID1, expect new permission show up.
         mPermissionMonitor.sendPackagePermissionsForUid(MOCK_UID1,
                 INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         // Change permissions of SYSTEM_UID2, expect new permission show up and old permission
         // revoked.
         mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID2,
                 INetd.PERMISSION_INTERNET);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2});
 
         // Revoke permission from SYSTEM_UID1, expect no permission stored.
         mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.PERMISSION_NONE);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1});
     }
 
     private PackageInfo setPackagePermissions(String packageName, int uid, String[] permissions)
@@ -757,23 +758,23 @@
 
     @Test
     public void testPackageInstall() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         addPackage(MOCK_PACKAGE2, MOCK_UID2, new String[] {INTERNET});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID2});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID2});
     }
 
     @Test
     public void testPackageInstallSharedUid() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         PackageInfo packageInfo1 = addPackage(MOCK_PACKAGE1, MOCK_UID1,
                 new String[] {INTERNET, UPDATE_DEVICE_STATS});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         // Install another package with the same uid and no permissions should not cause the UID to
@@ -783,56 +784,56 @@
         when(mPackageManager.getPackagesForUid(MOCK_UID1))
               .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2});
         mPermissionMonitor.onPackageAdded(MOCK_PACKAGE2, MOCK_UID1);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
     }
 
     @Test
     public void testPackageUninstallBasic() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{});
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
     }
 
     @Test
     public void testPackageRemoveThenAdd() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{});
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
     }
 
     @Test
     public void testPackageUpdate() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID1});
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
     }
 
     @Test
     public void testPackageUninstallWithMultiplePackages() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
 
         addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
 
         // Mock another package with the same uid but different permissions.
@@ -842,7 +843,7 @@
                 MOCK_PACKAGE2});
 
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
     }
 
     @Test
@@ -859,7 +860,7 @@
 
     @Test
     public void testUpdateUidPermissionsFromSystemConfig() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
         when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(new ArrayList<>());
         when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET)))
                 .thenReturn(new int[]{ MOCK_UID1, MOCK_UID2 });
@@ -867,15 +868,15 @@
                 .thenReturn(new int[]{ MOCK_UID2 });
 
         mPermissionMonitor.startMonitoring();
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{ MOCK_UID1 });
-        mNetdServiceMonitor.expectPermission(
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{ MOCK_UID1 });
+        netdServiceMonitor.expectPermission(
                 INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS,
                 new int[]{ MOCK_UID2 });
     }
 
     @Test
     public void testIntentReceiver() throws Exception {
-        final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(), any(), any(), any());
@@ -888,7 +889,7 @@
         setPackagePermissions(MOCK_PACKAGE1, MOCK_UID1,
                 new String[] { INTERNET, UPDATE_DEVICE_STATS });
         receiver.onReceive(mContext, addedIntent);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
                 | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[] { MOCK_UID1 });
 
         // Verify receiving PACKAGE_REMOVED intent.
@@ -897,98 +898,140 @@
                 Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */));
         removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1);
         receiver.onReceive(mContext, removedIntent);
-        mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 });
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 });
+    }
+
+    private ContentObserver expectRegisterContentObserver(Uri expectedUri) {
+        final ArgumentCaptor<ContentObserver> captor =
+                ArgumentCaptor.forClass(ContentObserver.class);
+        verify(mDeps).registerContentObserver(any(),
+                argThat(uri -> uri.equals(expectedUri)), anyBoolean(), captor.capture());
+        return captor.getValue();
+    }
+
+    private void buildAndMockPackageInfoWithPermissions(String packageName, int uid,
+            String... permissions) throws Exception {
+        final PackageInfo packageInfo = setPackagePermissions(packageName, uid, permissions);
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo.uid = uid;
     }
 
     @Test
     public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
-        final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService);
-        final ArgumentCaptor<ContentObserver> captor =
-                ArgumentCaptor.forClass(ContentObserver.class);
-        verify(mDeps, times(1)).registerContentObserver(any(),
-                argThat(uri -> uri.getEncodedPath().contains(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS)),
-                anyBoolean(), captor.capture());
-        final ContentObserver contentObserver = captor.getValue();
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
 
         mPermissionMonitor.onUserAdded(MOCK_USER1);
-        // Prepare PackageInfo for MOCK_PACKAGE1
-        final PackageInfo packageInfo = buildPackageInfo(
-                false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1);
-        packageInfo.packageName = MOCK_PACKAGE1;
-        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo);
-        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE1});
-        // Prepare PackageInfo for MOCK_PACKAGE2
-        final PackageInfo packageInfo2 = buildPackageInfo(
-                false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1);
-        packageInfo2.packageName = MOCK_PACKAGE2;
-        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
-        when(mPackageManager.getPackagesForUid(MOCK_UID2)).thenReturn(new String[]{MOCK_PACKAGE2});
+        // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID2);
 
         // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
         // should have SYSTEM permission.
         when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
                 new ArraySet<>(new Integer[] { MOCK_UID1 }));
         contentObserver.onChange(true /* selfChange */);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
 
         // MOCK_UID2 is listed in setting that allow to use restricted networks, MOCK_UID2
         // should have SYSTEM permission but MOCK_UID1 should revoke permission.
         when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
                 new ArraySet<>(new Integer[] { MOCK_UID2 }));
         contentObserver.onChange(true /* selfChange */);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
 
         // No uid lists in setting, should revoke permission from all uids.
         when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
         contentObserver.onChange(true /* selfChange */);
-        mNetdMonitor.expectNoPermission(
+        netdMonitor.expectNoPermission(
                 new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1, MOCK_UID2});
     }
 
     @Test
-    public void testAppsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
-        final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService);
-        final ArgumentCaptor<ContentObserver> captor =
-                ArgumentCaptor.forClass(ContentObserver.class);
-        verify(mDeps, times(1)).registerContentObserver(any(),
-                argThat(uri -> uri.getEncodedPath().contains(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS)),
-                anyBoolean(), captor.capture());
-        final ContentObserver contentObserver = captor.getValue();
+    public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
 
         mPermissionMonitor.onUserAdded(MOCK_USER1);
-        // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2 with shared uid MOCK_UID1.
-        final PackageInfo packageInfo = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE);
-        packageInfo.applicationInfo.uid = MOCK_USER1.getUid(MOCK_UID1);
-        packageInfo.packageName = MOCK_PACKAGE1;
-        final PackageInfo packageInfo2 = buildPackageInfo(
-                false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1);
-        packageInfo2.packageName = MOCK_PACKAGE2;
-        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo);
-        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1, CHANGE_NETWORK_STATE);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID1);
         when(mPackageManager.getPackagesForUid(MOCK_UID1))
                 .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2});
 
         // MOCK_PACKAGE1 have CHANGE_NETWORK_STATE, MOCK_UID1 should have NETWORK permission.
         addPackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
 
         // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
         // should upgrade to SYSTEM permission.
         when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
                 new ArraySet<>(new Integer[] { MOCK_UID1 }));
         contentObserver.onChange(true /* selfChange */);
-        mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
 
         // No app lists in setting, MOCK_UID1 should downgrade to NETWORK permission.
         when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
         contentObserver.onChange(true /* selfChange */);
-        mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
 
         // MOCK_PACKAGE1 removed, should revoke permission from MOCK_UID1.
         when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE2});
         removePackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
-        mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
+
+        // One user MOCK_USER1
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2.
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID2);
+
+        // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
+        // in MOCK_USER1 should have SYSTEM permission and MOCK_UID2 has no permissions.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+
+        // Add user MOCK_USER2.
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+        // MOCK_UID1 in both users should all have SYSTEM permission and MOCK_UID2 has no
+        // permissions in either user.
+        netdMonitor.expectPermission(
+                SYSTEM, new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(
+                new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID2});
+
+        // MOCK_UID2 is listed in setting that allow to use restricted networks, MOCK_UID2
+        // in both users should have SYSTEM permission and MOCK_UID1 has no permissions.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID2 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(
+                SYSTEM, new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(
+                new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID1});
+
+        // Remove user MOCK_USER1
+        mPermissionMonitor.onUserRemoved(MOCK_USER1);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[] {MOCK_USER2}, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(new UserHandle[] {MOCK_USER2}, new int[]{MOCK_UID1});
+
+        // No uid lists in setting, should revoke permission from all uids.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectNoPermission(
+                new UserHandle[]{MOCK_USER2}, new int[]{ MOCK_UID1, MOCK_UID2 });
     }
 }