Merge "Changing target_sdk for CtsNetTestCasesLatestSdk"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 95f854b..c4c79c6 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -22,6 +22,18 @@
         }
       ]
     },
+    // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
+    {
+      "name": "CtsNetTestCasesLatestSdk",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
     {
       "name": "bpf_existence_test"
     },
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 29f6e12..026ed54 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -33,6 +33,7 @@
         ":framework-connectivity-shared-srcs",
         ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
+        ":statslog-tethering-java-gen",
     ],
     static_libs: [
         "androidx.annotation_annotation",
@@ -223,3 +224,11 @@
     apex_available: ["com.android.tethering"],
     min_sdk_version: "30",
 }
+
+genrule {
+    name: "statslog-tethering-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
+         " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+    out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
+}
\ No newline at end of file
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index bd8fe7c..76c5d5c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -70,9 +70,9 @@
     canned_fs_config: "canned_fs_config",
     bpfs: [
         "block.o",
-        "clatd.o_mainline",
+        "clatd.o",
         "dscp_policy.o",
-        "netd.o_mainline",
+        "netd.o",
         "offload.o",
         "test.o",
     ],
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c718f4c..437ed71 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -69,6 +69,7 @@
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 
@@ -282,13 +283,15 @@
 
     private LinkAddress mIpv4Address;
 
+    private final TetheringMetrics mTetheringMetrics;
+
     // TODO: Add a dependency object to pass the data members or variables from the tethering
     // object. It helps to reduce the arguments of the constructor.
     public IpServer(
             String ifaceName, Looper looper, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
-            Dependencies deps) {
+            TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, looper);
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -303,6 +306,7 @@
         mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
+        mTetheringMetrics = tetheringMetrics;
         resetLinkProperties();
         mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
@@ -1201,6 +1205,9 @@
             stopConntrackMonitoring();
 
             resetLinkProperties();
+
+            mTetheringMetrics.updateErrorCode(mInterfaceType, mLastError);
+            mTetheringMetrics.sendReport(mInterfaceType);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index ecb6478..c403548 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -77,6 +77,7 @@
 import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
 
+import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -1024,7 +1025,7 @@
             map.forEach((k, v) -> {
                 pw.println(String.format("%s: %s", k, v));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping BPF stats map: " + e);
         }
     }
@@ -1072,7 +1073,7 @@
                 return;
             }
             map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv6 upstream map: " + e);
         }
     }
@@ -1116,7 +1117,7 @@
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
             try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
                 dumpRawMap(statsMap, pw);
-            } catch (ErrnoException e) {
+            } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping stats map: " + e);
             }
             return;
@@ -1124,7 +1125,7 @@
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
                 dumpRawMap(upstreamMap, pw);
-            } catch (ErrnoException e) {
+            } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
             return;
@@ -1195,7 +1196,7 @@
             pw.increaseIndent();
             dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
             pw.decreaseIndent();
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
     }
@@ -1220,7 +1221,7 @@
                 }
                 if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping counter map: " + e);
         }
     }
@@ -1244,7 +1245,7 @@
                 pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
                         v.ifIndex, getIfName(v.ifIndex)));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping dev map: " + e);
         }
         pw.decreaseIndent();
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index cc2422f..41a10ae 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -172,6 +172,9 @@
             return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
         }
 
+        // This ensures that tethering isn't started on 2 different interfaces with the same type.
+        // Once tethering could support multiple interface with the same type,
+        // TetheringSoftApCallback would need to handle it among others.
         final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
         if (useLastAddress && cachedAddress != null
                 && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 44935fc..af017f3 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -140,6 +140,7 @@
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.TetheringUtils;
@@ -254,6 +255,7 @@
     private final UserManager mUserManager;
     private final BpfCoordinator mBpfCoordinator;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+    private final TetheringMetrics mTetheringMetrics;
     private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
 
     private volatile TetheringConfiguration mConfig;
@@ -292,6 +294,7 @@
         mNetd = mDeps.getINetd(mContext);
         mLooper = mDeps.getTetheringLooper();
         mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
+        mTetheringMetrics = mDeps.getTetheringMetrics();
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -445,8 +448,22 @@
                 mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
 
         final WifiManager wifiManager = getWifiManager();
+        TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
         if (wifiManager != null) {
-            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+            wifiManager.registerSoftApCallback(mExecutor, softApCallback);
+        }
+        if (SdkLevel.isAtLeastT() && wifiManager != null) {
+            try {
+                // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+                // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+                // or MAINLINE_NETWORK_STACK permission would also able to use this API.
+                wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+            } catch (UnsupportedOperationException e) {
+                // Since wifi module development in internal branch,
+                // #registerLocalOnlyHotspotSoftApCallback currently doesn't supported in AOSP
+                // before AOSP switch to Android T + 1.
+                Log.wtf(TAG, "registerLocalOnlyHotspotSoftApCallback API is not supported");
+            }
         }
 
         startTrackDefaultNetwork();
@@ -542,6 +559,13 @@
         }
 
         // Called by wifi when the number of soft AP clients changed.
+        // Currently multiple softAp would not behave well in PrivateAddressCoordinator
+        // (where it gets the address from cache), it ensure tethering only support one ipServer for
+        // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
+        // onConnectedClientsChanged should also be updated to support tracking different softAp's
+        // clients individually.
+        // TODO: Add wtf log and have check to reject request duplicated type with different
+        // interface.
         @Override
         public void onConnectedClientsChanged(final List<WifiClient> clients) {
             updateConnectedClients(clients);
@@ -616,7 +640,8 @@
         processInterfaceStateChange(iface, false /* enabled */);
     }
 
-    void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
+    void startTethering(final TetheringRequestParcel request, final String callerPkg,
+            final IIntResultListener listener) {
         mHandler.post(() -> {
             final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
                     request.tetheringType);
@@ -636,6 +661,7 @@
                         request.showProvisioningUi);
             }
             enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
+            mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
         });
     }
 
@@ -695,7 +721,11 @@
 
         // If changing tethering fail, remove corresponding request
         // no matter who trigger the start/stop.
-        if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type);
+        if (result != TETHER_ERROR_NO_ERROR) {
+            mActiveTetheringRequests.remove(type);
+            mTetheringMetrics.updateErrorCode(type, result);
+            mTetheringMetrics.sendReport(type);
+        }
     }
 
     private int setWifiTethering(final boolean enable) {
@@ -1288,7 +1318,7 @@
 
             // Finally bring up serving on the new interface
             mWifiP2pTetherInterface = group.getInterface();
-            enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+            enableWifiP2pIpServing(mWifiP2pTetherInterface);
         }
 
         private void handleUserRestrictionAction() {
@@ -1379,20 +1409,22 @@
         changeInterfaceState(ifname, ipServingMode);
     }
 
-    private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
-        mLog.log("Canceling WiFi tethering request -"
-                + " type=" + tetheringType
-                + " interface=" + ifname
-                + " state=" + apState);
-
-        if (!TextUtils.isEmpty(ifname)) {
-            final TetherState ts = mTetherStates.get(ifname);
-            if (ts != null) {
-                ts.ipServer.unwanted();
-                return;
-            }
+    private void disableWifiIpServingCommon(int tetheringType, String ifname) {
+        if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
+            mTetherStates.get(ifname).ipServer.unwanted();
+            return;
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mLog.e("Tethering no longer handle untracked interface after T: " + ifname);
+            return;
+        }
+
+        // Attempt to guess the interface name before T. Pure AOSP code should never enter here
+        // because WIFI_AP_STATE_CHANGED intent always include ifname and it should be tracked
+        // by mTetherStates. In case OEMs have some modification in wifi side which pass null
+        // or empty ifname. Before T, tethering allow to disable the first wifi ipServer if
+        // given ifname don't match any tracking ipServer.
         for (int i = 0; i < mTetherStates.size(); i++) {
             final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
             if (ipServer.interfaceType() == tetheringType) {
@@ -1400,7 +1432,6 @@
                 return;
             }
         }
-
         mLog.log("Error disabling Wi-Fi IP serving; "
                 + (TextUtils.isEmpty(ifname) ? "no interface name specified"
                                            : "specified interface: " + ifname));
@@ -1409,20 +1440,39 @@
     private void disableWifiIpServing(String ifname, int apState) {
         // Regardless of whether we requested this transition, the AP has gone
         // down.  Don't try to tether again unless we're requested to do so.
-        // TODO: Remove this altogether, once Wi-Fi reliably gives us an
-        // interface name with every broadcast.
         mWifiTetherRequested = false;
 
-        disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
+        mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
+
+        disableWifiIpServingCommon(TETHERING_WIFI, ifname);
+    }
+
+    private void enableWifiP2pIpServing(String ifname) {
+        if (TextUtils.isEmpty(ifname)) {
+            mLog.e("Cannot enable P2P IP serving with invalid interface");
+            return;
+        }
+
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable p2p regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI_P2P : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+        enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
     }
 
     private void disableWifiP2pIpServingIfNeeded(String ifname) {
         if (TextUtils.isEmpty(ifname)) return;
 
-        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+        mLog.log("Canceling P2P tethering request - interface=" + ifname);
+        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
     }
 
     private void enableWifiIpServing(String ifname, int wifiIpMode) {
+        mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
+
         // Map wifiIpMode values to IpServer.Callback serving states, inferring
         // from mWifiTetherRequested as a final "best guess".
         final int ipServingMode;
@@ -1438,13 +1488,18 @@
                 return;
         }
 
+        // After T, tethering always trust the iface pass by state change intent. This allow
+        // tethering to deprecate tetherable wifi regexs after T.
+        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+        if (!checkTetherableType(type)) {
+            mLog.e(ifname + " is not a tetherable iface, ignoring");
+            return;
+        }
+
         if (!TextUtils.isEmpty(ifname)) {
-            ensureIpServerStarted(ifname);
-            changeInterfaceState(ifname, ipServingMode);
+            enableIpServing(type, ifname, ipServingMode);
         } else {
-            mLog.e(String.format(
-                    "Cannot enable IP serving in mode %s on missing interface name",
-                    ipServingMode));
+            mLog.e("Cannot enable IP serving on missing interface name");
         }
     }
 
@@ -2715,23 +2770,28 @@
         mTetherMainSM.sendMessage(which, state, 0, newLp);
     }
 
+    private boolean hasSystemFeature(final String feature) {
+        return mContext.getPackageManager().hasSystemFeature(feature);
+    }
+
+    private boolean checkTetherableType(int type) {
+        if ((type == TETHERING_WIFI || type == TETHERING_WIGIG)
+                && !hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            return false;
+        }
+
+        if (type == TETHERING_WIFI_P2P && !hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+            return false;
+        }
+
+        return type != TETHERING_INVALID;
+    }
+
     private void ensureIpServerStarted(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
-        if (interfaceType == TETHERING_INVALID) {
-            mLog.log(iface + " is not a tetherable iface, ignoring");
-            return;
-        }
-
-        final PackageManager pm = mContext.getPackageManager();
-        if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
-            mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
-            return;
-        }
-        if (interfaceType == TETHERING_WIFI_P2P
-                && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
-            mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+        if (!checkTetherableType(interfaceType)) {
+            mLog.log(iface + " is used for " + interfaceType + " which is not tetherable");
             return;
         }
 
@@ -2749,7 +2809,7 @@
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
                              makeControlCallback(), mConfig, mPrivateAddressCoordinator,
-                             mDeps.getIpServerDependencies()), isNcm);
+                             mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
         tetherState.ipServer.start();
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9224213..8e0354d 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -34,6 +34,7 @@
 import com.android.internal.util.StateMachine;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 
 import java.util.ArrayList;
 
@@ -163,4 +164,11 @@
     public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
         return BluetoothPanShimImpl.newInstance(pan);
     }
+
+    /**
+     * Get a reference to the TetheringMetrics to be used by tethering.
+     */
+    public TetheringMetrics getTetheringMetrics() {
+        return new TetheringMetrics();
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 9fb61fe..f147e10 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -137,7 +137,7 @@
                 return;
             }
 
-            mTethering.startTethering(request, listener);
+            mTethering.startTethering(request, callerPkg, listener);
         }
 
         @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
new file mode 100644
index 0000000..e25f2ae
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Collection of utilities for tethering metrics.
+ *
+ * To see if the logs are properly sent to statsd, execute following commands
+ *
+ * $ adb shell cmd stats print-logs
+ * $ adb logcat | grep statsd OR $ adb logcat -b stats
+ *
+ * @hide
+ */
+public class TetheringMetrics {
+    private static final String TAG = TetheringMetrics.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String SETTINGS_PKG_NAME = "com.android.settings";
+    private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
+    private static final String GMS_PKG_NAME = "com.google.android.gms";
+    private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
+
+    /** Update Tethering stats about caller's package name and downstream type. */
+    public void createBuilder(final int downstreamType, final String callerPkg) {
+        mBuilderMap.clear();
+        NetworkTetheringReported.Builder statsBuilder =
+                    NetworkTetheringReported.newBuilder();
+        statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
+                    .setUserType(userTypeToEnum(callerPkg))
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+        mBuilderMap.put(downstreamType, statsBuilder);
+    }
+
+    /** Update error code of given downstreamType. */
+    public void updateErrorCode(final int downstreamType, final int errCode) {
+        NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        statsBuilder.setErrorCode(errorCodeToEnum(errCode));
+    }
+
+    /** Remove Tethering stats.
+     *  If Tethering stats is ready to write then write it before removing.
+     */
+    public void sendReport(final int downstreamType) {
+        final NetworkTetheringReported.Builder statsBuilder =
+                mBuilderMap.get(downstreamType);
+        if (statsBuilder == null) {
+            Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+            return;
+        }
+        write(statsBuilder.build());
+        mBuilderMap.remove(downstreamType);
+    }
+
+    /** Collect Tethering stats and write metrics data to statsd pipeline. */
+    @VisibleForTesting
+    public void write(@NonNull final NetworkTetheringReported reported) {
+        TetheringStatsLog.write(TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                reported.getErrorCode().getNumber(),
+                reported.getDownstreamType().getNumber(),
+                reported.getUpstreamType().getNumber(),
+                reported.getUserType().getNumber());
+        if (DBG) {
+            Log.d(TAG, "Write errorCode: " + reported.getErrorCode().getNumber()
+                    + ", downstreamType: " + reported.getDownstreamType().getNumber()
+                    + ", upstreamType: " + reported.getUpstreamType().getNumber()
+                    + ", userType: " + reported.getUserType().getNumber());
+        }
+    }
+
+    /** Map {@link TetheringType} to {@link DownstreamType} */
+    private DownstreamType downstreamTypeToEnum(final int ifaceType) {
+        switch(ifaceType) {
+            case TETHERING_WIFI:
+                return DownstreamType.DS_TETHERING_WIFI;
+            case TETHERING_WIFI_P2P:
+                return DownstreamType.DS_TETHERING_WIFI_P2P;
+            case TETHERING_USB:
+                return DownstreamType.DS_TETHERING_USB;
+            case TETHERING_BLUETOOTH:
+                return DownstreamType.DS_TETHERING_BLUETOOTH;
+            case TETHERING_NCM:
+                return DownstreamType.DS_TETHERING_NCM;
+            case TETHERING_ETHERNET:
+                return DownstreamType.DS_TETHERING_ETHERNET;
+            default:
+                return DownstreamType.DS_UNSPECIFIED;
+        }
+    }
+
+    /** Map {@link StartTetheringError} to {@link ErrorCode} */
+    private ErrorCode errorCodeToEnum(final int lastError) {
+        switch(lastError) {
+            case TETHER_ERROR_NO_ERROR:
+                return ErrorCode.EC_NO_ERROR;
+            case TETHER_ERROR_UNKNOWN_IFACE:
+                return ErrorCode.EC_UNKNOWN_IFACE;
+            case TETHER_ERROR_SERVICE_UNAVAIL:
+                return ErrorCode.EC_SERVICE_UNAVAIL;
+            case TETHER_ERROR_UNSUPPORTED:
+                return ErrorCode.EC_UNSUPPORTED;
+            case TETHER_ERROR_UNAVAIL_IFACE:
+                return ErrorCode.EC_UNAVAIL_IFACE;
+            case TETHER_ERROR_INTERNAL_ERROR:
+                return ErrorCode.EC_INTERNAL_ERROR;
+            case TETHER_ERROR_TETHER_IFACE_ERROR:
+                return ErrorCode.EC_TETHER_IFACE_ERROR;
+            case TETHER_ERROR_UNTETHER_IFACE_ERROR:
+                return ErrorCode.EC_UNTETHER_IFACE_ERROR;
+            case TETHER_ERROR_ENABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_ENABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_DISABLE_FORWARDING_ERROR:
+                return ErrorCode.EC_DISABLE_FORWARDING_ERROR;
+            case TETHER_ERROR_IFACE_CFG_ERROR:
+                return ErrorCode.EC_IFACE_CFG_ERROR;
+            case TETHER_ERROR_PROVISIONING_FAILED:
+                return ErrorCode.EC_PROVISIONING_FAILED;
+            case TETHER_ERROR_DHCPSERVER_ERROR:
+                return ErrorCode.EC_DHCPSERVER_ERROR;
+            case TETHER_ERROR_ENTITLEMENT_UNKNOWN:
+                return ErrorCode.EC_ENTITLEMENT_UNKNOWN;
+            case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION;
+            case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION:
+                return ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION;
+            default:
+                return ErrorCode.EC_UNKNOWN_TYPE;
+        }
+    }
+
+    /** Map callerPkg to {@link UserType} */
+    private UserType userTypeToEnum(final String callerPkg) {
+        if (callerPkg.equals(SETTINGS_PKG_NAME)) {
+            return UserType.USER_SETTINGS;
+        } else if (callerPkg.equals(SYSTEMUI_PKG_NAME)) {
+            return UserType.USER_SYSTEMUI;
+        } else if (callerPkg.equals(GMS_PKG_NAME)) {
+            return UserType.USER_GMS;
+        } else {
+            return UserType.USER_UNKNOWN;
+        }
+    }
+}
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index e73b7d5..3699f7a 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -59,6 +59,7 @@
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.Log;
@@ -84,6 +85,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
@@ -1058,19 +1060,33 @@
     }
 
     @Test
-    @IgnoreAfter(Build.VERSION_CODES.Q)
-    public void testTetherUdpV4WithoutBpf() throws Exception {
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testTetherUdpV4UpToR() throws Exception {
         initializeTethering();
         runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
                 false /* usingBpf */);
     }
 
+    private static boolean isUdpOffloadSupportedByKernel() {
+        final String kVersionString = VintfRuntimeInfo.getKernelVersion();
+        // Kernel version which is older than 4.14 doesn't support UDP offload absolutely. Kernel
+        // version which is between 4.14 and 5.8 support UDP offload probably. Simply apply kernel
+        // 4.14 to be threshold first and monitor on what devices tests fail for improving the
+        // offload support checking.
+        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.14") >= 0;
+    }
+
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testTetherUdpV4WithBpf() throws Exception {
+    public void testTetherUdpV4AfterR() throws Exception {
         initializeTethering();
+        boolean usingBpf = isUdpOffloadSupportedByKernel();
+        if (!usingBpf) {
+            Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
+                    + VintfRuntimeInfo.getKernelVersion());
+        }
         runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
-                true /* usingBpf */);
+                usingBpf);
     }
 
     @Nullable
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index ad2faa0..68c1c57 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -352,15 +352,6 @@
         assertFalse(mTestMap.isEmpty());
         mTestMap.clear();
         assertTrue(mTestMap.isEmpty());
-
-        // Clearing an already-closed map throws.
-        mTestMap.close();
-        try {
-            mTestMap.clear();
-            fail("clearing already-closed map should throw");
-        } catch (ErrnoException expected) {
-            assertEquals(OsConstants.EBADF, expected.errno);
-        }
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index aac531a..bf7e887 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -116,6 +116,7 @@
 import com.android.networkstack.tethering.TetherLimitValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -186,6 +187,7 @@
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
@@ -235,7 +237,7 @@
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         mIpServer = new IpServer(
                 IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mCallback, mTetherConfig, mAddressCoordinator, mDependencies);
+                mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
         mIpServer.start();
         mNeighborEventConsumer = neighborCaptor.getValue();
 
@@ -367,7 +369,7 @@
                 .thenReturn(mIpNeighborMonitor);
         mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
                 mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
-                mDependencies);
+                mTetheringMetrics, mDependencies);
         mIpServer.start();
         mLooper.dispatchAll();
         verify(mCallback).updateInterfaceState(
@@ -451,6 +453,9 @@
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_BLUETOOTH),
+                eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_BLUETOOTH));
         verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
     }
 
@@ -658,6 +663,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_TETHER_IFACE_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
@@ -676,6 +684,9 @@
         usbTeardownOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), mLinkPropertiesCaptor.capture());
         assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+                eq(TETHER_ERROR_ENABLE_FORWARDING_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index e9716b3..8ef0c76 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,6 +26,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
@@ -668,7 +669,7 @@
 
         if (isAtLeastT()) {
             mTetherStatsProviderCb.expectNotifyLimitReached();
-        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+        } else if (isAtLeastS()) {
             mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
         } else {
             mTetherStatsProviderCb.expectNotifyLimitReached();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index f664d5d..9db8f16 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -275,7 +275,7 @@
         mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                 result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).startTethering(eq(request), eq(result));
+        verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 2fd7f48..773cae3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -45,6 +45,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
@@ -57,6 +58,7 @@
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -192,11 +194,15 @@
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.MiscAsserts;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -219,6 +225,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TetheringTest {
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final int IFINDEX_OFFSET = 100;
 
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -239,6 +247,7 @@
     private static final String TEST_WIFI_REGEX = "test_wlan\\d";
     private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
     private static final String TEST_BT_REGEX = "test_pan\\d";
+    private static final String TEST_CALLER_PKG = "com.test.tethering";
 
     private static final int CELLULAR_NETID = 100;
     private static final int WIFI_NETID = 101;
@@ -273,6 +282,7 @@
     @Mock private BluetoothPan mBluetoothPan;
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
+    @Mock private TetheringMetrics mTetheringMetrics;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -296,6 +306,7 @@
     private OffloadController mOffloadCtrl;
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private SoftApCallback mSoftApCallback;
+    private SoftApCallback mLocalOnlyHotspotCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
@@ -497,6 +508,11 @@
         }
 
         @Override
+        public TetheringMetrics getTetheringMetrics() {
+            return mTetheringMetrics;
+        }
+
+        @Override
         public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
             mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
@@ -661,6 +677,14 @@
         verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
         mSoftApCallback = softApCallbackCaptor.getValue();
 
+        if (isAtLeastT()) {
+            final ArgumentCaptor<SoftApCallback> localOnlyCallbackCaptor =
+                    ArgumentCaptor.forClass(SoftApCallback.class);
+            verify(mWifiManager).registerLocalOnlyHotspotSoftApCallback(any(),
+                    localOnlyCallbackCaptor.capture());
+            mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
+        }
+
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
     }
@@ -855,7 +879,8 @@
 
     private void prepareNcmTethering() {
         // Emulate startTethering(TETHERING_NCM) called
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
     }
@@ -863,7 +888,7 @@
     private void prepareUsbTethering() {
         // Emulate pressing the USB tethering button in Settings UI.
         final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
-        mTethering.startTethering(request, null);
+        mTethering.startTethering(request, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
 
         assertEquals(1, mTethering.getActiveTetheringRequests().size());
@@ -936,7 +961,7 @@
 
         // Emulate externally-visible WifiManager effects, when hotspot mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1433,7 +1458,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1460,7 +1486,8 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
@@ -1509,7 +1536,7 @@
 
         // Emulate externally-visible WifiManager effects, when tethering mode
         // is being torn down.
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
@@ -1536,11 +1563,13 @@
         doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_WIFI), anyString());
 
         // Emulate externally-visible WifiManager effects, causing the
         // per-interface state machine to start up, and telling us that
@@ -1579,6 +1608,10 @@
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
 
+        verify(mTetheringMetrics, times(2)).updateErrorCode(eq(TETHERING_WIFI),
+                eq(TETHER_ERROR_INTERNAL_ERROR));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_WIFI));
+
         verifyNoMoreInteractions(mWifiManager);
         verifyNoMoreInteractions(mNetd);
     }
@@ -1881,7 +1914,8 @@
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
@@ -1903,7 +1937,13 @@
         mTethering.unregisterTetheringEventCallback(callback);
         mLooper.dispatchAll();
         mTethering.stopTethering(TETHERING_WIFI);
-        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+        if (isAtLeastT()) {
+            // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+            callback2.assertNoStateChangeCallback();
+            sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+                    IFACE_IP_MODE_TETHERED);
+        }
         tetherState = callback2.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
         mLooper.dispatchAll();
@@ -1978,10 +2018,12 @@
     public void testNoDuplicatedEthernetRequest() throws Exception {
         final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verify(mEm, times(1)).requestTetheredInterface(any(), any());
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+                null);
         mLooper.dispatchAll();
         verifyNoMoreInteractions(mEm);
         mTethering.stopTethering(TETHERING_ETHERNET);
@@ -2185,14 +2227,16 @@
         final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
 
         // Enable USB tethering and check that Tethering starts USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         verifyNoMoreInteractions(mUsbManager);
 
         // Enable USB tethering again with the same request and expect no change to USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+                secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2201,7 +2245,8 @@
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2230,7 +2275,8 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
         mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
@@ -2298,7 +2344,7 @@
         final TetheringRequestParcel wifiNotExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2312,7 +2358,7 @@
         final TetheringRequestParcel wifiExemptRequest =
                 createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
                         CONNECTIVITY_SCOPE_GLOBAL);
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2325,14 +2371,14 @@
         // If one app enables tethering without provisioning check first, then another app enables
         // tethering of the same type but does not disable the provisioning check.
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiExemptRequest, null);
+        mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
         assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
         reset(mEntitleMgr);
         setupForRequiredProvisioning();
-        mTethering.startTethering(wifiNotExemptRequest, null);
+        mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
         verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2422,7 +2468,8 @@
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
         final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
                 ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+                TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
         TetheredInterfaceCallback ethCallback = callbackCaptor.getValue();
@@ -2504,12 +2551,11 @@
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update lease for local only tethering.
         final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
-        final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
-        p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
-                Long.MAX_VALUE, "test1"));
-        notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
-        final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
+                "192.168.50.24", 24, Long.MAX_VALUE, "test1");
+        final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+                eventCallbacks, p2pLease);
+        callback.expectTetheredClientChanged(p2pClients);
         reset(mDhcpServer);
 
         // Run wifi tethering.
@@ -2519,25 +2565,20 @@
                 any(), dhcpEventCbsCaptor.capture());
         eventCallbacks = dhcpEventCbsCaptor.getValue();
         // Update mac address from softAp callback before getting dhcp lease.
-        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
         final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
-        final WifiClient testClient = mock(WifiClient.class);
-        when(testClient.getMacAddress()).thenReturn(testMac2);
-        wifiClients.add(testClient);
-        mSoftApCallback.onConnectedClientsChanged(wifiClients);
-        final TetheredClient noAddrClient = new TetheredClient(testMac2,
-                Collections.emptyList() /* addresses */, TETHERING_WIFI);
-        clients.add(noAddrClient);
-        callback.expectTetheredClientChanged(clients);
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac2,
+                false /* isLocalOnly */);
+        final List<TetheredClient> p2pAndNoAddrClients = new ArrayList<>(p2pClients);
+        p2pAndNoAddrClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(p2pAndNoAddrClients);
 
         // Update dhcp lease for wifi tethering.
-        clients.remove(noAddrClient);
-        final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
-        wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
-                Long.MAX_VALUE, "test2"));
-        notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
-        clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
-        callback.expectTetheredClientChanged(clients);
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId2", testMac2,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test2");
+        final List<TetheredClient> p2pAndWifiClients = new ArrayList<>(p2pClients);
+        p2pAndWifiClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease));
+        callback.expectTetheredClientChanged(p2pAndWifiClients);
 
         // Test onStarted callback that register second callback when tethering is running.
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -2545,18 +2586,74 @@
             mTethering.registerTetheringEventCallback(callback2);
             mLooper.dispatchAll();
         });
-        callback2.expectTetheredClientChanged(clients);
+        callback2.expectTetheredClientChanged(p2pAndWifiClients);
     }
 
-    private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
-            IDhcpEventCallbacks callback) throws Exception {
-        callback.onLeasesChanged(leaseParcelables);
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback);
+            mLooper.dispatchAll();
+        });
+        callback.expectTetheredClientChanged(Collections.emptyList());
+
+        // Run local only hotspot.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+        final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+                 ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        final IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
+        // Update mac address from softAp callback before getting dhcp lease.
+        final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
+        final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac,
+                true /* isLocalOnly */);
+        final List<TetheredClient> noAddrLocalOnlyClients = new ArrayList<>();
+        noAddrLocalOnlyClients.add(noAddrClient);
+        callback.expectTetheredClientChanged(noAddrLocalOnlyClients);
+
+        // Update dhcp lease for local only hotspot.
+        final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", testMac,
+                "192.168.43.24", 24, Long.MAX_VALUE, "test");
+        final List<TetheredClient> localOnlyClients = notifyDhcpLeasesChanged(TETHERING_WIFI,
+                eventCallbacks, wifiLease);
+        callback.expectTetheredClientChanged(localOnlyClients);
+
+        // Client disconnect from local only hotspot.
+        mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
+        callback.expectTetheredClientChanged(Collections.emptyList());
+    }
+
+    private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
+            boolean isLocalOnly) throws Exception {
+        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+        final WifiClient testClient = mock(WifiClient.class);
+        when(testClient.getMacAddress()).thenReturn(mac);
+        wifiClients.add(testClient);
+        if (isLocalOnly) {
+            mLocalOnlyHotspotCallback.onConnectedClientsChanged(wifiClients);
+        } else {
+            mSoftApCallback.onConnectedClientsChanged(wifiClients);
+        }
+        return new TetheredClient(mac, Collections.emptyList() /* addresses */, TETHERING_WIFI);
+    }
+
+    private List<TetheredClient> notifyDhcpLeasesChanged(int type, IDhcpEventCallbacks callback,
+            DhcpLeaseParcelable... leases) throws Exception {
+        final List<DhcpLeaseParcelable> dhcpLeases = Arrays.asList(leases);
+        callback.onLeasesChanged(dhcpLeases);
         mLooper.dispatchAll();
+
+        return toTetheredClients(dhcpLeases, type);
     }
 
     private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
             int type) throws Exception {
-        final ArrayList<TetheredClient> leases = new ArrayList<>();
+        final ArrayList<TetheredClient> clients = new ArrayList<>();
         for (DhcpLeaseParcelable lease : leaseParcelables) {
             final LinkAddress address = new LinkAddress(
                     intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
@@ -2566,13 +2663,13 @@
             final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
 
             final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
-            leases.add(new TetheredClient(
+            clients.add(new TetheredClient(
                     macAddress,
                     Collections.singletonList(addressInfo),
                     type));
         }
 
-        return leases;
+        return clients;
     }
 
     private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
@@ -2597,7 +2694,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2632,7 +2730,8 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
         result.assertHasResult();
@@ -2653,7 +2752,8 @@
         // already bound.
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), secondResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, secondResult);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
         secondResult.assertHasResult();
@@ -2674,7 +2774,8 @@
     public void testBluetoothServiceDisconnects() throws Exception {
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
                 true /* bindToPanService */);
@@ -2825,18 +2926,26 @@
         runNcmTethering();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
+        verify(mTetheringMetrics).createBuilder(eq(TETHERING_NCM), anyString());
 
         // Change the USB tethering function to NCM. Because the USB tethering function was set to
         // RNDIS (the default), tethering is stopped.
         forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
         verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+        verify(mTetheringMetrics).updateErrorCode(anyInt(), eq(TETHER_ERROR_NO_ERROR));
+        verify(mTetheringMetrics).sendReport(eq(TETHERING_NCM));
 
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
         // available.
         final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), ncmResult);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+                ncmResult);
         mLooper.dispatchAll();
         ncmResult.assertHasResult();
+        verify(mTetheringMetrics, times(2)).createBuilder(eq(TETHERING_NCM), anyString());
+        verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_NCM),
+                eq(TETHER_ERROR_SERVICE_UNAVAIL));
+        verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_NCM));
 
         // Run TETHERING_USB with ncm configuration.
         runDualStackUsbTethering(TEST_NCM_IFNAME);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
new file mode 100644
index 0000000..c34cf5f
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TetheringMetricsTest {
+    private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
+    private static final String SETTINGS_PKG = "com.android.settings";
+    private static final String SYSTEMUI_PKG = "com.android.systemui";
+    private static final String GMS_PKG = "com.google.android.gms";
+    private TetheringMetrics mTetheringMetrics;
+
+    private final NetworkTetheringReported.Builder mStatsBuilder =
+            NetworkTetheringReported.newBuilder();
+
+    private class MockTetheringMetrics extends TetheringMetrics {
+        @Override
+        public void write(final NetworkTetheringReported reported) { }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTetheringMetrics = spy(new MockTetheringMetrics());
+    }
+
+    private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
+            throws Exception {
+        for (Pair<Integer, DownstreamType> testPair : testPairs) {
+            final int type = testPair.first;
+            final DownstreamType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+            mTetheringMetrics.updateErrorCode(type, TETHER_ERROR_NO_ERROR);
+            mTetheringMetrics.sendReport(type);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(expectedResult)
+                    .setUserType(UserType.USER_UNKNOWN)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testDownstreamTypes() throws Exception {
+        runDownstreamTypesTest(new Pair<>(TETHERING_WIFI, DownstreamType.DS_TETHERING_WIFI),
+                new Pair<>(TETHERING_WIFI_P2P, DownstreamType.DS_TETHERING_WIFI_P2P),
+                new Pair<>(TETHERING_BLUETOOTH, DownstreamType.DS_TETHERING_BLUETOOTH),
+                new Pair<>(TETHERING_USB, DownstreamType.DS_TETHERING_USB),
+                new Pair<>(TETHERING_NCM, DownstreamType.DS_TETHERING_NCM),
+                new Pair<>(TETHERING_ETHERNET, DownstreamType.DS_TETHERING_ETHERNET));
+    }
+
+    private void runErrorCodesTest(final Pair<Integer, ErrorCode>... testPairs)
+            throws Exception {
+        for (Pair<Integer, ErrorCode> testPair : testPairs) {
+            final int errorCode = testPair.first;
+            final ErrorCode expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
+            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, errorCode);
+            mTetheringMetrics.sendReport(TETHERING_WIFI);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
+                    .setUserType(UserType.USER_UNKNOWN)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(expectedResult)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testErrorCodes() throws Exception {
+        runErrorCodesTest(new Pair<>(TETHER_ERROR_NO_ERROR, ErrorCode.EC_NO_ERROR),
+                new Pair<>(TETHER_ERROR_UNKNOWN_IFACE, ErrorCode.EC_UNKNOWN_IFACE),
+                new Pair<>(TETHER_ERROR_SERVICE_UNAVAIL, ErrorCode.EC_SERVICE_UNAVAIL),
+                new Pair<>(TETHER_ERROR_UNSUPPORTED, ErrorCode.EC_UNSUPPORTED),
+                new Pair<>(TETHER_ERROR_UNAVAIL_IFACE, ErrorCode.EC_UNAVAIL_IFACE),
+                new Pair<>(TETHER_ERROR_INTERNAL_ERROR, ErrorCode.EC_INTERNAL_ERROR),
+                new Pair<>(TETHER_ERROR_TETHER_IFACE_ERROR, ErrorCode.EC_TETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_UNTETHER_IFACE_ERROR, ErrorCode.EC_UNTETHER_IFACE_ERROR),
+                new Pair<>(TETHER_ERROR_ENABLE_FORWARDING_ERROR,
+                ErrorCode.EC_ENABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_DISABLE_FORWARDING_ERROR,
+                ErrorCode.EC_DISABLE_FORWARDING_ERROR),
+                new Pair<>(TETHER_ERROR_IFACE_CFG_ERROR, ErrorCode.EC_IFACE_CFG_ERROR),
+                new Pair<>(TETHER_ERROR_PROVISIONING_FAILED, ErrorCode.EC_PROVISIONING_FAILED),
+                new Pair<>(TETHER_ERROR_DHCPSERVER_ERROR, ErrorCode.EC_DHCPSERVER_ERROR),
+                new Pair<>(TETHER_ERROR_ENTITLEMENT_UNKNOWN, ErrorCode.EC_ENTITLEMENT_UNKNOWN),
+                new Pair<>(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION,
+                ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION),
+                new Pair<>(TETHER_ERROR_UNKNOWN_TYPE, ErrorCode.EC_UNKNOWN_TYPE));
+    }
+
+    private void runUserTypesTest(final Pair<String, UserType>... testPairs)
+            throws Exception {
+        for (Pair<String, UserType> testPair : testPairs) {
+            final String callerPkg = testPair.first;
+            final UserType expectedResult = testPair.second;
+
+            mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+            mTetheringMetrics.sendReport(TETHERING_WIFI);
+            NetworkTetheringReported expectedReport =
+                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
+                    .setUserType(expectedResult)
+                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                    .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .build();
+            verify(mTetheringMetrics).write(expectedReport);
+            reset(mTetheringMetrics);
+        }
+    }
+
+    @Test
+    public void testUserTypes() throws Exception {
+        runUserTypesTest(new Pair<>(TEST_CALLER_PKG, UserType.USER_UNKNOWN),
+                new Pair<>(SETTINGS_PKG, UserType.USER_SETTINGS),
+                new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
+                new Pair<>(GMS_PKG, UserType.USER_GMS));
+    }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 1fe0e9a..23af3e3 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -61,6 +61,7 @@
 bpf {
     name: "block.o",
     srcs: ["block.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -71,6 +72,7 @@
 bpf {
     name: "dscp_policy.o",
     srcs: ["dscp_policy.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -97,8 +99,9 @@
 }
 
 bpf {
-    name: "clatd.o_mainline",
+    name: "clatd.o",
     srcs: ["clatd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -110,8 +113,9 @@
 }
 
 bpf {
-    name: "netd.o_mainline",
+    name: "netd.o",
     srcs: ["netd.c"],
+    btf: true,
     cflags: [
         "-Wall",
         "-Werror",
@@ -119,5 +123,5 @@
     include_dirs: [
         "frameworks/libs/net/common/netd/libnetdutils/include",
     ],
-    sub_dir: "net_shared",
+    sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index ddd9a1c..f2a3e62 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,6 +19,9 @@
 #include <netinet/in.h>
 #include <stdint.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 
 #define ALLOW 1
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index a6e78b6..14fcdd6 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -98,29 +98,29 @@
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 2000;
 
-#define BPF_PATH "/sys/fs/bpf/net_shared/"
+#define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
 
-#define BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_egress_stats"
-#define BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_denylist_xtbpf"
-#define CGROUP_SOCKET_PROG_PATH BPF_PATH "prog_netd_cgroupsock_inet_create"
+#define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
+#define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
+#define XT_BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf"
+#define XT_BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf"
+#define XT_BPF_ALLOWLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf"
+#define XT_BPF_DENYLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf"
+#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
-#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
+#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
 
-#define COOKIE_TAG_MAP_PATH BPF_PATH "map_netd_cookie_tag_map"
-#define UID_COUNTERSET_MAP_PATH BPF_PATH "map_netd_uid_counterset_map"
-#define APP_UID_STATS_MAP_PATH BPF_PATH "map_netd_app_uid_stats_map"
-#define STATS_MAP_A_PATH BPF_PATH "map_netd_stats_map_A"
-#define STATS_MAP_B_PATH BPF_PATH "map_netd_stats_map_B"
-#define IFACE_INDEX_NAME_MAP_PATH BPF_PATH "map_netd_iface_index_name_map"
-#define IFACE_STATS_MAP_PATH BPF_PATH "map_netd_iface_stats_map"
-#define CONFIGURATION_MAP_PATH BPF_PATH "map_netd_configuration_map"
-#define UID_OWNER_MAP_PATH BPF_PATH "map_netd_uid_owner_map"
-#define UID_PERMISSION_MAP_PATH BPF_PATH "map_netd_uid_permission_map"
+#define COOKIE_TAG_MAP_PATH BPF_NETD_PATH "map_netd_cookie_tag_map"
+#define UID_COUNTERSET_MAP_PATH BPF_NETD_PATH "map_netd_uid_counterset_map"
+#define APP_UID_STATS_MAP_PATH BPF_NETD_PATH "map_netd_app_uid_stats_map"
+#define STATS_MAP_A_PATH BPF_NETD_PATH "map_netd_stats_map_A"
+#define STATS_MAP_B_PATH BPF_NETD_PATH "map_netd_stats_map_B"
+#define IFACE_INDEX_NAME_MAP_PATH BPF_NETD_PATH "map_netd_iface_index_name_map"
+#define IFACE_STATS_MAP_PATH BPF_NETD_PATH "map_netd_iface_stats_map"
+#define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
+#define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
+#define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
 
 enum UidOwnerMatchType {
     NO_MATCH = 0,
@@ -132,6 +132,7 @@
     RESTRICTED_MATCH = (1 << 5),
     LOW_POWER_STANDBY_MATCH = (1 << 6),
     IIF_MATCH = (1 << 7),
+    LOCKDOWN_VPN_MATCH = (1 << 8),
 };
 
 enum BpfPermissionMatch {
@@ -162,13 +163,15 @@
 #define UID_RULES_CONFIGURATION_KEY 1
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
 
+#define BPF_CLATD_PATH "/sys/fs/bpf/net_shared/"
+
 #define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
 #define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
 
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_PATH CLAT_INGRESS6_PROG_ETHER_NAME
+#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
+#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_ETHER_NAME
 
-#define CLAT_INGRESS6_MAP_PATH BPF_PATH "map_clatd_clat_ingress6_map"
+#define CLAT_INGRESS6_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_ingress6_map"
 
 typedef struct {
     uint32_t iif;            // The input interface index
@@ -186,10 +189,10 @@
 #define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
 #define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
 
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_PATH CLAT_EGRESS4_PROG_ETHER_NAME
+#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
+#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_ETHER_NAME
 
-#define CLAT_EGRESS4_MAP_PATH BPF_PATH "map_clatd_clat_egress4_map"
+#define CLAT_EGRESS4_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_egress4_map"
 
 typedef struct {
     uint32_t iif;           // The input interface index
diff --git a/bpf_progs/bpf_tethering.h b/bpf_progs/bpf_tethering.h
index b0ec8f6..f9ef6ef 100644
--- a/bpf_progs/bpf_tethering.h
+++ b/bpf_progs/bpf_tethering.h
@@ -73,10 +73,6 @@
 #define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
 
 
-#define BPF_PATH_TETHER BPF_PATH "tethering/"
-
-#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
-
 typedef uint32_t TetherStatsKey;  // upstream ifindex
 
 typedef struct {
@@ -89,19 +85,9 @@
 } TetherStatsValue;
 STRUCT_SIZE(TetherStatsValue, 6 * 8);  // 48
 
-#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
-
 typedef uint32_t TetherLimitKey;    // upstream ifindex
 typedef uint64_t TetherLimitValue;  // in bytes
 
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
-
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
-
 // For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
 // because all downstream types that are currently supported (WiFi, USB, Bluetooth and
 // Ethernet) have 6-byte MAC addresses.
@@ -121,8 +107,6 @@
 } Tether6Value;
 STRUCT_SIZE(Tether6Value, 4 + 14 + 2);  // 20
 
-#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -146,14 +130,6 @@
 } TetherDownstream64Value;
 STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8);  // 40
 
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
-
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -162,23 +138,6 @@
 } TetherUpstream6Key;
 STRUCT_SIZE(TetherUpstream6Key, 12);
 
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
-
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
-
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
-
 typedef struct {
     uint32_t iif;              // The input interface index
     uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
@@ -202,16 +161,4 @@
 } Tether4Value;
 STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8);  // 64
 
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
-
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
-
 #undef STRUCT_SIZE
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 9a9d337..c5b8555 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,6 +30,9 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "bpf_shared.h"
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
index d5df7ef..538a9e4 100644
--- a/bpf_progs/dscp_policy.c
+++ b/bpf_progs/dscp_policy.c
@@ -27,6 +27,9 @@
 #include <netinet/udp.h>
 #include <string.h>
 
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include "bpf_helpers.h"
 #include "dscp_policy.h"
 
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscp_policy.h
index 777c4ff..1637f7a 100644
--- a/bpf_progs/dscp_policy.h
+++ b/bpf_progs/dscp_policy.h
@@ -26,12 +26,11 @@
 
 #define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
 
-#ifndef v6_equal
-#define v6_equal(a, b)    (a.s6_addr32[0] == b.s6_addr32[0] && \
-                 a.s6_addr32[1] == b.s6_addr32[1] && \
-                 a.s6_addr32[2] == b.s6_addr32[2] && \
-                 a.s6_addr32[3] == b.s6_addr32[3])
-#endif
+#define v6_equal(a, b) \
+    (((a.s6_addr32[0] ^ b.s6_addr32[0]) | \
+      (a.s6_addr32[1] ^ b.s6_addr32[1]) | \
+      (a.s6_addr32[2] ^ b.s6_addr32[2]) | \
+      (a.s6_addr32[3] ^ b.s6_addr32[3])) == 0)
 
 // TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
 // smove to common location in future.
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index fe9a871..ae92686 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// The resulting .o needs to load on the Android T Beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
 #include <bpf_helpers.h>
 #include <linux/bpf.h>
 #include <linux/if.h>
@@ -194,7 +197,7 @@
     BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
 
     UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
-    uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+    uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
 
     if (enabledRules) {
@@ -214,9 +217,16 @@
             return BPF_DROP;
         }
     }
-    if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
-        // Drops packets not coming from lo nor the allowlisted interface
-        if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+    if (direction == BPF_INGRESS && skb->ifindex != 1) {
+        if (uidRules & IIF_MATCH) {
+            if (allowed_iif && skb->ifindex != allowed_iif) {
+                // Drops packets not coming from lo nor the allowed interface
+                // allowed interface=0 is a wildcard and does not drop packets
+                return BPF_DROP_UNLESS_DNS;
+            }
+        } else if (uidRules & LOCKDOWN_VPN_MATCH) {
+            // Drops packets not coming from lo and rule does not have IIF_MATCH but has
+            // LOCKDOWN_VPN_MATCH
             return BPF_DROP_UNLESS_DNS;
         }
     }
@@ -373,8 +383,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
-                     KVER(4, 14, 0))
+DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
 (struct bpf_sock* sk) {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 92a774c..2ec0792 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,8 +24,8 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -355,88 +355,10 @@
 
 DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const bool updatetime) {
-    // Require ethernet dst mac address to be our unicast address.
-    if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
-
-    // Must be meta-ethernet IPv4 frame
-    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
-    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-
-    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
-    // not trigger and thus we need to manually make sure we can read packet headers via DPA.
-    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
-    // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
-
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
-    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
-
-    // Must have (ethernet and) ipv4 header
-    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
-
-    // Ethertype - if present - must be IPv4
-    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
-
-    // IP version must be 4
-    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
-
-    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
-
-    // Calculate the IPv4 one's complement checksum of the IPv4 header.
-    __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
-        sum4 += ((__u16*)ip)[i];
-    }
-    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
-    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
-    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
-    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
-
-    // Minimum IPv4 total length is the size of the header
-    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
-
-    // We are incapable of dealing with IPv4 fragments
-    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
-
-    // Cannot decrement during forward if already zero or would be zero,
-    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
-    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
-
-    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
-    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
-    // in such a situation we can only support TCP.  This also has the added nice benefit of
-    // using a separate error counter, and thus making it obvious which version of the program
-    // is loaded.
-    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
-
-    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
-    // but no need to check this if !updatetime due to check immediately above.
-    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
-        TC_PUNT(NON_TCP_UDP);
-
-    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
-    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
-    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
-
-    // This is a bit of a hack to make things easier on the bpf verifier.
-    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
-    // what offsets into the packet are valid and can spuriously reject the program, this is
-    // because it fails to realize that is_tcp && !is_tcp is impossible)
-    //
-    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
-    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
-    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
-    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
-    // As such we *always* need access to at least 8 bytes.
-    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
-
+static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
+        const int l2_header_size, void* data, const void* data_end,
+        struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
+        const bool downstream, const bool updatetime, const bool is_tcp) {
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
 
@@ -625,6 +547,102 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+        const bool downstream, const bool updatetime) {
+    // Require ethernet dst mac address to be our unicast address.
+    if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
+
+    // Must be meta-ethernet IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+    // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+    // not trigger and thus we need to manually make sure we can read packet headers via DPA.
+    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
+    // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
+    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    struct ethhdr* eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+    // Must have (ethernet and) ipv4 header
+    if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
+
+    // Ethertype - if present - must be IPv4
+    if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
+
+    // IP version must be 4
+    if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+    if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+    // Calculate the IPv4 one's complement checksum of the IPv4 header.
+    __wsum sum4 = 0;
+    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+        sum4 += ((__u16*)ip)[i];
+    }
+    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
+    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+    if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+    // Minimum IPv4 total length is the size of the header
+    if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+    // We are incapable of dealing with IPv4 fragments
+    if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+    // Cannot decrement during forward if already zero or would be zero,
+    // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+    if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+    // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+    // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+    // in such a situation we can only support TCP.  This also has the added nice benefit of
+    // using a separate error counter, and thus making it obvious which version of the program
+    // is loaded.
+    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+    // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+    // but no need to check this if !updatetime due to check immediately above.
+    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+        TC_PUNT(NON_TCP_UDP);
+
+    // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+    // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
+    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+    // This is a bit of a hack to make things easier on the bpf verifier.
+    // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+    // what offsets into the packet are valid and can spuriously reject the program, this is
+    // because it fails to realize that is_tcp && !is_tcp is impossible)
+    //
+    // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+    // always be in the first 4 bytes of the L4 header.  Additionally for UDP we'll need access
+    // to the checksum field which is in bytes 7 and 8.  While for TCP we'll need to read the
+    // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+    // As such we *always* need access to at least 8 bytes.
+    if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+    // We're forcing the compiler to emit two copies of the following code, optimized
+    // separately for is_tcp being true or false.  This simplifies the resulting bpf
+    // byte code sufficiently that the 4.14 bpf verifier is able to keep track of things.
+    // Without this (updatetime == true) case would fail to bpf verify on 4.14 even
+    // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
+    if (is_tcp) {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ true);
+    } else {
+      return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+                                is_ethernet, downstream, updatetime, /* is_tcp */ false);
+    }
+}
+
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index c9c73f1..f2fcc8c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,8 +18,8 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index 886d194..b8070f0 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -22,13 +22,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
@@ -573,7 +571,6 @@
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
-    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
     public void enableInterface(
             @NonNull String iface,
             @Nullable @CallbackExecutor Executor executor,
@@ -582,7 +579,7 @@
         final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                 executor, callback);
         try {
-            mService.connectNetwork(iface, proxy);
+            mService.enableInterface(iface, proxy);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -610,7 +607,6 @@
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
-    @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
     public void disableInterface(
             @NonNull String iface,
             @Nullable @CallbackExecutor Executor executor,
@@ -619,7 +615,7 @@
         final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                 executor, callback);
         try {
-            mService.disconnectNetwork(iface, proxy);
+            mService.disableInterface(iface, proxy);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/framework-t/src/android/net/IEthernetManager.aidl b/framework-t/src/android/net/IEthernetManager.aidl
index 42e4c1a..c1efc29 100644
--- a/framework-t/src/android/net/IEthernetManager.aidl
+++ b/framework-t/src/android/net/IEthernetManager.aidl
@@ -43,8 +43,8 @@
     void releaseTetheredInterface(in ITetheredInterfaceCallback callback);
     void updateConfiguration(String iface, in EthernetNetworkUpdateRequest request,
         in INetworkInterfaceOutcomeReceiver listener);
-    void connectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
-    void disconnectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void enableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
+    void disableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
     void setEthernetEnabled(boolean enabled);
     List<String> getInterfaceList();
 }
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 9cb0947..9cceac2 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -817,10 +817,10 @@
          * </ol>
          *
          * @param underlyingNetwork the new {@link Network} that will carry traffic for this tunnel.
-         *     This network MUST never be the network exposing this IpSecTunnelInterface, otherwise
-         *     this method will throw an {@link IllegalArgumentException}. If the
-         *     IpSecTunnelInterface is later added to this network, all outbound traffic will be
-         *     blackholed.
+         *     This network MUST be a functional {@link Network} with valid {@link LinkProperties},
+         *     and MUST never be the network exposing this IpSecTunnelInterface, otherwise this
+         *     method will throw an {@link IllegalArgumentException}. If the IpSecTunnelInterface is
+         *     later added to this network, all outbound traffic will be blackholed.
          */
         // TODO: b/169171001 Update the documentation when transform migration is supported.
         // The purpose of making updating network and applying transforms separate is to leave open
@@ -962,7 +962,6 @@
      * IP header and IPsec Header on all inbound traffic).
      * <p>Applications should probably not use this API directly.
      *
-     *
      * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
      *        transform.
      * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index b59a890..29ea772 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -694,6 +694,26 @@
         }
     }
 
+    /**
+     * Remove histories which contains or is before the cutoff timestamp.
+     * @hide
+     */
+    public void removeHistoryBefore(long cutoffMillis) {
+        final ArrayList<Key> knownKeys = new ArrayList<>();
+        knownKeys.addAll(mStats.keySet());
+
+        for (Key key : knownKeys) {
+            final NetworkStatsHistory history = mStats.get(key);
+            if (history.getStart() > cutoffMillis) continue;
+
+            history.removeBucketsStartingBefore(cutoffMillis);
+            if (history.size() == 0) {
+                mStats.remove(key);
+            }
+            mDirty = true;
+        }
+    }
+
     private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
         if (startMillis < mStartMillis) mStartMillis = startMillis;
         if (endMillis > mEndMillis) mEndMillis = endMillis;
diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java
index 301fef9..b45d44d 100644
--- a/framework-t/src/android/net/NetworkStatsHistory.java
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -680,19 +680,21 @@
     }
 
     /**
-     * Remove buckets older than requested cutoff.
+     * Remove buckets that start older than requested cutoff.
+     *
+     * This method will remove any bucket that contains any data older than the requested
+     * cutoff, even if that same bucket includes some data from after the cutoff.
+     *
      * @hide
      */
-    public void removeBucketsBefore(long cutoff) {
+    public void removeBucketsStartingBefore(final long cutoff) {
         // TODO: Consider use getIndexBefore.
         int i;
         for (i = 0; i < bucketCount; i++) {
             final long curStart = bucketStart[i];
-            final long curEnd = curStart + bucketDuration;
 
-            // cutoff happens before or during this bucket; everything before
-            // this bucket should be removed.
-            if (curEnd > cutoff) break;
+            // This bucket starts after or at the cutoff, so it should be kept.
+            if (curStart >= cutoff) break;
         }
 
         if (i > 0) {
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a174fe3..9f9ee95 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,16 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
+    /**
+     * Firewall chain used for lockdown VPN.
+     * Denylist of apps that cannot receive incoming packets except on loopback because they are
+     * subject to an always-on VPN which is not currently connected.
+     *
+     * @see #BLOCKED_REASON_LOCKDOWN_VPN
+     * @hide
+     */
+    public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,7 +999,8 @@
         FIREWALL_CHAIN_STANDBY,
         FIREWALL_CHAIN_POWERSAVE,
         FIREWALL_CHAIN_RESTRICTED,
-        FIREWALL_CHAIN_LOW_POWER_STANDBY
+        FIREWALL_CHAIN_LOW_POWER_STANDBY,
+        FIREWALL_CHAIN_LOCKDOWN_VPN
     })
     public @interface FirewallChain {}
     // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
@@ -2589,9 +2600,24 @@
      * {@hide}
      */
     public ConnectivityManager(Context context, IConnectivityManager service) {
+        this(context, service, true /* newStatic */);
+    }
+
+    private ConnectivityManager(Context context, IConnectivityManager service, boolean newStatic) {
         mContext = Objects.requireNonNull(context, "missing context");
         mService = Objects.requireNonNull(service, "missing IConnectivityManager");
-        sInstance = this;
+        // sInstance is accessed without a lock, so it may actually be reassigned several times with
+        // different ConnectivityManager, but that's still OK considering its usage.
+        if (sInstance == null && newStatic) {
+            final Context appContext = mContext.getApplicationContext();
+            // Don't create static ConnectivityManager instance again to prevent infinite loop.
+            // If the application context is null, we're either in the system process or
+            // it's the application context very early in app initialization. In both these
+            // cases, the passed-in Context will not be freed, so it's safe to pass it to the
+            // service. http://b/27532714 .
+            sInstance = new ConnectivityManager(appContext != null ? appContext : context, service,
+                    false /* newStatic */);
+        }
     }
 
     /** {@hide} */
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 126a5ff..a8f707e 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -1366,6 +1366,21 @@
     }
 
     /**
+     * Returns true if this link has a throw route.
+     *
+     * @return {@code true} if there is an exclude route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasExcludeRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.getType() == RouteInfo.RTN_THROW) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Compares this {@code LinkProperties} interface name against the target
      *
      * @param target LinkProperties to compare.
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index fb271e3..fdcab02 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -120,8 +120,8 @@
     public String toString() {
         return "ProfileNetworkPreference{"
                 + "mPreference=" + getPreference()
-                + "mIncludedUids=" + mIncludedUids.toString()
-                + "mExcludedUids=" + mExcludedUids.toString()
+                + "mIncludedUids=" + Arrays.toString(mIncludedUids)
+                + "mExcludedUids=" + Arrays.toString(mExcludedUids)
                 + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
                 + '}';
     }
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index ed6eb15..b80cff4 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -46,8 +46,10 @@
             EX_TYPE_FILTER_NONE,
             EX_TYPE_FILTER_NETWORK_RELEASED,
             EX_TYPE_FILTER_SOCKET_NOT_BOUND,
+            EX_TYPE_FILTER_SOCKET_NOT_CONNECTED,
             EX_TYPE_FILTER_NOT_SUPPORTED,
             EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED,
+            EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ExceptionType {}
@@ -65,10 +67,16 @@
     public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
+    public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 3;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
+    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 4;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 5;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED = 6;
 
     /**
      * Creates exception based off of a type and message.  Not all types of exceptions accept a
@@ -83,12 +91,17 @@
                 return new QosCallbackException(new NetworkReleasedException());
             case EX_TYPE_FILTER_SOCKET_NOT_BOUND:
                 return new QosCallbackException(new SocketNotBoundException());
+            case EX_TYPE_FILTER_SOCKET_NOT_CONNECTED:
+                return new QosCallbackException(new SocketNotConnectedException());
             case EX_TYPE_FILTER_NOT_SUPPORTED:
                 return new QosCallbackException(new UnsupportedOperationException(
                         "This device does not support the specified filter"));
             case EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED:
                 return new QosCallbackException(
                         new SocketLocalAddressChangedException());
+            case EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED:
+                return new QosCallbackException(
+                        new SocketRemoteAddressChangedException());
             default:
                 Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'");
                 return new QosCallbackException(
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index 5c1c3cc..b432644 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -90,5 +90,15 @@
      */
     public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
             int startPort, int endPort);
+
+    /**
+     * Determines whether or not the parameter will be matched with this filter.
+     *
+     * @param protocol the protocol such as TCP or UDP included in IP packet filter set of a QoS
+     *                 flow assigned on {@link Network}.
+     * @return whether the parameters match the socket type of the filter
+     * @hide
+     */
+    public abstract boolean matchesProtocol(int protocol);
 }
 
diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java
index da3b2cf..6e71fa3 100644
--- a/framework/src/android/net/QosFilterParcelable.java
+++ b/framework/src/android/net/QosFilterParcelable.java
@@ -104,7 +104,7 @@
         if (mQosFilter instanceof QosSocketFilter) {
             dest.writeInt(QOS_SOCKET_FILTER);
             final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter;
-            qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0);
+            qosSocketFilter.getQosSocketInfo().writeToParcelWithoutFd(dest, 0);
             return;
         }
         dest.writeInt(NO_FILTER_PRESENT);
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
index 69da7f4..5ceeb67 100644
--- a/framework/src/android/net/QosSocketFilter.java
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -18,6 +18,13 @@
 
 import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
 import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -74,19 +81,34 @@
      * 2. In the scenario that the socket is now bound to a different local address, which can
      *    happen in the case of UDP, then
      *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED} is returned.
+     * 3. In the scenario that the UDP socket changed remote address, then
+     *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED} is returned.
+     *
      * @return validation error code
      */
     @Override
     public int validate() {
-        final InetSocketAddress sa = getAddressFromFileDescriptor();
-        if (sa == null) {
-            return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+        final InetSocketAddress sa = getLocalAddressFromFileDescriptor();
+
+        if (sa == null || (sa.getAddress().isAnyLocalAddress() && sa.getPort() == 0)) {
+            return EX_TYPE_FILTER_SOCKET_NOT_BOUND;
         }
 
         if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) {
             return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
         }
 
+        if (mQosSocketInfo.getRemoteSocketAddress() != null) {
+            final InetSocketAddress da = getRemoteAddressFromFileDescriptor();
+            if (da == null) {
+                return EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+            }
+
+            if (!da.equals(mQosSocketInfo.getRemoteSocketAddress())) {
+                return EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+            }
+        }
+
         return EX_TYPE_FILTER_NONE;
     }
 
@@ -98,17 +120,14 @@
      * @return the local address
      */
     @Nullable
-    private InetSocketAddress getAddressFromFileDescriptor() {
+    private InetSocketAddress getLocalAddressFromFileDescriptor() {
         final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
-        if (parcelFileDescriptor == null) return null;
-
         final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
-        if (fd == null) return null;
 
         final SocketAddress address;
         try {
             address = Os.getsockname(fd);
-        } catch (final ErrnoException e) {
+        } catch (ErrnoException e) {
             Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e);
             return null;
         }
@@ -119,6 +138,31 @@
     }
 
     /**
+     * The remote address of the socket's connected.
+     *
+     * <p>Note: If the socket is no longer connected, null is returned.
+     *
+     * @return the remote address
+     */
+    @Nullable
+    private InetSocketAddress getRemoteAddressFromFileDescriptor() {
+        final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
+        final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
+
+        final SocketAddress address;
+        try {
+            address = Os.getpeername(fd);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "getAddressFromFileDescriptor: getRemoteAddress exception", e);
+            return null;
+        }
+        if (address instanceof InetSocketAddress) {
+            return (InetSocketAddress) address;
+        }
+        return null;
+    }
+
+    /**
      * The network used with this filter.
      *
      * @return the registered {@link Network}
@@ -156,6 +200,18 @@
     }
 
     /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean matchesProtocol(final int protocol) {
+        if ((mQosSocketInfo.getSocketType() == SOCK_STREAM && protocol == IPPROTO_TCP)
+                || (mQosSocketInfo.getSocketType() == SOCK_DGRAM && protocol == IPPROTO_UDP)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
      * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
      * filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
@@ -174,6 +230,7 @@
             final int startPort, final int endPort) {
         return startPort <= filterSocketAddress.getPort()
                 && endPort >= filterSocketAddress.getPort()
-                && filterSocketAddress.getAddress().equals(address);
+                && (address.isAnyLocalAddress()
+                        || filterSocketAddress.getAddress().equals(address));
     }
 }
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 39c2f33..49ac22b 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -165,25 +165,28 @@
     /* Parcelable methods */
     private QosSocketInfo(final Parcel in) {
         mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
-        mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        final boolean withFd = in.readBoolean();
+        if (withFd) {
+            mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+        } else {
+            mParcelFileDescriptor = null;
+        }
 
-        final int localAddressLength = in.readInt();
-        mLocalSocketAddress = readSocketAddress(in, localAddressLength);
-
-        final int remoteAddressLength = in.readInt();
-        mRemoteSocketAddress = remoteAddressLength == 0 ? null
-                : readSocketAddress(in, remoteAddressLength);
+        mLocalSocketAddress = readSocketAddress(in);
+        mRemoteSocketAddress = readSocketAddress(in);
 
         mSocketType = in.readInt();
     }
 
-    private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
-        final byte[] address = new byte[addressLength];
-        in.readByteArray(address);
+    private InetSocketAddress readSocketAddress(final Parcel in) {
+        final byte[] addrBytes = in.createByteArray();
+        if (addrBytes == null) {
+            return null;
+        }
         final int port = in.readInt();
 
         try {
-            return new InetSocketAddress(InetAddress.getByAddress(address), port);
+            return new InetSocketAddress(InetAddress.getByAddress(addrBytes), port);
         } catch (final UnknownHostException e) {
             /* This can never happen. UnknownHostException will never be thrown
                since the address provided is numeric and non-null. */
@@ -198,20 +201,35 @@
 
     @Override
     public void writeToParcel(@NonNull final Parcel dest, final int flags) {
-        mNetwork.writeToParcel(dest, 0);
-        mParcelFileDescriptor.writeToParcel(dest, 0);
+        writeToParcelInternal(dest, flags, /*includeFd=*/ true);
+    }
 
-        final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
-        dest.writeInt(localAddress.length);
-        dest.writeByteArray(localAddress);
+    /**
+     * Used when sending QosSocketInfo to telephony, which does not need access to the socket FD.
+     * @hide
+     */
+    public void writeToParcelWithoutFd(@NonNull final Parcel dest, final int flags) {
+        writeToParcelInternal(dest, flags, /*includeFd=*/ false);
+    }
+
+    private void writeToParcelInternal(
+            @NonNull final Parcel dest, final int flags, boolean includeFd) {
+        mNetwork.writeToParcel(dest, 0);
+
+        if (includeFd) {
+            dest.writeBoolean(true);
+            mParcelFileDescriptor.writeToParcel(dest, 0);
+        } else {
+            dest.writeBoolean(false);
+        }
+
+        dest.writeByteArray(mLocalSocketAddress.getAddress().getAddress());
         dest.writeInt(mLocalSocketAddress.getPort());
 
         if (mRemoteSocketAddress == null) {
-            dest.writeInt(0);
+            dest.writeByteArray(null);
         } else {
-            final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
-            dest.writeInt(remoteAddress.length);
-            dest.writeByteArray(remoteAddress);
+            dest.writeByteArray(mRemoteSocketAddress.getAddress().getAddress());
             dest.writeInt(mRemoteSocketAddress.getPort());
         }
         dest.writeInt(mSocketType);
diff --git a/framework/src/android/net/SocketNotConnectedException.java b/framework/src/android/net/SocketNotConnectedException.java
new file mode 100644
index 0000000..fa2a615
--- /dev/null
+++ b/framework/src/android/net/SocketNotConnectedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Thrown when a previously bound socket becomes unbound.
+ *
+ * @hide
+ */
+public class SocketNotConnectedException extends Exception {
+    /** @hide */
+    public SocketNotConnectedException() {
+        super("The socket is not connected");
+    }
+}
diff --git a/framework/src/android/net/SocketRemoteAddressChangedException.java b/framework/src/android/net/SocketRemoteAddressChangedException.java
new file mode 100644
index 0000000..ecaeebc
--- /dev/null
+++ b/framework/src/android/net/SocketRemoteAddressChangedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Thrown when the local address of the socket has changed.
+ *
+ * @hide
+ */
+public class SocketRemoteAddressChangedException extends Exception {
+    /** @hide */
+    public SocketRemoteAddressChangedException() {
+        super("The remote address of the socket changed");
+    }
+}
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index f3dfb57..42d0de5 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -73,16 +73,7 @@
     }
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
     RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
-
-    // For the devices that support cgroup socket filter, the socket filter
-    // should be loaded successfully by bpfloader. So we attach the filter to
-    // cgroup if the program is pinned properly.
-    // TODO: delete the if statement once all devices should support cgroup
-    // socket filter (ie. the minimum kernel version required is 4.14).
-    if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
-        RETURN_IF_NOT_OK(
-                attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
-    }
+    RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
     return netdutils::status::ok;
 }
 
@@ -113,6 +104,7 @@
     RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
                                                   BPF_ANY));
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -242,7 +234,7 @@
     if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
     base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
     if (!res.ok()) {
-        ALOGE("Failed to untag socket: %s\n", strerror(res.error().code()));
+        ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
     return 0;
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 4d605ce..6c7a15e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -205,6 +205,10 @@
               configuration.error().message().c_str());
         return -configuration.error().code();
     }
+    if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+        ALOGE("%s unknown configuration value: %d", __func__, configuration.value());
+        return -EINVAL;
+    }
     const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
     BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
     if (!statsMap.isValid()) {
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index 4bc40ea..16b9f1e 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -1452,6 +1452,11 @@
         final ConnectivityManager connectivityManager =
                 mContext.getSystemService(ConnectivityManager.class);
         final LinkProperties lp = connectivityManager.getLinkProperties(underlyingNetwork);
+        if (lp == null) {
+            throw new IllegalArgumentException(
+                    "LinkProperties is null. The underlyingNetwork may not be functional");
+        }
+
         if (tunnelInterfaceInfo.getInterfaceName().equals(lp.getInterfaceName())) {
             throw new IllegalArgumentException(
                     "Underlying network cannot be the network being exposed by this tunnel");
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 4086e4e..6def44f 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -513,7 +513,7 @@
                             break;
                         }
 
-                        String name = fullName.substring(0, index);
+                        String name = unescape(fullName.substring(0, index));
                         String rest = fullName.substring(index);
                         String type = rest.replace(".local.", "");
 
@@ -590,6 +590,35 @@
        }
     }
 
+    // The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
+    // for passing to standard system DNS APIs such as res_query() . Thus, make the service name
+    // unescape for getting right service address. See "Notes on DNS Name Escaping" on
+    // external/mdnsresponder/mDNSShared/dns_sd.h for more details.
+    private String unescape(String s) {
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); ++i) {
+            char c = s.charAt(i);
+            if (c == '\\') {
+                if (++i >= s.length()) {
+                    Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                    break;
+                }
+                c = s.charAt(i);
+                if (c != '.' && c != '\\') {
+                    if (i + 2 >= s.length()) {
+                        Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+                        break;
+                    }
+                    c = (char) ((c - '0') * 100 + (s.charAt(i + 1) - '0') * 10
+                            + (s.charAt(i + 2) - '0'));
+                    i += 2;
+                }
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
     @VisibleForTesting
     NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
         mCleanupDelayMs = cleanupDelayMs;
@@ -738,7 +767,13 @@
         String type = service.getServiceType();
         int port = service.getPort();
         byte[] textRecord = service.getTxtRecord();
-        return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY);
+        final Network network = service.getNetwork();
+        final int registerInterface = getNetworkInterfaceIndex(network);
+        if (network != null && registerInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to register service on not found");
+            return false;
+        }
+        return mMDnsManager.registerService(regId, name, type, port, textRecord, registerInterface);
     }
 
     private boolean unregisterService(int regId) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
index 6b623f4..6006539 100644
--- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -16,23 +16,37 @@
 
 package com.android.server.ethernet;
 
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
 import android.annotation.Nullable;
+import android.content.ApexEnvironment;
 import android.net.IpConfiguration;
 import android.os.Environment;
 import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.net.IpConfigStore;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 /**
  * This class provides an API to store and manage Ethernet network configuration.
  */
 public class EthernetConfigStore {
-    private static final String ipConfigFile = Environment.getDataDirectory() +
-            "/misc/ethernet/ipconfig.txt";
+    private static final String TAG = EthernetConfigStore.class.getSimpleName();
+    private static final String CONFIG_FILE = "ipconfig.txt";
+    private static final String FILE_PATH = "/misc/ethernet/";
+    private static final String LEGACY_IP_CONFIG_FILE_PATH = Environment.getDataDirectory()
+            + FILE_PATH;
+    private static final String APEX_IP_CONFIG_FILE_PATH = ApexEnvironment.getApexEnvironment(
+            TETHERING_MODULE_NAME).getDeviceProtectedDataDir() + FILE_PATH;
 
     private IpConfigStore mStore = new IpConfigStore();
-    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private final ArrayMap<String, IpConfiguration> mIpConfigurations;
     private IpConfiguration mIpConfigurationForDefaultInterface;
     private final Object mSync = new Object();
 
@@ -40,22 +54,70 @@
         mIpConfigurations = new ArrayMap<>(0);
     }
 
-    public void read() {
-        synchronized (mSync) {
-            ArrayMap<String, IpConfiguration> configs =
-                    IpConfigStore.readIpConfigurations(ipConfigFile);
+    private static boolean doesConfigFileExist(final String filepath) {
+        return new File(filepath).exists();
+    }
 
-            // This configuration may exist in old file versions when there was only a single active
-            // Ethernet interface.
-            if (configs.containsKey("0")) {
-                mIpConfigurationForDefaultInterface = configs.remove("0");
+    private void writeLegacyIpConfigToApexPath(final String newFilePath, final String oldFilePath,
+            final String filename) {
+        final File directory = new File(newFilePath);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
+
+        // Write the legacy IP config to the apex file path.
+        FileOutputStream fos = null;
+        final AtomicFile dst = new AtomicFile(new File(newFilePath + filename));
+        final AtomicFile src = new AtomicFile(new File(oldFilePath + filename));
+        try {
+            final byte[] raw = src.readFully();
+            if (raw.length > 0) {
+                fos = dst.startWrite();
+                fos.write(raw);
+                fos.flush();
+                dst.finishWrite(fos);
             }
-
-            mIpConfigurations = configs;
+        } catch (IOException e) {
+            Log.e(TAG, "Fail to sync the legacy IP config to the apex file path.");
+            dst.failWrite(fos);
         }
     }
 
+    public void read() {
+        read(APEX_IP_CONFIG_FILE_PATH, LEGACY_IP_CONFIG_FILE_PATH, CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void read(final String newFilePath, final String oldFilePath, final String filename) {
+        synchronized (mSync) {
+            // Attempt to read the IP configuration from apex file path first.
+            if (doesConfigFileExist(newFilePath + filename)) {
+                loadConfigFileLocked(newFilePath + filename);
+                return;
+            }
+
+            // If the config file doesn't exist in the apex file path, attempt to read it from
+            // the legacy file path, if config file exists, write the legacy IP configuration to
+            // apex config file path, this should just happen on the first boot. New or updated
+            // config entries are only written to the apex config file later.
+            if (!doesConfigFileExist(oldFilePath + filename)) return;
+            loadConfigFileLocked(oldFilePath + filename);
+            writeLegacyIpConfigToApexPath(newFilePath, oldFilePath, filename);
+        }
+    }
+
+    private void loadConfigFileLocked(final String filepath) {
+        final ArrayMap<String, IpConfiguration> configs =
+                IpConfigStore.readIpConfigurations(filepath);
+        mIpConfigurations.putAll(configs);
+    }
+
     public void write(String iface, IpConfiguration config) {
+        write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE);
+    }
+
+    @VisibleForTesting
+    void write(String iface, IpConfiguration config, String filepath) {
         boolean modified;
 
         synchronized (mSync) {
@@ -67,7 +129,7 @@
             }
 
             if (modified) {
-                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+                mStore.writeIpConfigurations(filepath, mIpConfigurations);
             }
         }
     }
@@ -80,9 +142,6 @@
 
     @Nullable
     public IpConfiguration getIpConfigurationForDefaultInterface() {
-        synchronized (mSync) {
-            return mIpConfigurationForDefaultInterface == null
-                    ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
-        }
+        return null;
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index fe27335..79802fb 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -31,10 +31,9 @@
 import android.net.LinkProperties;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
-import android.net.NetworkFactory;
 import android.net.NetworkProvider;
 import android.net.NetworkRequest;
-import android.net.NetworkSpecifier;
+import android.net.NetworkScore;
 import android.net.ip.IIpClient;
 import android.net.ip.IpClientCallbacks;
 import android.net.ip.IpClientManager;
@@ -46,6 +45,7 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -56,25 +56,23 @@
 
 import java.io.FileDescriptor;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * {@link NetworkFactory} that represents Ethernet networks.
- *
- * This class reports a static network score of 70 when it is tracking an interface and that
- * interface's link is up, and a score of 0 otherwise.
+ * {@link NetworkProvider} that manages NetworkOffers for Ethernet networks.
  */
-public class EthernetNetworkFactory extends NetworkFactory {
+public class EthernetNetworkFactory {
     private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
     final static boolean DBG = true;
 
-    private final static int NETWORK_SCORE = 70;
     private static final String NETWORK_TYPE = "Ethernet";
 
     private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
             new ConcurrentHashMap<>();
     private final Handler mHandler;
     private final Context mContext;
+    private final NetworkProvider mProvider;
     final Dependencies mDeps;
 
     public static class Dependencies {
@@ -109,54 +107,24 @@
     }
 
     public EthernetNetworkFactory(Handler handler, Context context) {
-        this(handler, context, new Dependencies());
+        this(handler, context, new NetworkProvider(context, handler.getLooper(), TAG),
+            new Dependencies());
     }
 
     @VisibleForTesting
-    EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
-        super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
-
+    EthernetNetworkFactory(Handler handler, Context context, NetworkProvider provider,
+            Dependencies deps) {
         mHandler = handler;
         mContext = context;
+        mProvider = provider;
         mDeps = deps;
-
-        setScoreFilter(NETWORK_SCORE);
     }
 
-    @Override
-    public boolean acceptRequest(NetworkRequest request) {
-        if (DBG) {
-            Log.d(TAG, "acceptRequest, request: " + request);
-        }
-
-        return networkForRequest(request) != null;
-    }
-
-    @Override
-    protected void needNetworkFor(NetworkRequest networkRequest) {
-        NetworkInterfaceState network = networkForRequest(networkRequest);
-
-        if (network == null) {
-            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
-            return;
-        }
-
-        if (++network.refCount == 1) {
-            network.start();
-        }
-    }
-
-    @Override
-    protected void releaseNetworkFor(NetworkRequest networkRequest) {
-        NetworkInterfaceState network = networkForRequest(networkRequest);
-        if (network == null) {
-            Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
-            return;
-        }
-
-        if (--network.refCount == 0) {
-            network.stop();
-        }
+    /**
+     * Registers the network provider with the system.
+     */
+    public void register() {
+        mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
     }
 
     /**
@@ -194,9 +162,8 @@
         }
 
         final NetworkInterfaceState iface = new NetworkInterfaceState(
-                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, mProvider, mDeps);
         mTrackingInterfaces.put(ifaceName, iface);
-        updateCapabilityFilter();
     }
 
     @VisibleForTesting
@@ -237,7 +204,6 @@
         final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
         iface.updateInterface(ipConfig, capabilities, listener);
         mTrackingInterfaces.put(ifaceName, iface);
-        updateCapabilityFilter();
     }
 
     private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
@@ -248,16 +214,6 @@
        return builder.build();
     }
 
-    private void updateCapabilityFilter() {
-        NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
-        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
-            capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
-        }
-
-        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
-        setCapabilityFilter(capabilitiesFilter);
-    }
-
     private static NetworkCapabilities createDefaultNetworkCapabilities() {
         return NetworkCapabilities.Builder
                 .withoutDefaultCapabilities()
@@ -268,11 +224,8 @@
     protected void removeInterface(String interfaceName) {
         NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
         if (iface != null) {
-            iface.maybeSendNetworkManagementCallbackForAbort();
-            iface.stop();
+            iface.destroy();
         }
-
-        updateCapabilityFilter();
     }
 
     /** Returns true if state has been modified */
@@ -304,37 +257,6 @@
         return mTrackingInterfaces.containsKey(ifaceName);
     }
 
-    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
-        String requestedIface = null;
-
-        NetworkSpecifier specifier = request.getNetworkSpecifier();
-        if (specifier instanceof EthernetNetworkSpecifier) {
-            requestedIface = ((EthernetNetworkSpecifier) specifier)
-                .getInterfaceName();
-        }
-
-        NetworkInterfaceState network = null;
-        if (!TextUtils.isEmpty(requestedIface)) {
-            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
-            if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
-                network = n;
-            }
-        } else {
-            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
-                if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
-                    network = n;
-                    break;
-                }
-            }
-        }
-
-        if (DBG) {
-            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
-        }
-
-        return network;
-    }
-
     private static void maybeSendNetworkManagementCallback(
             @Nullable final INetworkInterfaceOutcomeReceiver listener,
             @Nullable final String iface,
@@ -361,14 +283,16 @@
         private final String mHwAddress;
         private final Handler mHandler;
         private final Context mContext;
-        private final NetworkFactory mNetworkFactory;
+        private final NetworkProvider mNetworkProvider;
         private final Dependencies mDeps;
+        private final NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
 
         private static String sTcpBufferSizes = null;  // Lazy initialized.
 
         private boolean mLinkUp;
         private int mLegacyType;
         private LinkProperties mLinkProperties = new LinkProperties();
+        private Set<NetworkRequest> mRequests = new ArraySet<>();
 
         private volatile @Nullable IpClientManager mIpClient;
         private @NonNull NetworkCapabilities mCapabilities;
@@ -397,8 +321,6 @@
                     ConnectivityManager.TYPE_NONE);
         }
 
-        long refCount = 0;
-
         private class EthernetIpClientCallback extends IpClientCallbacks {
             private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
             private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
@@ -469,17 +391,47 @@
             }
         }
 
+        private class EthernetNetworkOfferCallback implements NetworkProvider.NetworkOfferCallback {
+            @Override
+            public void onNetworkNeeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG, String.format("%s: onNetworkNeeded for request: %s", name, request));
+                }
+                // When the network offer is first registered, onNetworkNeeded is called with all
+                // existing requests.
+                // ConnectivityService filters requests for us based on the NetworkCapabilities
+                // passed in the registerNetworkOffer() call.
+                mRequests.add(request);
+                // if the network is already started, this is a no-op.
+                start();
+            }
+
+            @Override
+            public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+                if (DBG) {
+                    Log.d(TAG,
+                            String.format("%s: onNetworkUnneeded for request: %s", name, request));
+                }
+                mRequests.remove(request);
+                if (mRequests.isEmpty()) {
+                    // not currently serving any requests, stop the network.
+                    stop();
+                }
+            }
+        }
+
         NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
                 @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
-                NetworkFactory networkFactory, Dependencies deps) {
+                NetworkProvider networkProvider, Dependencies deps) {
             name = ifaceName;
             mIpConfig = Objects.requireNonNull(ipConfig);
             mCapabilities = Objects.requireNonNull(capabilities);
             mLegacyType = getLegacyType(mCapabilities);
             mHandler = handler;
             mContext = context;
-            mNetworkFactory = networkFactory;
+            mNetworkProvider = networkProvider;
             mDeps = deps;
+            mNetworkOfferCallback = new EthernetNetworkOfferCallback();
             mHwAddress = hwAddress;
         }
 
@@ -502,9 +454,21 @@
                     + "transport type.");
         }
 
+        private static NetworkScore getBestNetworkScore() {
+            return new NetworkScore.Builder().build();
+        }
+
         private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
             mCapabilities = new NetworkCapabilities(capabilities);
             mLegacyType = getLegacyType(mCapabilities);
+
+            if (mLinkUp) {
+                // registering a new network offer will update the existing one, not install a
+                // new one.
+                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
+                        new NetworkCapabilities(capabilities), cmd -> mHandler.post(cmd),
+                        mNetworkOfferCallback);
+            }
         }
 
         void updateInterface(@Nullable final IpConfiguration ipConfig,
@@ -575,7 +539,7 @@
                     .setLegacyExtraInfo(mHwAddress)
                     .build();
             mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
-                    mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+                    mCapabilities, mLinkProperties, config, mNetworkProvider,
                     new EthernetNetworkAgent.Callbacks() {
                         @Override
                         public void onNetworkUnwanted() {
@@ -666,20 +630,21 @@
             mLinkUp = up;
 
             if (!up) { // was up, goes down
-                // Send an abort on a provisioning request callback if necessary before stopping.
-                maybeSendNetworkManagementCallbackForAbort();
-                stop();
+                // retract network offer and stop IpClient.
+                destroy();
                 // If only setting the interface down, send a callback to signal completion.
                 EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
             } else { // was down, goes up
-                stop();
-                start(listener);
+                // register network offer
+                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
+                        new NetworkCapabilities(mCapabilities), (cmd) -> mHandler.post(cmd),
+                        mNetworkOfferCallback);
             }
 
             return true;
         }
 
-        void stop() {
+        private void stop() {
             // Invalidate all previous start requests
             if (mIpClient != null) {
                 mIpClient.shutdown();
@@ -695,6 +660,13 @@
             mLinkProperties.clear();
         }
 
+        public void destroy() {
+            mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+            maybeSendNetworkManagementCallbackForAbort();
+            stop();
+            mRequests.clear();
+        }
+
         private static void provisionIpClient(@NonNull final IpClientManager ipClient,
                 @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
             if (config.getProxySettings() == ProxySettings.STATIC ||
@@ -734,7 +706,6 @@
         @Override
         public String toString() {
             return getClass().getSimpleName() + "{ "
-                    + "refCount: " + refCount + ", "
                     + "iface: " + name + ", "
                     + "up: " + mLinkUp + ", "
                     + "hwAddress: " + mHwAddress + ", "
@@ -747,7 +718,6 @@
     }
 
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
-        super.dump(fd, pw, args);
         pw.println(getClass().getSimpleName());
         pw.println("Tracking interfaces:");
         pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 5e830ad..71d3e4f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -22,11 +22,11 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.net.EthernetNetworkUpdateRequest;
 import android.net.IEthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.ITetheredInterfaceCallback;
-import android.net.EthernetNetworkUpdateRequest;
 import android.net.IpConfiguration;
 import android.net.NetworkCapabilities;
 import android.os.Binder;
@@ -260,27 +260,27 @@
     }
 
     @Override
-    public void connectNetwork(@NonNull final String iface,
+    public void enableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
-        enforceAdminPermission(iface, true, "connectNetwork()");
+        enforceAdminPermission(iface, false, "enableInterface()");
 
-        mTracker.connectNetwork(iface, listener);
+        mTracker.enableInterface(iface, listener);
     }
 
     @Override
-    public void disconnectNetwork(@NonNull final String iface,
+    public void disableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
-        Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
         Objects.requireNonNull(iface);
         throwIfEthernetNotStarted();
 
-        enforceAdminPermission(iface, true, "connectNetwork()");
+        enforceAdminPermission(iface, false, "disableInterface()");
 
-        mTracker.disconnectNetwork(iface, listener);
+        mTracker.disableInterface(iface, listener);
     }
 
     @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index e9053dd..c8a0412 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -285,13 +285,13 @@
     }
 
     @VisibleForTesting(visibility = PACKAGE)
-    protected void connectNetwork(@NonNull final String iface,
+    protected void enableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
         mHandler.post(() -> updateInterfaceState(iface, true, listener));
     }
 
     @VisibleForTesting(visibility = PACKAGE)
-    protected void disconnectNetwork(@NonNull final String iface,
+    protected void disableInterface(@NonNull final String iface,
             @Nullable final INetworkInterfaceOutcomeReceiver listener) {
         mHandler.post(() -> updateInterfaceState(iface, false, listener));
     }
@@ -586,14 +586,18 @@
         }
     }
 
-    private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+    @VisibleForTesting
+    class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
 
         @Override
         public void onInterfaceLinkStateChanged(String iface, boolean up) {
             if (DBG) {
                 Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
             }
-            mHandler.post(() -> updateInterfaceState(iface, up));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                updateInterfaceState(iface, up);
+            });
         }
 
         @Override
@@ -601,7 +605,10 @@
             if (DBG) {
                 Log.i(TAG, "onInterfaceAdded, iface: " + iface);
             }
-            mHandler.post(() -> maybeTrackInterface(iface));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                maybeTrackInterface(iface);
+            });
         }
 
         @Override
@@ -609,7 +616,10 @@
             if (DBG) {
                 Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
             }
-            mHandler.post(() -> stopTrackingInterface(iface));
+            mHandler.post(() -> {
+                if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+                stopTrackingInterface(iface);
+            });
         }
     }
 
@@ -888,6 +898,8 @@
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
         postAndWaitForRunnable(() -> {
             pw.println(getClass().getSimpleName());
+            pw.println("Ethernet State: "
+                    + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
             pw.println("Ethernet interface name filter: " + mIfaceMatch);
             pw.println("Default interface: " + mDefaultInterface);
             pw.println("Default interface mode: " + mDefaultInterfaceMode);
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
index 5011dec..3b44d81 100644
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -38,7 +38,7 @@
     private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
     // This is current path but may be changed soon.
     private static final String IFACE_INDEX_NAME_MAP_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_iface_index_name_map";
+            "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
     private final IBpfMap<U32, InterfaceMapValue> mBpfMap;
     private final INetd mNetd;
     private final Handler mHandler;
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index fdfc893..df4e7f5 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -18,6 +18,7 @@
 
 import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
 
+import android.annotation.NonNull;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -38,10 +39,12 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.PerUidCounter;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -52,16 +55,26 @@
  */
 class NetworkStatsObservers {
     private static final String TAG = "NetworkStatsObservers";
+    private static final boolean LOG = true;
     private static final boolean LOGV = false;
 
     private static final int MSG_REGISTER = 1;
     private static final int MSG_UNREGISTER = 2;
     private static final int MSG_UPDATE_STATS = 3;
 
+    private static final int DUMP_USAGE_REQUESTS_COUNT = 200;
+
+    // The maximum number of request allowed per uid before an exception is thrown.
+    @VisibleForTesting
+    static final int MAX_REQUESTS_PER_UID = 100;
+
     // All access to this map must be done from the handler thread.
     // indexed by DataUsageRequest#requestId
     private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
 
+    // Request counters per uid, this is thread safe.
+    private final PerUidCounter mDataUsageRequestsPerUid = new PerUidCounter(MAX_REQUESTS_PER_UID);
+
     // Sequence number of DataUsageRequests
     private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
 
@@ -77,13 +90,16 @@
      *
      * @return the normalized request wrapped within {@link RequestInfo}.
      */
-    public DataUsageRequest register(Context context, DataUsageRequest inputRequest,
-            IUsageCallback callback, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+    public DataUsageRequest register(@NonNull Context context,
+            @NonNull DataUsageRequest inputRequest, @NonNull IUsageCallback callback,
+            int callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
         DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
-        RequestInfo requestInfo = buildRequestInfo(request, callback, callingUid,
-                accessLevel);
+        RequestInfo requestInfo = buildRequestInfo(request, callback, callingPid, callingUid,
+                callingPackage, accessLevel);
+        if (LOG) Log.d(TAG, "Registering observer for " + requestInfo);
+        mDataUsageRequestsPerUid.incrementCountOrThrow(callingUid);
 
-        if (LOGV) Log.v(TAG, "Registering observer for " + request);
         getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
         return request;
     }
@@ -172,7 +188,7 @@
         RequestInfo requestInfo;
         requestInfo = mDataUsageRequests.get(request.requestId);
         if (requestInfo == null) {
-            if (LOGV) Log.v(TAG, "Trying to unregister unknown request " + request);
+            if (LOG) Log.d(TAG, "Trying to unregister unknown request " + request);
             return;
         }
         if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
@@ -180,8 +196,9 @@
             return;
         }
 
-        if (LOGV) Log.v(TAG, "Unregistering " + request);
+        if (LOG) Log.d(TAG, "Unregistering " + requestInfo);
         mDataUsageRequests.remove(request.requestId);
+        mDataUsageRequestsPerUid.decrementCountOrThrow(callingUid);
         requestInfo.unlinkDeathRecipient();
         requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
     }
@@ -214,18 +231,19 @@
     }
 
     private RequestInfo buildRequestInfo(DataUsageRequest request, IUsageCallback callback,
-            int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+            int callingPid, int callingUid, @NonNull String callingPackage,
+            @NetworkStatsAccess.Level int accessLevel) {
         if (accessLevel <= NetworkStatsAccess.Level.USER) {
-            return new UserUsageRequestInfo(this, request, callback, callingUid,
-                    accessLevel);
+            return new UserUsageRequestInfo(this, request, callback, callingPid,
+                    callingUid, callingPackage, accessLevel);
         } else {
             // Safety check in case a new access level is added and we forgot to update this
             if (accessLevel < NetworkStatsAccess.Level.DEVICESUMMARY) {
                 throw new IllegalArgumentException(
                         "accessLevel " + accessLevel + " is less than DEVICESUMMARY.");
             }
-            return new NetworkUsageRequestInfo(this, request, callback, callingUid,
-                    accessLevel);
+            return new NetworkUsageRequestInfo(this, request, callback, callingPid,
+                    callingUid, callingPackage, accessLevel);
         }
     }
 
@@ -237,18 +255,22 @@
         private final NetworkStatsObservers mStatsObserver;
         protected final DataUsageRequest mRequest;
         private final IUsageCallback mCallback;
+        protected final int mCallingPid;
         protected final int mCallingUid;
+        protected final String mCallingPackage;
         protected final @NetworkStatsAccess.Level int mAccessLevel;
         protected NetworkStatsRecorder mRecorder;
         protected NetworkStatsCollection mCollection;
 
         RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
             mStatsObserver = statsObserver;
             mRequest = request;
             mCallback = callback;
+            mCallingPid = callingPid;
             mCallingUid = callingUid;
+            mCallingPackage = callingPackage;
             mAccessLevel = accessLevel;
 
             try {
@@ -269,7 +291,8 @@
 
         @Override
         public String toString() {
-            return "RequestInfo from uid:" + mCallingUid
+            return "RequestInfo from pid/uid:" + mCallingPid + "/" + mCallingUid
+                    + "(" + mCallingPackage + ")"
                     + " for " + mRequest + " accessLevel:" + mAccessLevel;
         }
 
@@ -338,9 +361,10 @@
 
     private static class NetworkUsageRequestInfo extends RequestInfo {
         NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
-            super(statsObserver, request, callback, callingUid, accessLevel);
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid, callingPackage,
+                    accessLevel);
         }
 
         @Override
@@ -380,9 +404,10 @@
 
     private static class UserUsageRequestInfo extends RequestInfo {
         UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
-                    IUsageCallback callback, int callingUid,
-                    @NetworkStatsAccess.Level int accessLevel) {
-            super(statsObserver, request, callback, callingUid, accessLevel);
+                IUsageCallback callback, int callingPid, int callingUid,
+                @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+            super(statsObserver, request, callback, callingPid, callingUid,
+                    callingPackage, accessLevel);
         }
 
         @Override
@@ -448,4 +473,10 @@
             mCurrentTime = currentTime;
         }
     }
+
+    public void dump(IndentingPrintWriter pw) {
+        for (int i = 0; i < Math.min(mDataUsageRequests.size(), DUMP_USAGE_REQUESTS_COUNT); i++) {
+            pw.println(mDataUsageRequests.valueAt(i));
+        }
+    }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index f62765d..6f070d7 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -455,6 +455,73 @@
         }
     }
 
+    /**
+     * Rewriter that will remove any histories or persisted data points before the
+     * specified cutoff time, only writing data back when modified.
+     */
+    public static class RemoveDataBeforeRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mTemp;
+        private final long mCutoffMills;
+
+        public RemoveDataBeforeRewriter(long bucketDuration, long cutoffMills) {
+            mTemp = new NetworkStatsCollection(bucketDuration);
+            mCutoffMills = cutoffMills;
+        }
+
+        @Override
+        public void reset() {
+            mTemp.reset();
+        }
+
+        @Override
+        public void read(InputStream in) throws IOException {
+            mTemp.read(in);
+            mTemp.clearDirty();
+            mTemp.removeHistoryBefore(mCutoffMills);
+        }
+
+        @Override
+        public boolean shouldWrite() {
+            return mTemp.isDirty();
+        }
+
+        @Override
+        public void write(OutputStream out) throws IOException {
+            mTemp.write(out);
+        }
+    }
+
+    /**
+     * Remove persisted data which contains or is before the cutoff timestamp.
+     */
+    public void removeDataBefore(long cutoffMillis) throws IOException {
+        if (mRotator != null) {
+            try {
+                mRotator.rewriteAll(new RemoveDataBeforeRewriter(
+                        mBucketDuration, cutoffMillis));
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverFromWtf();
+            } catch (OutOfMemoryError e) {
+                Log.wtf(TAG, "problem importing netstats", e);
+                recoverFromWtf();
+            }
+        }
+
+        // Clean up any pending stats
+        if (mPending != null) {
+            mPending.removeHistoryBefore(cutoffMillis);
+        }
+        if (mSinceBoot != null) {
+            mSinceBoot.removeHistoryBefore(cutoffMillis);
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+        if (complete != null) {
+            complete.removeHistoryBefore(cutoffMillis);
+        }
+    }
+
     public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
         if (mPending != null) {
             pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 82b1fb5..a015177 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -159,8 +159,11 @@
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -219,15 +222,15 @@
             "netstats_combine_subtype_enabled";
 
     private static final String UID_COUNTERSET_MAP_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_uid_counterset_map";
+            "/sys/fs/bpf/netd_shared/map_netd_uid_counterset_map";
     private static final String COOKIE_TAG_MAP_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_cookie_tag_map";
+            "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
     private static final String APP_UID_STATS_MAP_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_app_uid_stats_map";
+            "/sys/fs/bpf/netd_shared/map_netd_app_uid_stats_map";
     private static final String STATS_MAP_A_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_stats_map_A";
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_A";
     private static final String STATS_MAP_B_PATH =
-            "/sys/fs/bpf/net_shared/map_netd_stats_map_B";
+            "/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
 
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
@@ -374,9 +377,19 @@
 
     private long mLastStatsSessionPoll;
 
-    /** Map from UID to number of opened sessions */
-    @GuardedBy("mOpenSessionCallsPerUid")
+    private final Object mOpenSessionCallsLock = new Object();
+    /**
+     * Map from UID to number of opened sessions. This is used for rate-limt an app to open
+     * session frequently
+     */
+    @GuardedBy("mOpenSessionCallsLock")
     private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+    /**
+     * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
+     * the caller of open session and it is only for debugging.
+     */
+    @GuardedBy("mOpenSessionCallsLock")
+    private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
 
     private final static int DUMP_STATS_SESSION_COUNT = 20;
 
@@ -407,6 +420,44 @@
                 Clock.systemUTC());
     }
 
+    /**
+     * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of
+     * the caller.
+     */
+    private static class OpenSessionKey {
+        public final int uid;
+        public final String packageName;
+
+        OpenSessionKey(int uid, @NonNull String packageName) {
+            this.uid = uid;
+            this.packageName = packageName;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append("uid=").append(uid).append(",");
+            sb.append("package=").append(packageName);
+            sb.append("}");
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(@NonNull Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+
+            final OpenSessionKey key = (OpenSessionKey) o;
+            return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, packageName);
+        }
+    }
+
     private final class NetworkStatsHandler extends Handler {
         NetworkStatsHandler(@NonNull Looper looper) {
             super(looper);
@@ -794,16 +845,27 @@
         return openSessionInternal(flags, callingPackage);
     }
 
-    private boolean isRateLimitedForPoll(int callingUid) {
-        if (callingUid == android.os.Process.SYSTEM_UID) {
-            return false;
-        }
-
+    private boolean isRateLimitedForPoll(@NonNull OpenSessionKey key) {
         final long lastCallTime;
         final long now = SystemClock.elapsedRealtime();
-        synchronized (mOpenSessionCallsPerUid) {
-            int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
-            mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+
+        synchronized (mOpenSessionCallsLock) {
+            Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key);
+            if (callsPerCaller == null) {
+                mOpenSessionCallsPerCaller.put((key), 1);
+            } else {
+                mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1));
+            }
+
+            int callsPerUid = mOpenSessionCallsPerUid.get(key.uid, 0);
+            mOpenSessionCallsPerUid.put(key.uid, callsPerUid + 1);
+
+            if (key.uid == android.os.Process.SYSTEM_UID) {
+                return false;
+            }
+
+            // To avoid a non-system user to be rate-limited after system users open sessions,
+            // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID.
             lastCallTime = mLastStatsSessionPoll;
             mLastStatsSessionPoll = now;
         }
@@ -811,7 +873,7 @@
         return now - lastCallTime < POLL_RATE_LIMIT_MS;
     }
 
-    private int restrictFlagsForCaller(int flags) {
+    private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
         // All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
         final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -821,14 +883,15 @@
         }
         // Non-system uids are rate limited for POLL_ON_OPEN.
         final int callingUid = Binder.getCallingUid();
-        flags = isRateLimitedForPoll(callingUid)
+        final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage);
+        flags = isRateLimitedForPoll(key)
                 ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
                 : flags;
         return flags;
     }
 
     private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
-        final int restrictedFlags = restrictFlagsForCaller(flags);
+        final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
         if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
                 | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
             final long ident = Binder.clearCallingIdentity();
@@ -1279,13 +1342,14 @@
         Objects.requireNonNull(request.template, "NetworkTemplate is null");
         Objects.requireNonNull(callback, "callback is null");
 
-        int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
         @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
         DataUsageRequest normalizedRequest;
         final long token = Binder.clearCallingIdentity();
         try {
             normalizedRequest = mStatsObservers.register(mContext,
-                    request, callback, callingUid, accessLevel);
+                    request, callback, callingPid, callingUid, callingPackage, accessLevel);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -1937,6 +2001,9 @@
         for (int uid : uids) {
             deleteKernelTagData(uid);
         }
+
+       // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+       // mOpenSessionCallsPerCaller
     }
 
     /**
@@ -2060,25 +2127,21 @@
             pw.decreaseIndent();
 
             // Get the top openSession callers
-            final SparseIntArray calls;
-            synchronized (mOpenSessionCallsPerUid) {
-                calls = mOpenSessionCallsPerUid.clone();
+            final HashMap calls;
+            synchronized (mOpenSessionCallsLock) {
+                calls = new HashMap<>(mOpenSessionCallsPerCaller);
             }
-
-            final int N = calls.size();
-            final long[] values = new long[N];
-            for (int j = 0; j < N; j++) {
-                values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
-            }
-            Arrays.sort(values);
-
-            pw.println("Top openSession callers (uid=count):");
+            final List<Map.Entry<OpenSessionKey, Integer>> list = new ArrayList<>(calls.entrySet());
+            Collections.sort(list,
+                    (left, right) -> Integer.compare(left.getValue(), right.getValue()));
+            final int num = list.size();
+            final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT);
+            pw.println("Top openSession callers:");
             pw.increaseIndent();
-            final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
-            for (int j = N - 1; j >= end; j--) {
-                final int uid = (int) (values[j] & 0xffffffff);
-                final int count = (int) (values[j] >> 32);
-                pw.print(uid); pw.print("="); pw.println(count);
+            for (int j = num - 1; j >= end; j--) {
+                final Map.Entry<OpenSessionKey, Integer> entry = list.get(j);
+                pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+
             }
             pw.decreaseIndent();
             pw.println();
@@ -2098,6 +2161,13 @@
                 }
             });
             pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Stats Observers:");
+            pw.increaseIndent();
+            mStatsObservers.dump(pw);
+            pw.decreaseIndent();
+            pw.println();
 
             pw.println("Dev stats:");
             pw.increaseIndent();
diff --git a/service/Android.bp b/service/Android.bp
index 25b970a..91b9d1c 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -169,7 +169,7 @@
         "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
-        "NetworkStackApiCurrentShims",
+        "NetworkStackApiStableShims",
     ],
     apex_available: [
         "com.android.tethering",
@@ -216,6 +216,11 @@
     apex_available: [
         "com.android.tethering",
     ],
+    optimize: {
+        enabled: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
+    },
     lint: { strict_updatability_linting: true },
 }
 
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index f491cc7..02b2875 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -23,6 +23,7 @@
     name: "ServiceConnectivityResources",
     sdk_version: "module_30",
     min_sdk_version: "30",
+    target_sdk_version: "33",
     resource_dirs: [
         "res",
     ],
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index 4b21569..c7223fc 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -111,5 +111,13 @@
 # From mdns-aidl-interface
 rule android.net.mdns.aidl.** android.net.connectivity.@0
 
+# From nearby-service, including proto
+rule service.proto.** com.android.server.nearby.@0
+rule androidx.annotation.Keep* com.android.server.nearby.@0
+rule androidx.collection.** com.android.server.nearby.@0
+rule androidx.core.** com.android.server.nearby.@0
+rule androidx.versionedparcelable.** com.android.server.nearby.@0
+rule com.google.common.** com.android.server.nearby.@0
+
 # Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
 # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index f13c68d..bc70c93 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -39,148 +39,126 @@
 
 namespace android {
 
-static void native_init(JNIEnv* env, jobject clazz) {
+#define CHECK_LOG(status) \
+  do { \
+    if (!isOk(status)) \
+      ALOGE("%s failed, error code = %d", __func__, status.code()); \
+  } while (0)
+
+static void native_init(JNIEnv* env, jclass clazz) {
   Status status = mTc.start();
-   if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
 }
 
-static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpInsert);
-  if (!isOk(status)) {
-    ALOGE("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
   const uint32_t appUids = static_cast<uint32_t>(abs(uid));
   Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
       TrafficController::IptOp::IptOpDelete);
-  if (!isOk(status)) {
-    ALOGD("%s failed, error code = %d", __func__, status.code());
-  }
+  CHECK_LOG(status);
   return (jint)status.code();
 }
 
-static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
+static jint native_setChildChain(JNIEnv* env, jobject self, jint childChain, jboolean enable) {
   auto chain = static_cast<ChildChain>(childChain);
   int res = mTc.toggleUidOwnerMap(chain, enable);
-  if (res) {
-    ALOGE("%s failed, error code = %d", __func__, res);
-  }
+  if (res) ALOGE("%s failed, error code = %d", __func__, res);
   return (jint)res;
 }
 
-static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
-                                jintArray jUids) {
+static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
+                                   jintArray jUids) {
     const ScopedUtfChars chainNameUtf8(env, name);
-    if (chainNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
-    }
+    if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
     const std::string chainName(chainNameUtf8.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
-    if (res) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
-                          jint firewallRule) {
+static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
+                              jint firewallRule) {
     auto chain = static_cast<ChildChain>(childChain);
     auto rule = static_cast<FirewallRule>(firewallRule);
     FirewallType fType = mTc.getFirewallType(chain);
 
     int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
-    if (res) {
-      ALOGE("%s failed, error code = %d", __func__, res);
-    }
+    if (res) ALOGE("%s failed, error code = %d", __func__, res);
     return (jint)res;
 }
 
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
-                                    jintArray jUids) {
-    const ScopedUtfChars ifNameUtf8(env, ifName);
-    if (ifNameUtf8.c_str() == nullptr) {
-        return -EINVAL;
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
+                                        jintArray jUids) {
+    // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
+    // set to 0.
+    int ifIndex = 0;
+    if (ifName != nullptr) {
+        const ScopedUtfChars ifNameUtf8(env, ifName);
+        const std::string interfaceName(ifNameUtf8.c_str());
+        ifIndex = if_nametoindex(interfaceName.c_str());
     }
-    const std::string interfaceName(ifNameUtf8.c_str());
-    const int ifIndex = if_nametoindex(interfaceName.c_str());
 
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.addUidInterfaceRules(ifIndex, data);
-    if (!isOk(status)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
-    if (uids.get() == nullptr) {
-        return -EINVAL;
-    }
+    if (uids.get() == nullptr) return -EINVAL;
 
     size_t size = uids.size();
     static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
     std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
     Status status = mTc.removeUidInterfaceRules(data);
-    if (!isOk(status)) {
-        ALOGE("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
     Status status = mTc.swapActiveStatsMap();
-    if (!isOk(status)) {
-        ALOGD("%s failed, error code = %d", __func__, status.code());
-    }
+    CHECK_LOG(status);
     return (jint)status.code();
 }
 
-static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
-                                      jintArray jUids) {
+static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
+                                        jintArray jUids) {
     ScopedIntArrayRO uids(env, jUids);
     if (uids.get() == nullptr) return;
 
@@ -190,7 +168,7 @@
     mTc.setPermissionForUids(permission, data);
 }
 
-static void native_dump(JNIEnv* env, jobject clazz, jobject javaFd, jboolean verbose) {
+static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
     int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (fd < 0) {
         jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
@@ -235,9 +213,8 @@
 // clang-format on
 
 int register_com_android_server_BpfNetMaps(JNIEnv* env) {
-    return jniRegisterNativeMethods(env,
-    "com/android/server/BpfNetMaps",
-    gMethods, NELEM(gMethods));
+    return jniRegisterNativeMethods(env, "com/android/server/BpfNetMaps",
+                                    gMethods, NELEM(gMethods));
 }
 
 }; // namespace android
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 3e98edb..a9ede6a 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -88,7 +88,7 @@
         }                                   \
     } while (0)
 
-const std::string uidMatchTypeToString(uint8_t match) {
+const std::string uidMatchTypeToString(uint32_t match) {
     std::string matchType;
     FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
     FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
@@ -98,6 +98,7 @@
     FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
     FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
     FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+    FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
     if (match) {
         return StringPrintf("Unknown match: %u", match);
     }
@@ -183,6 +184,7 @@
     RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
     RETURN_IF_NOT_OK(mUidOwnerMap.clear());
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -272,7 +274,7 @@
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
                 .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+                .rule = oldMatch.value().rule & ~match,
         };
         if (newMatch.rule == 0) {
             RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
@@ -286,23 +288,20 @@
 }
 
 Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
-    // iif should be non-zero if and only if match == MATCH_IIF
-    if (match == IIF_MATCH && iif == 0) {
-        return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
-    } else if (match != IIF_MATCH && iif != 0) {
+    if (match != IIF_MATCH && iif != 0) {
         return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
     }
     auto oldMatch = mUidOwnerMap.readValue(uid);
     if (oldMatch.ok()) {
         UidOwnerValue newMatch = {
-                .iif = iif ? iif : oldMatch.value().iif,
-                .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+                .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
+                .rule = oldMatch.value().rule | match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     } else {
         UidOwnerValue newMatch = {
                 .iif = iif,
-                .rule = static_cast<uint8_t>(match),
+                .rule = match,
         };
         RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
     }
@@ -335,6 +334,8 @@
             return ALLOWLIST;
         case LOW_POWER_STANDBY:
             return ALLOWLIST;
+        case LOCKDOWN:
+            return DENYLIST;
         case NONE:
         default:
             return DENYLIST;
@@ -360,6 +361,9 @@
         case LOW_POWER_STANDBY:
             res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
             break;
+        case LOCKDOWN:
+            res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
+            break;
         case NONE:
         default:
             ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +403,6 @@
 
 Status TrafficController::addUidInterfaceRules(const int iif,
                                                const std::vector<int32_t>& uidsToAdd) {
-    if (!iif) {
-        return statusFromErrno(EINVAL, "Interface rule must specify interface");
-    }
     std::lock_guard guard(mMutex);
 
     for (auto uid : uidsToAdd) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..3f13532 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -307,6 +307,7 @@
     checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
     checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
     checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+    checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
     ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
 }
@@ -491,6 +492,70 @@
     checkEachUidValue({10001, 10002}, IIF_MATCH);
 }
 
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to two uids
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+    // Remove interface rule from one of the uids
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+    checkEachUidValue({1001}, IIF_MATCH);
+
+    // Remove interface rule from the remaining uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+    // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Add interface rule with wildcard to the existing uid
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove interface rule with wildcard from the existing uid
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+    // iif=0 is a wildcard
+    int iif = 0;
+    // Set up existing interface rule with wildcard
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+    // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+    // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
 TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
     std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
 
@@ -620,7 +685,7 @@
                 if (res.ok() || (res.error().code() == ENOENT)) {
                     return Result<void>();
                 }
-                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+                ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
                       strerror(res.error().code()));
             }
             // Move forward to next cookie in the map.
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..847acec 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -35,6 +35,7 @@
     POWERSAVE = 3,
     RESTRICTED = 4,
     LOW_POWER_STANDBY = 5,
+    LOCKDOWN = 6,
     INVALID_CHAIN
 };
 // LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..94397ab
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,18 @@
+# Make sure proguard keeps all connectivity classes
+# TODO: instead of keeping everything, consider listing only "entry points"
+# (service loader, JNI registered methods, etc) and letting the optimizer do its job
+-keep class android.net.** { *; }
+-keep class com.android.connectivity.** { *; }
+-keep class com.android.net.** { *; }
+-keep class !com.android.server.nearby.**,com.android.server.** { *; }
+
+# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
+-keep class com.android.server.nearby.NearbyService { *; }
+-keep class com.android.server.nearby.service.proto { *; }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+# This replicates the base proguard rule used by the build by default
+# (proguard_basic_keeps.flags), but needs to be specified here because the
+# com.google.protobuf package is jarjared to the below package.
+-keepclassmembers class * extends com.android.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 4183ac6..f760d3b 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -610,13 +610,6 @@
     // Handle private DNS validation status updates.
     private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
 
-    /**
-     * used to remove a network request, either a listener or a real request and call unavailable
-     * arg1 = UID of caller
-     * obj  = NetworkRequest
-     */
-    private static final int EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE = 39;
-
      /**
       * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
       * been tested.
@@ -755,7 +748,7 @@
      * The BPF program attached to the tc-police hook to account for to-be-dropped traffic.
      */
     private static final String TC_POLICE_BPF_PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_netd_schedact_ingress_account";
+            "/sys/fs/bpf/netd_shared/prog_netd_schedact_ingress_account";
 
     private static String eventName(int what) {
         return sMagicDecoderRing.get(what, Integer.toString(what));
@@ -1194,6 +1187,7 @@
     /**
      * Keeps track of the number of requests made under different uids.
      */
+    // TODO: Remove the hack and use com.android.net.module.util.PerUidCounter instead.
     public static class PerUidCounter {
         private final int mMaxCountPerUid;
 
@@ -2628,7 +2622,7 @@
         verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
         enforceChangePermission(callingPackageName, callingAttributionTag);
         if (mProtectedNetworks.contains(networkType)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         }
 
         InetAddress addr;
@@ -2982,18 +2976,35 @@
                 android.Manifest.permission.NETWORK_SETTINGS);
     }
 
-    private void enforceConnectivityRestrictedNetworksPermission() {
-        try {
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
-                    "ConnectivityService");
-            return;
-        } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
-        //  TODO: Remove this fallback check after all apps have declared
-        //   CONNECTIVITY_USE_RESTRICTED_NETWORKS.
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CONNECTIVITY_INTERNAL,
-                "ConnectivityService");
+    private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+            boolean checkUidsAllowedList) {
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
+            return true;
+        }
+
+        // fallback to ConnectivityInternalPermission
+        // TODO: Remove this fallback check after all apps have declared
+        //  CONNECTIVITY_USE_RESTRICTED_NETWORKS.
+        if (PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+            return true;
+        }
+
+        // Check whether uid is in allowed on restricted networks list.
+        if (checkUidsAllowedList
+                && mPermissionMonitor.isUidAllowedOnRestrictedNetworks(callingUid)) {
+            return true;
+        }
+        return false;
+    }
+
+    private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
+        final int callingUid = mDeps.getCallingUid();
+        if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+            throw new SecurityException("ConnectivityService: user " + callingUid
+                    + " has no permission to access restricted network.");
+        }
     }
 
     private void enforceKeepalivePermission() {
@@ -3417,6 +3428,10 @@
         for (NetworkAgentInfo nai : networksSortedById()) {
             pw.println(nai.toString());
             pw.increaseIndent();
+            pw.println("Nat464Xlat:");
+            pw.increaseIndent();
+            nai.dumpNat464Xlat(pw);
+            pw.decreaseIndent();
             pw.println(String.format(
                     "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
                     nai.numForegroundNetworkRequests(),
@@ -3432,10 +3447,6 @@
             pw.increaseIndent();
             nai.dumpInactivityTimers(pw);
             pw.decreaseIndent();
-            pw.println("Nat464Xlat:");
-            pw.increaseIndent();
-            nai.dumpNat464Xlat(pw);
-            pw.decreaseIndent();
             pw.decreaseIndent();
         }
     }
@@ -4495,7 +4506,7 @@
 
     private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
             @NonNull final NetworkCapabilities caps) {
-        if (SdkLevel.isAtLeastT() && mCarrierPrivilegeAuthenticator != null) {
+        if (mCarrierPrivilegeAuthenticator != null) {
             return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
                     callingUid, caps);
         }
@@ -4525,7 +4536,6 @@
 
     private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
         ensureRunningOnConnectivityServiceThread();
-        NetworkRequest requestToBeReleased = null;
         for (final NetworkRequestInfo nri : nris) {
             mNetworkRequestInfoLogs.log("REGISTER " + nri);
             checkNrisConsistency(nri);
@@ -4540,13 +4550,6 @@
                         }
                     }
                 }
-                if (req.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                    if (!hasCarrierPrivilegeForNetworkCaps(nri.mUid, req.networkCapabilities)
-                            && !checkConnectivityRestrictedNetworksPermission(
-                                    nri.mPid, nri.mUid)) {
-                        requestToBeReleased = req;
-                    }
-                }
             }
 
             // If this NRI has a satisfier already, it is replacing an older request that
@@ -4558,11 +4561,6 @@
             }
         }
 
-        if (requestToBeReleased != null) {
-            releaseNetworkRequestAndCallOnUnavailable(requestToBeReleased);
-            return;
-        }
-
         if (mFlags.noRematchAllRequestsOnRegister()) {
             rematchNetworksAndRequests(nris);
         } else {
@@ -5402,11 +5400,6 @@
                             /* callOnUnavailable */ false);
                     break;
                 }
-                case EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE: {
-                    handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
-                            /* callOnUnavailable */ true);
-                    break;
-                }
                 case EVENT_SET_ACCEPT_UNVALIDATED: {
                     Network network = (Network) msg.obj;
                     handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
@@ -5976,6 +5969,10 @@
                     + Arrays.toString(ranges) + "): netd command failed: " + e);
         }
 
+        if (SdkLevel.isAtLeastT()) {
+            mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
+        }
+
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
             final boolean curMetered = nai.networkCapabilities.isMetered();
             maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -6625,7 +6622,7 @@
             case REQUEST:
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
                 enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                        callingAttributionTag);
+                        callingAttributionTag, callingUid);
                 // TODO: this is incorrect. We mark the request as metered or not depending on
                 //  the state of the app when the request is filed, but we never change the
                 //  request if the app changes network state. http://b/29964605
@@ -6715,26 +6712,19 @@
     }
 
     private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
-            String callingPackageName, String callingAttributionTag) {
+            String callingPackageName, String callingAttributionTag, final int callingUid) {
         if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
-            if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
-                enforceConnectivityRestrictedNetworksPermission();
+            // For T+ devices, callers with carrier privilege could request with CBS capabilities.
+            if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                    && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
+                return;
             }
+            enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
         } else {
             enforceChangePermission(callingPackageName, callingAttributionTag);
         }
     }
 
-    private boolean checkConnectivityRestrictedNetworksPermission(int callerPid, int callerUid) {
-        if (checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)
-                || checkAnyPermissionOf(callerPid, callerUid,
-                android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
-            return true;
-        }
-        return false;
-    }
-
     @Override
     public boolean requestBandwidthUpdate(Network network) {
         enforceAccessPermission();
@@ -6793,7 +6783,7 @@
         final int callingUid = mDeps.getCallingUid();
         networkCapabilities = new NetworkCapabilities(networkCapabilities);
         enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
-                callingAttributionTag);
+                callingAttributionTag, callingUid);
         enforceMeteredApnPolicy(networkCapabilities);
         ensureRequestableCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
@@ -6916,13 +6906,6 @@
                 EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
     }
 
-    private void releaseNetworkRequestAndCallOnUnavailable(NetworkRequest networkRequest) {
-        ensureNetworkRequestHasType(networkRequest);
-        mHandler.sendMessage(mHandler.obtainMessage(
-                EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE, mDeps.getCallingUid(), 0,
-                networkRequest));
-    }
-
     private void handleRegisterNetworkProvider(NetworkProviderInfo npi) {
         if (mNetworkProviderInfos.containsKey(npi.messenger)) {
             // Avoid creating duplicates. even if an app makes a direct AIDL call.
@@ -7753,10 +7736,10 @@
 
     private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
             NetworkAgentInfo nai) {
-        final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
-        final String newIface = newLp != null ? newLp.getInterfaceName() : null;
-        final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
-        final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+        final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
+        final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
+        final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
+        final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
 
         if (!wasFiltering && !needsFiltering) {
             // Nothing to do.
@@ -7769,11 +7752,19 @@
         }
 
         final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+        if (ranges == null || ranges.isEmpty()) {
+            return;
+        }
+
         final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
         // TODO: this create a window of opportunity for apps to receive traffic between the time
         // when the old rules are removed and the time when new rules are added. To fix this,
         // make eBPF support two allowlisted interfaces so here new rules can be added before the
         // old rules are being removed.
+
+        // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
+        // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
+        // by overriding the Lockdown blocking rule.
         if (wasFiltering) {
             mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
         }
@@ -7834,6 +7825,7 @@
         }
         nai.declaredCapabilities = new NetworkCapabilities(nc);
         NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(nc, nai.creatorUid,
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE),
                 mCarrierPrivilegeAuthenticator);
     }
 
@@ -8062,15 +8054,14 @@
     }
 
     /**
-     * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
-     * network.
+     * Returns the interface which requires VPN isolation (ingress interface filtering).
      *
      * Ingress interface filtering enforces that all apps under the given network can only receive
      * packets from the network's interface (and loopback). This is important for VPNs because
      * apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
      * non-VPN interfaces.
      *
-     * As a result, this method should return true iff
+     * As a result, this method should return Non-null interface iff
      *  1. the network is an app VPN (not legacy VPN)
      *  2. the VPN does not allow bypass
      *  3. the VPN is fully-routed
@@ -8079,15 +8070,32 @@
      * @see INetd#firewallAddUidInterfaceRules
      * @see INetd#firewallRemoveUidInterfaceRules
      */
-    private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+    @Nullable
+    private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
             LinkProperties lp) {
-        if (nc == null || lp == null) return false;
-        return nai.isVPN()
+        if (nc == null || lp == null) return null;
+        if (nai.isVPN()
                 && !nai.networkAgentConfig.allowBypass
                 && nc.getOwnerUid() != Process.SYSTEM_UID
                 && lp.getInterfaceName() != null
                 && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
-                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
+                && !lp.hasExcludeRoute()) {
+            return lp.getInterfaceName();
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether we need to set interface filtering rule or not
+     */
+    private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+            String filterIface) {
+        // Only filter if lp has an interface.
+        if (lp == null || lp.getInterfaceName() == null) return false;
+        // Before T, allow rules are only needed if VPN isolation is enabled.
+        // T and After T, allow rules are needed for all VPNs.
+        return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8215,9 +8223,10 @@
             if (!prevRanges.isEmpty()) {
                 updateVpnUidRanges(false, nai, prevRanges);
             }
-            final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
-            final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
-            final String iface = nai.linkProperties.getInterfaceName();
+            final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
+            final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
+            final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
+            final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
             // For VPN uid interface filtering, old ranges need to be removed before new ranges can
             // be added, due to the range being expanded and stored as individual UIDs. For example
             // the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8229,11 +8238,16 @@
             // above, where the addition of new ranges happens before the removal of old ranges.
             // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
             // to be removed will never overlap with the new range to be added.
+
+            // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
+            // receive packets on all interfaces. This is required to accept incoming traffic in
+            // Lockdown mode by overriding the Lockdown blocking rule.
             if (wasFiltering && !prevRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+                        prevNc.getOwnerUid());
             }
             if (shouldFilter && !newRanges.isEmpty()) {
-                mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+                mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
             }
         } catch (Exception e) {
             // Never crash!
@@ -10624,7 +10638,11 @@
         if (callback == null) throw new IllegalArgumentException("callback must be non-null");
 
         if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
-            enforceConnectivityRestrictedNetworksPermission();
+            // TODO: Check allowed list here and ensure that either a) any QoS callback registered
+            //  on this network is unregistered when the app loses permission or b) no QoS
+            //  callbacks are sent for restricted networks unless the app currently has permission
+            //  to access restricted networks.
+            enforceConnectivityRestrictedNetworksPermission(false /* checkUidsAllowedList */);
         }
         mQosCallbackTracker.registerCallback(callback, filter, nai);
     }
@@ -10676,7 +10694,10 @@
         Objects.requireNonNull(profile);
 
         if (preferences.size() == 0) {
-            preferences.add((new ProfileNetworkPreference.Builder()).build());
+            final ProfileNetworkPreference pref = new ProfileNetworkPreference.Builder()
+                    .setPreference(ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    .build();
+            preferences.add(pref);
         }
 
         PermissionUtils.enforceNetworkStackPermission(mContext);
@@ -10694,12 +10715,14 @@
 
         final List<ProfileNetworkPreferenceList.Preference> preferenceList =
                 new ArrayList<ProfileNetworkPreferenceList.Preference>();
-        boolean allowFallback = true;
+        boolean hasDefaultPreference = false;
         for (final ProfileNetworkPreference preference : preferences) {
             final NetworkCapabilities nc;
+            boolean allowFallback = true;
             switch (preference.getPreference()) {
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
                     nc = null;
+                    hasDefaultPreference = true;
                     if (preference.getPreferenceEnterpriseId() != 0) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10709,6 +10732,14 @@
                     allowFallback = false;
                     // continue to process the enterprise preference.
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                    // This code is needed even though there is a check later on,
+                    // because isRangeAlreadyInPreferenceList assumes that every preference
+                    // has a UID list.
+                    if (hasDefaultPreference) {
+                        throw new IllegalArgumentException(
+                                "Default profile preference should not be set along with other "
+                                        + "preference");
+                    }
                     if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
                         throw new IllegalArgumentException(
                                 "Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10732,6 +10763,10 @@
             }
             preferenceList.add(new ProfileNetworkPreferenceList.Preference(
                     profile, nc, allowFallback));
+            if (hasDefaultPreference && preferenceList.size() > 1) {
+                throw new IllegalArgumentException(
+                        "Default profile preference should not be set along with other preference");
+            }
         }
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
                 new Pair<>(preferenceList, listener)));
@@ -10776,12 +10811,6 @@
         return false;
     }
 
-    private void validateNetworkCapabilitiesOfProfileNetworkPreference(
-            @Nullable final NetworkCapabilities nc) {
-        if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
-        ensureRequestableCapabilities(nc);
-    }
-
     private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
             @NonNull final ProfileNetworkPreferenceList prefs) {
         final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
@@ -10832,10 +10861,19 @@
     private void handleSetProfileNetworkPreference(
             @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
             @Nullable final IOnCompleteListener listener) {
+        /*
+         * handleSetProfileNetworkPreference is always called for single user.
+         * preferenceList only contains preferences for different uids within the same user
+         * (enforced by getUidListToBeAppliedForNetworkPreference).
+         * Clear all the existing preferences for the user before applying new preferences.
+         *
+         */
+        mProfileNetworkPreferences = mProfileNetworkPreferences.withoutUser(
+                preferenceList.get(0).user);
         for (final ProfileNetworkPreferenceList.Preference preference : preferenceList) {
-            validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
             mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
         }
+
         removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_PROFILE);
         addPerAppDefaultNetworkRequests(
                 createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences));
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index e8fc06d..738caab 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -540,7 +540,7 @@
                 mClatCoordinator.dump(pw);
                 pw.decreaseIndent();
             } else {
-                pw.println("<not start>");
+                pw.println("<not started>");
             }
         }
     }
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 323888a..b40b6e0 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -19,6 +19,7 @@
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.transportNamesOf;
 
@@ -1224,20 +1225,22 @@
      *
      * @param nc the capabilities to sanitize
      * @param creatorUid the UID of the process creating this network agent
+     * @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
      * @param authenticator the carrier privilege authenticator to check for telephony constraints
      */
     public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
-            final int creatorUid, @NonNull final CarrierPrivilegeAuthenticator authenticator) {
+            final int creatorUid, final boolean hasAutomotiveFeature,
+            @Nullable final CarrierPrivilegeAuthenticator authenticator) {
         if (nc.hasTransport(TRANSPORT_TEST)) {
             nc.restrictCapabilitiesForTestNetwork(creatorUid);
         }
-        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, authenticator)) {
+        if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
             nc.setAllowedUids(new ArraySet<>());
         }
     }
 
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
-            @NonNull final NetworkCapabilities nc,
+            @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
         // NCs without access UIDs are fine.
         if (!nc.hasAllowedUids()) return true;
@@ -1252,6 +1255,11 @@
         // access UIDs
         if (nc.hasTransport(TRANSPORT_TEST)) return true;
 
+        // Factories that make ethernet networks can allow UIDs for automotive devices.
+        if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
+            return true;
+        }
+
         // Factories that make cell networks can allow the UID for the carrier service package.
         // This can only work in T where there is support for CarrierPrivilegeAuthenticator
         if (null != carrierPrivilegeAuthenticator
@@ -1262,8 +1270,6 @@
             return true;
         }
 
-        // TODO : accept Railway callers
-
         return false;
     }
 
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index c02d9cf..e4a2c20 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -23,6 +23,9 @@
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -37,6 +40,7 @@
 import static com.android.net.module.util.CollectionUtils.toIntArray;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -74,7 +78,6 @@
 import com.android.server.BpfNetMaps;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -108,10 +111,19 @@
     @GuardedBy("this")
     private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
 
-    // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
-    // for apps under the VPN
+    // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+    // ranges for apps under the VPNs which enable interface filtering.
+    // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+    // enable interface filtering.
     @GuardedBy("this")
-    private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+    private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+    // Items are uid ranges for apps under the VPN Lockdown
+    // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+    // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+    // So we need to maintain uid range with multiset.
+    @GuardedBy("this")
+    private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
 
     // A set of appIds for apps across all users on the device. We track appIds instead of uids
     // directly to reduce its size and also eliminate the need to update this set when user is
@@ -201,6 +213,38 @@
         }
     }
 
+    private static class MultiSet<T> {
+        private final Map<T, Integer> mMap = new ArrayMap<>();
+
+        /**
+         * Returns the number of key in the set before this addition.
+         */
+        public int add(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            mMap.put(key, oldCount + 1);
+            return oldCount;
+        }
+
+        /**
+         * Return the number of key in the set before this removal.
+         */
+        public int remove(T key) {
+            final int oldCount = mMap.getOrDefault(key, 0);
+            if (oldCount == 0) {
+                Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+            } else if (oldCount == 1) {
+                mMap.remove(key);
+            } else {
+                mMap.put(key, oldCount - 1);
+            }
+            return oldCount;
+        }
+
+        public Set<T> getSet() {
+            return mMap.keySet();
+        }
+    }
+
     public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
             @NonNull final BpfNetMaps bpfNetMaps) {
         this(context, netd, bpfNetMaps, new Dependencies());
@@ -421,7 +465,14 @@
         if (appInfo == null) return false;
         // Check whether package's uid is in allowed on restricted networks uid list. If so, this
         // uid can have netd system permission.
-        return mUidsAllowedOnRestrictedNetworks.contains(appInfo.uid);
+        return isUidAllowedOnRestrictedNetworks(appInfo.uid);
+    }
+
+    /**
+     * Returns whether the given uid is in allowed on restricted networks list.
+     */
+    public synchronized boolean isUidAllowedOnRestrictedNetworks(final int uid) {
+        return mUidsAllowedOnRestrictedNetworks.contains(uid);
     }
 
     @VisibleForTesting
@@ -619,16 +670,26 @@
     }
 
     private synchronized void updateVpnUid(int uid, boolean add) {
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             if (UidRange.containsUid(vpn.getValue(), uid)) {
                 final Set<Integer> changedUids = new HashSet<>();
                 changedUids.add(uid);
-                removeBypassingUids(changedUids, -1 /* vpnAppUid */);
                 updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
             }
         }
     }
 
+    private synchronized void updateLockdownUid(int uid, boolean add) {
+        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
+                && !hasRestrictedNetworksPermission(uid)) {
+            updateLockdownUidRule(uid, add);
+        }
+    }
+
     /**
      * This handles both network and traffic permission, because there is no overlap in actual
      * values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -722,9 +783,10 @@
 
         // If the newly-installed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen after the mUidToNetworkPerm update above, since
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, true /* add */);
+        updateLockdownUid(uid, true /* add */);
         mAllApps.add(appId);
 
         // Log package added.
@@ -768,9 +830,10 @@
 
         // If the newly-removed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen before the mUidToNetworkPerm update below, since
-        // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
-        // package can bypass VPN.
+        // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+        // mUidToNetworkPerm to check if the package can bypass VPN.
         updateVpnUid(uid, false /* add */);
+        updateLockdownUid(uid, false /* add */);
         // If the package has been removed from all users on the device, clear it form mAllApps.
         if (mPackageManager.getNameForUid(uid) == null) {
             mAllApps.remove(appId);
@@ -852,48 +915,100 @@
     /**
      * Called when a new set of UID ranges are added to an active VPN network
      *
-     * @param iface The active VPN network's interface name
+     * @param iface The active VPN network's interface name. Null iface indicates that the app is
+     *              allowed to receive packets on all interfaces.
      * @param rangesToAdd The new UID ranges to be added to the network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+    public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
             int vpnAppUid) {
         // Calculate the list of new app uids under the VPN due to the new UID ranges and update
         // Netd about them. Because mAllApps only contains appIds instead of uids, the result might
         // be an overestimation if an app is not installed on the user on which the VPN is running,
-        // but that's safe.
+        // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+        // packets to that UID is fine.
         final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
-        if (mVpnUidRanges.containsKey(iface)) {
-            mVpnUidRanges.get(iface).addAll(rangesToAdd);
+        if (mVpnInterfaceUidRanges.containsKey(iface)) {
+            mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
         } else {
-            mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+            mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
         }
     }
 
     /**
      * Called when a set of UID ranges are removed from an active VPN network
      *
-     * @param iface The VPN network's interface name
+     * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+     *              to receive packets on all interfaces.
      * @param rangesToRemove Existing UID ranges to be removed from the VPN network
      * @param vpnAppUid The uid of the VPN app
      */
-    public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+    public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
             Set<UidRange> rangesToRemove, int vpnAppUid) {
         // Calculate the list of app uids that are no longer under the VPN due to the removed UID
         // ranges and update Netd about them.
         final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
         removeBypassingUids(changedUids, vpnAppUid);
         updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
-        Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+        Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
         if (existingRanges == null) {
             loge("Attempt to remove unknown vpn uid Range iface = " + iface);
             return;
         }
         existingRanges.removeAll(rangesToRemove);
         if (existingRanges.size() == 0) {
-            mVpnUidRanges.remove(iface);
+            mVpnInterfaceUidRanges.remove(iface);
+        }
+    }
+
+    /**
+     * Called when UID ranges under VPN Lockdown are updated
+     *
+     * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+     *        are to be removed from the Lockdown.
+     * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+     *               app's UID in any special way. The caller is responsible for excluding the VPN
+     *               app UID from the passed-in ranges.
+     *               Ranges can have duplications and/or contain the range that is already subject
+     *               to lockdown. However, ranges can not have overlaps with other ranges including
+     *               ranges that are currently subject to lockdown.
+     */
+    public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+        final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+        for (final UidRange range : ranges) {
+            if (add) {
+                // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+                // currently.
+                if (mVpnLockdownUidRanges.add(range) == 0) {
+                    affectedUidRanges.add(range);
+                }
+            } else {
+                // Rule will be removed if the number of the range in the set is 1 before the
+                // removal.
+                if (mVpnLockdownUidRanges.remove(range) == 1) {
+                    affectedUidRanges.add(range);
+                }
+            }
+        }
+
+        // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+        // apps that are installed only on some users but not others. But that's safe: if an app is
+        // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+        final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+        // We skip adding rule to privileged apps and allow them to bypass incoming packet
+        // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+        // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+        // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+        // filtering, privileged apps can still bypass outgoing packet filtering because the
+        // prohibit rules observe the protected from VPN bit.
+        for (final int uid: affectedUids) {
+            if (!hasRestrictedNetworksPermission(uid)) {
+                updateLockdownUidRule(uid, add);
+            }
         }
     }
 
@@ -932,7 +1047,7 @@
      */
     private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
         uids.remove(vpnAppUid);
-        uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+        uids.removeIf(this::hasRestrictedNetworksPermission);
     }
 
     /**
@@ -941,6 +1056,7 @@
      *
      * This is to instruct netd to set up appropriate filtering rules for these uids, such that they
      * can only receive ingress packets from the VPN's tunnel interface (and loopback).
+     * Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
      *
      * @param iface the interface name of the active VPN connection
      * @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -961,6 +1077,18 @@
         }
     }
 
+    private void updateLockdownUidRule(int uid, boolean add) {
+        try {
+            if (add) {
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
+            } else {
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
+            }
+        } catch (ServiceSpecificException e) {
+            loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
+        }
+    }
+
     /**
      * Send the updated permission information to netd. Called upon package install/uninstall.
      *
@@ -1048,8 +1176,14 @@
 
     /** Should only be used by unit tests */
     @VisibleForTesting
-    public Set<UidRange> getVpnUidRanges(String iface) {
-        return mVpnUidRanges.get(iface);
+    public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+        return mVpnInterfaceUidRanges.get(iface);
+    }
+
+    /** Should only be used by unit tests */
+    @VisibleForTesting
+    public Set<UidRange> getVpnLockdownUidRanges() {
+        return mVpnLockdownUidRanges.getSet();
     }
 
     private synchronized void onSettingChanged() {
@@ -1114,7 +1248,7 @@
     public void dump(IndentingPrintWriter pw) {
         pw.println("Interface filtering rules:");
         pw.increaseIndent();
-        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
             pw.println("Interface: " + vpn.getKey());
             pw.println("UIDs: " + vpn.getValue().toString());
             pw.println();
@@ -1122,6 +1256,14 @@
         pw.decreaseIndent();
 
         pw.println();
+        pw.println("Lockdown filtering rules:");
+        pw.increaseIndent();
+        for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
+            pw.println("UIDs: " + range.toString());
+        }
+        pw.decreaseIndent();
+
+        pw.println();
         pw.println("Update logs:");
         pw.increaseIndent();
         mPermissionUpdateLogs.reverseDump(pw);
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
index 71f342d..5bafef9 100644
--- a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -70,23 +70,33 @@
     /**
      * Returns a new object consisting of this object plus the passed preference.
      *
-     * If a preference already exists for the same user, it will be replaced by the passed
-     * preference. Passing a Preference object containing a null capabilities object is equivalent
-     * to (and indeed, implemented as) removing the preference for this user.
+     * It is not expected that unwanted preference already exists for the same user.
+     * All preferences for the user that were previously configured should be cleared before
+     * adding a new preference.
+     * Passing a Preference object containing a null capabilities object is equivalent
+     * to removing the preference for this user.
      */
     public ProfileNetworkPreferenceList plus(@NonNull final Preference pref) {
-        final ArrayList<Preference> newPrefs = new ArrayList<>();
-        for (final Preference existingPref : preferences) {
-            if (!existingPref.user.equals(pref.user)) {
-                newPrefs.add(existingPref);
-            }
-        }
+        final ArrayList<Preference> newPrefs = new ArrayList<>(preferences);
         if (null != pref.capabilities) {
             newPrefs.add(pref);
         }
         return new ProfileNetworkPreferenceList(newPrefs);
     }
 
+    /**
+     * Remove all preferences corresponding to a user.
+     */
+    public ProfileNetworkPreferenceList withoutUser(UserHandle user) {
+        final ArrayList<Preference> newPrefs = new ArrayList<>();
+        for (final Preference existingPref : preferences) {
+            if (!existingPref.user.equals(user)) {
+                newPrefs.add(existingPref);
+            }
+        }
+        return new ProfileNetworkPreferenceList(newPrefs);
+    }
+
     public boolean isEmpty() {
         return preferences.isEmpty();
     }
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
index 534dbe7..e682026 100644
--- a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -30,6 +30,8 @@
 import android.telephony.data.NrQosSessionAttributes;
 import android.util.Log;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Objects;
 
 /**
@@ -149,6 +151,7 @@
 
     void sendEventEpsQosSessionAvailable(final QosSession session,
             final EpsBearerQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
             mCallback.onQosEpsBearerSessionAvailable(session, attributes);
@@ -159,6 +162,7 @@
 
     void sendEventNrQosSessionAvailable(final QosSession session,
             final NrQosSessionAttributes attributes) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventNrQosSessionAvailable: sending...");
             mCallback.onNrQosSessionAvailable(session, attributes);
@@ -168,6 +172,7 @@
     }
 
     void sendEventQosSessionLost(@NonNull final QosSession session) {
+        if (!validateOrSendErrorAndUnregister()) return;
         try {
             if (DBG) log("sendEventQosSessionLost: sending...");
             mCallback.onQosSessionLost(session);
@@ -185,6 +190,21 @@
         }
     }
 
+    private boolean validateOrSendErrorAndUnregister() {
+        final int exceptionType = mFilter.validate();
+        if (exceptionType != EX_TYPE_FILTER_NONE) {
+             log("validation fail before sending QosCallback.");
+             // Error callback is returned from Android T to prevent any disruption of application
+             // running on Android S.
+             if (SdkLevel.isAtLeastT()) {
+                sendEventQosCallbackError(exceptionType);
+                mQosCallbackTracker.unregisterCallback(mCallback);
+            }
+            return false;
+        }
+        return true;
+    }
+
     private static void log(@NonNull final String msg) {
         Log.d(TAG, msg);
     }
diff --git a/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
new file mode 100644
index 0000000..84b6e54
--- /dev/null
+++ b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class EthernetNetworkManagementExceptionTest {
+    private static final String ERROR_MESSAGE = "Test error message";
+
+    @Test
+    public void testEthernetNetworkManagementExceptionParcelable() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertParcelingIsLossless(e);
+    }
+
+    @Test
+    public void testEthernetNetworkManagementExceptionHasExpectedErrorMessage() {
+        final EthernetNetworkManagementException e =
+                new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+        assertEquals(ERROR_MESSAGE, e.getMessage());
+    }
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 8fc636a..b66a979 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -1261,6 +1261,18 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testHasExcludeRoute() {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 24), RTN_UNICAST));
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_UNICAST));
+        assertFalse(lp.hasExcludeRoute());
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 32), RTN_THROW));
+        assertTrue(lp.hasExcludeRoute());
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
diff --git a/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
index ca0e5ed..368a519 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
@@ -16,7 +16,7 @@
 
 package android.net.netstats
 
-import android.net.NetworkIdentitySet
+import android.net.NetworkIdentity
 import android.net.NetworkStatsCollection
 import android.net.NetworkStatsHistory
 import androidx.test.filters.SmallTest
@@ -40,7 +40,7 @@
 
     @Test
     fun testBuilder() {
-        val ident = NetworkIdentitySet()
+        val ident = setOf<NetworkIdentity>()
         val key1 = NetworkStatsCollection.Key(ident, /* uid */ 0, /* set */ 0, /* tag */ 0)
         val key2 = NetworkStatsCollection.Key(ident, /* uid */ 1, /* set */ 0, /* tag */ 0)
         val bucketDuration = 10L
@@ -63,4 +63,4 @@
         val actualHistory = actualEntries[key1] ?: fail("There should be an entry for $key1")
         assertEquals(history1.entries, actualHistory.entries)
     }
-}
\ No newline at end of file
+}
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
new file mode 100644
index 0000000..192694b
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats
+
+import android.net.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_BLUETOOTH
+import android.net.NetworkTemplate.MATCH_CARRIER
+import android.net.NetworkTemplate.MATCH_ETHERNET
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_PROXY
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.telephony.TelephonyManager
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+private const val TEST_IMSI1 = "imsi"
+private const val TEST_WIFI_KEY1 = "wifiKey1"
+private const val TEST_WIFI_KEY2 = "wifiKey2"
+
+@RunWith(JUnit4::class)
+@ConnectivityModuleTest
+class NetworkTemplateTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    @Test
+    fun testBuilderMatchRules() {
+        // Verify unknown match rules cannot construct templates.
+        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify hidden match rules cannot construct templates.
+        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(it).build()
+            }
+        }
+
+        // Verify template which matches metered cellular and carrier networks with
+        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify template which matches roaming cellular and carrier networks with
+        // the given IMSI.
+        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+                    .setRoaming(ROAMING_YES).setMeteredness(METERED_YES).build().let {
+                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                                ROAMING_YES, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                        assertEquals(expectedTemplate, it)
+                    }
+        }
+
+        // Verify carrier template cannot be created without IMSI.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_CARRIER).build()
+        }
+
+        // Verify template which matches metered cellular networks,
+        // regardless of IMSI. See buildTemplateMobileWildcard.
+        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches metered cellular networks and ratType.
+        // See NetworkTemplate#buildTemplateMobileWithRatType.
+        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
+                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+                .build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches all wifi networks,
+        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
+        NetworkTemplate.Builder(MATCH_WIFI).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches wifi networks with the given Wifi Network Key.
+        // See buildTemplateWifi(wifiNetworkKey).
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
+                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches all wifi networks with the
+        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
+        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
+                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
+                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
+                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                    assertEquals(expectedTemplate, it)
+                }
+
+        // Verify template which matches ethernet and bluetooth networks.
+        // See buildTemplateEthernet and buildTemplateBluetooth.
+        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+            NetworkTemplate.Builder(matchRule).build().let {
+                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
+                        null /*subscriberIds*/, arrayOf<String>(),
+                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+                assertEquals(expectedTemplate, it)
+            }
+        }
+    }
+
+    @Test
+    fun testBuilderWifiNetworkKeys() {
+        // Verify template builder which generates same template with the given different
+        // sequence keys.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
+            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
+            assertEquals(expectedTemplate, it)
+        }
+
+        // Verify template which matches non-wifi networks with the given key is invalid.
+        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
+                Integer.MAX_VALUE).forEach { matchRule ->
+            assertFailsWith<IllegalArgumentException> {
+                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+            }
+        }
+
+        // Verify template which matches wifi networks with the given null key is invalid.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
+        }
+
+        // Verify template which matches wifi wildcard with the given empty key set.
+        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
+            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
+                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+            assertEquals(expectedTemplate, it)
+        }
+    }
+}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 47ea53e..ac84e57 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -26,6 +26,7 @@
         "tradefed",
     ],
     static_libs: [
+        "CompatChangeGatingTestBase",
         "modules-utils-build-testing",
     ],
     // Tag this module as a cts test artifact
@@ -37,6 +38,8 @@
     data: [
         ":CtsHostsideNetworkTestsApp",
         ":CtsHostsideNetworkTestsApp2",
+        ":CtsHostsideNetworkTestsApp3",
+        ":CtsHostsideNetworkTestsApp3PreT",
         ":CtsHostsideNetworkTestsAppNext",
     ],
     per_testcase_directory: true,
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 524bd65..93e9dcd 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -217,7 +217,10 @@
             Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
                     + attempts + " attempts; sleeping "
                     + SLEEP_TIME_SEC + " seconds before trying again");
-            SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (attempts <= maxAttempts) {
+                SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            }
         } while (attempts <= maxAttempts);
         assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
                 + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
@@ -328,7 +331,10 @@
             }
             Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on background state after "
                 + maxTries + " attempts: " + state);
@@ -347,7 +353,10 @@
             Log.d(TAG, "App not on foreground state on attempt #" + i
                     + "; sleeping 1s before trying again");
             turnScreenOn();
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground state after "
                 + maxTries + " attempts: " + state);
@@ -365,7 +374,10 @@
             }
             Log.d(TAG, "App not on foreground service state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("App2 (" + mUid + ") is not on foreground service state after "
                 + maxTries + " attempts: " + state);
@@ -506,7 +518,10 @@
             Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
                     + checker.getExpected() + "' on attempt #" + i
                     + "; sleeping " + napTimeSeconds + "s before trying again");
-            SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+            }
         }
         fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
                 + maxTries
@@ -578,7 +593,10 @@
             }
             Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
                     + expected + ", got " + actual + "); sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
                 + ". Full list: " + uids);
@@ -738,7 +756,8 @@
 
     protected void assertAppIdle(boolean enabled) throws Exception {
         try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
+                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
         } catch (Throwable e) {
             throw e;
         }
@@ -765,7 +784,10 @@
                 return;
             }
             Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            SystemClock.sleep(SECOND_IN_MS);
+            // No sleep after the last turn
+            if (i < maxTries) {
+                SystemClock.sleep(SECOND_IN_MS);
+            }
         }
         fail("app2 receiver is not ready in " + mUid);
     }
@@ -814,8 +836,6 @@
             return;
         } else if (type == TYPE_COMPONENT_ACTIVTIY) {
             turnScreenOn();
-            // Wait for screen-on state to propagate through the system.
-            SystemClock.sleep(2000);
             final CountDownLatch latch = new CountDownLatch(1);
             final Intent launchIntent = getIntentForComponent(type);
             final Bundle extras = new Bundle();
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 01c8cd2..edfaf9f 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -23,6 +23,7 @@
     defaults: ["cts_support_defaults"],
     sdk_version: "test_current",
     static_libs: [
+        "androidx.annotation_annotation",
         "CtsHostsideNetworkTestsAidl",
         "NetworkStackApiStableShims",
     ],
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 08cdea7..82f13ae 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -29,6 +29,9 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.annotation.GuardedBy;
 
 import com.android.cts.net.hostside.INetworkStateObserver;
 
@@ -37,18 +40,24 @@
  */
 public class MyActivity extends Activity {
 
+    @GuardedBy("this")
     private BroadcastReceiver finishCommandReceiver = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "MyActivity.onCreate()");
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
     }
 
     @Override
     public void finish() {
-        if (finishCommandReceiver != null) {
-            unregisterReceiver(finishCommandReceiver);
+        synchronized (this) {
+            if (finishCommandReceiver != null) {
+                unregisterReceiver(finishCommandReceiver);
+                finishCommandReceiver = null;
+            }
         }
         super.finish();
     }
@@ -71,14 +80,17 @@
         super.onResume();
         Log.d(TAG, "MyActivity.onResume(): " + getIntent());
         Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
-        finishCommandReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.d(TAG, "Finishing MyActivity");
-                MyActivity.this.finish();
-            }
-        };
-        registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+        synchronized (this) {
+            finishCommandReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Log.d(TAG, "Finishing MyActivity");
+                    MyActivity.this.finish();
+                }
+            };
+            registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
+                    Context.RECEIVER_EXPORTED);
+        }
     }
 
     @Override
diff --git a/tests/cts/hostside/app3/Android.bp b/tests/cts/hostside/app3/Android.bp
new file mode 100644
index 0000000..141cf03
--- /dev/null
+++ b/tests/cts/hostside/app3/Android.bp
@@ -0,0 +1,54 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsHostsideNetworkTestsApp3Defaults",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "junit",
+    ],
+    static_libs: [
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3PreT",
+    target_sdk_version: "31",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
diff --git a/tests/cts/hostside/app3/AndroidManifest.xml b/tests/cts/hostside/app3/AndroidManifest.xml
new file mode 100644
index 0000000..eabcacb
--- /dev/null
+++ b/tests/cts/hostside/app3/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.net.hostside.app3">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.net.hostside.app3" />
+
+</manifest>
diff --git a/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
new file mode 100644
index 0000000..a1a8209
--- /dev/null
+++ b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside.app3;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify {@link LinkProperties#getRoutes} behavior, depending on
+ * {@LinkProperties#EXCLUDED_ROUTES} change state.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExcludedRoutesGatingTest {
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testExcludedRoutesChangeEnabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is enabled: non-RTN_UNICAST routes are visible.
+        assertEquals(2, lp.getRoutes().size());
+        assertEquals(2, lp.getAllRoutes().size());
+    }
+
+    @Test
+    public void testExcludedRoutesChangeDisabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is disabled: non-RTN_UNICAST routes are filtered out.
+        assertEquals(0, lp.getRoutes().size());
+        assertEquals(0, lp.getAllRoutes().size());
+    }
+
+    private LinkProperties makeLinkPropertiesWithExcludedRoutes() {
+        final LinkProperties lp = new LinkProperties();
+
+        lp.addRoute(new RouteInfo(new IpPrefix("10.0.0.0/8"), null, null, RouteInfo.RTN_THROW));
+        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null,
+                RouteInfo.RTN_UNREACHABLE));
+
+        return lp;
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
new file mode 100644
index 0000000..b65fb6b
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import java.util.Set;
+
+/**
+ * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ */
+public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
+    private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
+    private static final String TEST_APK_PRE_T = "CtsHostsideNetworkTestsApp3PreT.apk";
+    private static final String TEST_PKG = "com.android.cts.net.hostside.app3";
+    private static final String TEST_CLASS = ".ExcludedRoutesGatingTest";
+
+    private static final long EXCLUDED_ROUTES_CHANGE_ID = 186082280;
+
+    protected void tearDown() throws Exception {
+        uninstallPackage(TEST_PKG, true);
+    }
+
+    public void testExcludedRoutesChangeEnabled() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTest("testExcludedRoutesChangeEnabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledPreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTest("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledByOverride() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeEnabledByOverridePreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTestWithChangeEnabled("testExcludedRoutesChangeEnabled");
+    }
+
+    private void runDeviceCompatTest(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(), Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeEnabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(EXCLUDED_ROUTES_CHANGE_ID),
+                Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeDisabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(),
+                Set.of(EXCLUDED_ROUTES_CHANGE_ID));
+    }
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 3b47100..6b5bb93 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -44,7 +44,8 @@
              android.permission.MANAGE_TEST_NETWORKS
     -->
 
-    <application android:usesCleartextTraffic="true">
+    <application android:debuggable="true"
+                 android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
     </application>
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 48a1c79..33f3af5 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -38,4 +38,20 @@
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
     </test>
+    <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+        one of the Mainline modules below is present on the device used for testing. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <!-- Tethering Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+        <!-- Tethering Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.tethering" />
+        <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+        <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.android.networkstack" />
+        <!-- Resolver Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.resolv" />
+        <!-- Resolver Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.resolv" />
+    </object>
 </configuration>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 5b37294..9b81a56 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -51,5 +51,8 @@
         "cts",
         "general-tests",
     ],
-
+    data: [
+        ":CtsNetTestAppForApi23",
+    ],
+    per_testcase_directory: true,
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index a129108..3b88189 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -52,6 +52,7 @@
 import static android.net.ConnectivityManager.TYPE_PROXY;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -164,7 +165,6 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.InstrumentationRegistry;
@@ -182,6 +182,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.TestHttpServer;
@@ -1605,51 +1606,7 @@
 
     private static boolean isTcpKeepaliveSupportedByKernel() {
         final String kVersionString = VintfRuntimeInfo.getKernelVersion();
-        return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
-    }
-
-    private static Pair<Integer, Integer> getVersionFromString(String version) {
-        // Only gets major and minor number of the version string.
-        final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
-        final Matcher m = versionPattern.matcher(version);
-        if (m.matches()) {
-            final int major = Integer.parseInt(m.group(1));
-            final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
-            return new Pair<>(major, minor);
-        } else {
-            return new Pair<>(0, 0);
-        }
-    }
-
-    // TODO: Move to util class.
-    private static int compareMajorMinorVersion(final String s1, final String s2) {
-        final Pair<Integer, Integer> v1 = getVersionFromString(s1);
-        final Pair<Integer, Integer> v2 = getVersionFromString(s2);
-
-        if (v1.first == v2.first) {
-            return Integer.compare(v1.second, v2.second);
-        } else {
-            return Integer.compare(v1.first, v2.first);
-        }
-    }
-
-    /**
-     * Verifies that version string compare logic returns expected result for various cases.
-     * Note that only major and minor number are compared.
-     */
-    @Test
-    public void testMajorMinorVersionCompare() {
-        assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
-        assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
-        assertEquals(1, compareMajorMinorVersion("5", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("5", "5.0"));
-        assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
-        assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
-        assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
-        assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.8") >= 0;
     }
 
     /**
@@ -3212,7 +3169,7 @@
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test
     public void testUidsAllowedOnRestrictedNetworks() throws Exception {
-        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTestSApis();
 
         // TODO (b/175199465): figure out a reasonable permission check for
         //  setUidsAllowedOnRestrictedNetworks that allows tests but not system-external callers.
@@ -3225,10 +3182,10 @@
         // because it has been just installed to device. In case the uid is existed in setting
         // mistakenly, try to remove the uid and set correct uids to setting.
         originalUidsAllowedOnRestrictedNetworks.remove(uid);
-        runWithShellPermissionIdentity(() ->
-                ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                        mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+        runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
 
+        // File a restricted network request with permission first to hold the connection.
         final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
@@ -3240,6 +3197,19 @@
         runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
 
+        // File another restricted network request without permission.
+        final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
+        final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+                        TEST_RESTRICTED_NW_IFACE_NAME))
+                .build();
+        // Uid is not in allowed list and no permissions. Expect that SecurityException will throw.
+        assertThrows(SecurityException.class,
+                () -> mCm.requestNetwork(restrictedRequest, restrictedNetworkCb));
+
         final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
         final Network network = agent.getNetwork();
 
@@ -3259,19 +3229,28 @@
             final Set<Integer> newUidsAllowedOnRestrictedNetworks =
                     new ArraySet<>(originalUidsAllowedOnRestrictedNetworks);
             newUidsAllowedOnRestrictedNetworks.add(uid);
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
             // Wait a while for sending allowed uids on the restricted network to netd.
-            // TODD: Have a significant signal to know the uids has been send to netd.
+            // TODD: Have a significant signal to know the uids has been sent to netd.
             assertBindSocketToNetworkSuccess(network);
+
+            if (TestUtils.shouldTestTApis()) {
+                // Uid is in allowed list. Try file network request again.
+                requestNetwork(restrictedRequest, restrictedNetworkCb);
+                // Verify that the network is restricted.
+                restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+                        NETWORK_CALLBACK_TIMEOUT_MS,
+                        entry -> network.equals(entry.getNetwork())
+                                && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+            }
         } finally {
             agent.unregister();
 
             // Restore setting.
-            runWithShellPermissionIdentity(() ->
-                    ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
-                            mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+                    mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
         }
     }
 
@@ -3295,6 +3274,12 @@
         assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
     }
 
+    private void assumeTestSApis() {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 1e42fe6..bbac09b 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -626,15 +626,31 @@
     @Test
     fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
                 (agent, callback) ->
+        val policyId = 1
+        val dscpValue = 1
+        val range = Range(4444, 4444)
+        val srcPort = 555
+
         // Check that policy with partial parameters is lossless.
-        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+        val policy = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range).build()
+        assertEquals(policyId, policy.policyId)
+        assertEquals(dscpValue, policy.dscpValue)
+        assertEquals(range, policy.destinationPortRange)
         assertParcelingIsLossless(policy)
 
         // Check that policy with all parameters is lossless.
-        val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+        val policy2 = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range)
                 .setSourceAddress(LOCAL_IPV4_ADDRESS)
                 .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourcePort(srcPort)
                 .setProtocol(IPPROTO_UDP).build()
+        assertEquals(policyId, policy2.policyId)
+        assertEquals(dscpValue, policy2.dscpValue)
+        assertEquals(range, policy2.destinationPortRange)
+        assertEquals(TEST_TARGET_IPV4_ADDR, policy2.destinationAddress)
+        assertEquals(LOCAL_IPV4_ADDRESS, policy2.sourceAddress)
+        assertEquals(srcPort, policy2.sourcePort)
+        assertEquals(IPPROTO_UDP, policy2.protocol)
         assertParcelingIsLossless(policy2)
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 04434e5..0a02593 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -15,54 +15,71 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.EthernetNetworkSpecifier
 import android.net.InetAddresses
 import android.net.IpConfiguration
 import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
-import android.platform.test.annotations.AppModeFull
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
-import com.android.net.module.util.ArrayTrackRecord
-import com.android.net.module.util.TrackRecord
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
-import com.android.testutils.runAsShell
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import android.content.Context
-import org.junit.runner.RunWith
-import kotlin.test.assertNull
-import kotlin.test.fail
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
 import android.os.Handler
 import android.os.HandlerExecutor
 import android.os.Looper
+import android.platform.test.annotations.AppModeFull
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.TrackRecord
+import com.android.networkstack.apishim.EthernetManagerShimImpl
 import com.android.networkstack.apishim.common.EthernetManagerShim.InterfaceStateListener
+import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
+import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
 import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_ABSENT
 import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_DOWN
 import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_UP
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
-import com.android.networkstack.apishim.EthernetManagerShimImpl
+import com.android.testutils.anyNetwork
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.SC_V2
 import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
 import com.android.testutils.waitForIdle
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 import java.net.Inet6Address
-import java.util.concurrent.Executor
-import kotlin.test.assertFalse
 import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import java.net.NetworkInterface
+import kotlin.test.fail
 
 private const val TIMEOUT_MS = 1000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
     IpConfiguration.ProxySettings.NONE, null, null)
+private val ETH_REQUEST: NetworkRequest = NetworkRequest.Builder()
+    .addTransportType(TRANSPORT_TEST)
+    .addTransportType(TRANSPORT_ETHERNET)
+    .removeCapability(NET_CAPABILITY_TRUSTED)
+    .build()
 
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
 @RunWith(AndroidJUnit4::class)
@@ -73,9 +90,12 @@
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val em by lazy { EthernetManagerShimImpl.newInstance(context) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
 
+    private val ifaceListener = EthernetStateListener()
     private val createdIfaces = ArrayList<EthernetTestInterface>()
     private val addedListeners = ArrayList<EthernetStateListener>()
+    private val networkRequests = ArrayList<TestableNetworkCallback>()
 
     private class EthernetTestInterface(
         context: Context,
@@ -91,7 +111,7 @@
                 val tnm = context.getSystemService(TestNetworkManager::class.java)
                 tnm.createTapInterface(false /* bringUp */)
             }
-            val mtu = NetworkInterface.getByName(tapInterface.interfaceName).getMTU()
+            val mtu = 1500
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
             raResponder = RouterAdvertisementResponder(packetReader)
             raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
@@ -141,14 +161,23 @@
         }
 
         fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
-            expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
-                if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
+            expectCallback(createChangeEvent(iface, state, role))
         }
 
+        fun createChangeEvent(iface: EthernetTestInterface, state: Int, role: Int) =
+                InterfaceStateChanged(iface.interfaceName, state, role,
+                        if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
+
         fun pollForNextCallback(): CallbackEntry {
             return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
         }
 
+        fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
+
+        fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
+            assertNotNull(eventuallyExpect(createChangeEvent(iface, state, role)))
+        }
+
         fun assertNoCallback() {
             val cb = events.poll(NO_CALLBACK_TIMEOUT_MS)
             assertNull(cb, "Expected no callback but got $cb")
@@ -158,6 +187,7 @@
     @Before
     fun setUp() {
         setIncludeTestInterfaces(true)
+        addInterfaceStateListener(ifaceListener)
     }
 
     @After
@@ -165,22 +195,32 @@
         setIncludeTestInterfaces(false)
         for (iface in createdIfaces) {
             iface.destroy()
+            ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
         }
         for (listener in addedListeners) {
             em.removeInterfaceStateListener(listener)
         }
+        networkRequests.forEach { cm.unregisterNetworkCallback(it) }
     }
 
-    private fun addInterfaceStateListener(executor: Executor, listener: EthernetStateListener) {
-        em.addInterfaceStateListener(executor, listener)
+    private fun addInterfaceStateListener(listener: EthernetStateListener) {
+        runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+            em.addInterfaceStateListener(HandlerExecutor(Handler(Looper.getMainLooper())), listener)
+        }
         addedListeners.add(listener)
     }
 
     private fun createInterface(): EthernetTestInterface {
-        return EthernetTestInterface(
+        val iface = EthernetTestInterface(
             context,
             Handler(Looper.getMainLooper())
         ).also { createdIfaces.add(it) }
+        with(ifaceListener) {
+            // when an interface comes up, we should always see a down cb before an up cb.
+            eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+            expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+        }
+        return iface
     }
 
     private fun setIncludeTestInterfaces(value: Boolean) {
@@ -192,49 +232,97 @@
     private fun removeInterface(iface: EthernetTestInterface) {
         iface.destroy()
         createdIfaces.remove(iface)
+        ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
     }
 
-    @Test
-    public fun testCallbacks() {
-        val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+    private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.requestNetwork(request, it)
+            networkRequests.add(it)
+        }
+    }
 
+    private fun releaseNetwork(cb: TestableNetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        networkRequests.remove(cb)
+    }
+
+    private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
+        NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
+            .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
+
+    // It can take multiple seconds for the network to become available.
+    private fun TestableNetworkCallback.expectAvailable() =
+        expectCallback<Available>(anyNetwork(), 5000/*ms timeout*/).network
+
+    // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
+    // eventuallyExpect(Lost::class) instead.
+    private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
+        eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
+
+    private fun TestableNetworkCallback.assertNotLost(n: Network? = null) =
+        assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
+
+    @Test
+    fun testCallbacks() {
         // If an interface exists when the callback is registered, it is reported on registration.
         val iface = createInterface()
-        val listener = EthernetStateListener()
-        addInterfaceStateListener(executor, listener)
-        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+        val listener1 = EthernetStateListener()
+        addInterfaceStateListener(listener1)
+        validateListenerOnRegistration(listener1)
 
         // If an interface appears, existing callbacks see it.
         // TODO: fix the up/up/down/up callbacks and only send down/up.
         val iface2 = createInterface()
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
-        listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+        listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
 
         // Register a new listener, it should see state of all existing interfaces immediately.
         val listener2 = EthernetStateListener()
-        addInterfaceStateListener(executor, listener2)
-        listener2.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
-        listener2.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+        addInterfaceStateListener(listener2)
+        validateListenerOnRegistration(listener2)
 
         // Removing interfaces first sends link down, then STATE_ABSENT/ROLE_NONE.
         removeInterface(iface)
-        for (listener in addedListeners) {
+        for (listener in listOf(listener1, listener2)) {
             listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
             listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
         }
 
         removeInterface(iface2)
-        for (listener in addedListeners) {
+        for (listener in listOf(listener1, listener2)) {
             listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
             listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
             listener.assertNoCallback()
         }
     }
 
+    /**
+     * Validate all interfaces are returned for an EthernetStateListener upon registration.
+     */
+    private fun validateListenerOnRegistration(listener: EthernetStateListener) {
+        // Get all tracked interfaces to validate on listener registration. Ordering and interface
+        // state (up/down) can't be validated for interfaces not created as part of testing.
+        val ifaces = em.getInterfaceList()
+        val polledIfaces = ArraySet<String>()
+        for (i in ifaces) {
+            val event = (listener.pollForNextCallback() as InterfaceStateChanged)
+            val iface = event.iface
+            assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
+            assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
+            // If the event's iface was created in the test, additional criteria can be validated.
+            createdIfaces.find { it.interfaceName.equals(iface) }?.let {
+                assertEquals(event, listener.createChangeEvent(it, STATE_LINK_UP, ROLE_CLIENT))
+            }
+        }
+        // Assert all callbacks are accounted for.
+        listener.assertNoCallback()
+    }
+
     @Test
-    public fun testGetInterfaceList() {
+    fun testGetInterfaceList() {
         setIncludeTestInterfaces(true)
 
         // Create two test interfaces and check the return list contains the interface names.
@@ -254,4 +342,105 @@
 
         removeInterface(iface2)
     }
+
+    @Test
+    fun testNetworkRequest_withSingleExistingInterface() {
+        setIncludeTestInterfaces(true)
+        createInterface()
+
+        // install a listener which will later be used to verify the Lost callback
+        val listenerCb = TestableNetworkCallback()
+        cm.registerNetworkCallback(ETH_REQUEST, listenerCb)
+        networkRequests.add(listenerCb)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        cb.assertNotLost()
+        releaseNetwork(cb)
+        listenerCb.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_beforeSingleInterfaceIsUp() {
+        setIncludeTestInterfaces(true)
+
+        val cb = requestNetwork(ETH_REQUEST)
+
+        // bring up interface after network has been requested
+        val iface = createInterface()
+        val network = cb.expectAvailable()
+
+        // remove interface before network request has been removed
+        cb.assertNotLost()
+        removeInterface(iface)
+        cb.eventuallyExpectLost()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfaces() {
+        setIncludeTestInterfaces(true)
+
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+
+        val network = cb.expectAvailable()
+        cb.expectCapabilitiesThat(network) {
+            it.networkSpecifier == EthernetNetworkSpecifier(iface2.interfaceName)
+        }
+
+        removeInterface(iface1)
+        cb.assertNotLost()
+        removeInterface(iface2)
+        cb.eventuallyExpectLost()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withInterfaceBeingReplaced() {
+        setIncludeTestInterfaces(true)
+        val iface1 = createInterface()
+
+        val cb = requestNetwork(ETH_REQUEST)
+        val network = cb.expectAvailable()
+
+        // create another network and verify the request sticks to the current network
+        val iface2 = createInterface()
+        cb.assertNotLost()
+
+        // remove iface1 and verify the request brings up iface2
+        removeInterface(iface1)
+        cb.eventuallyExpectLost(network)
+        val network2 = cb.expectAvailable()
+
+        releaseNetwork(cb)
+    }
+
+    @Test
+    fun testNetworkRequest_withMultipleInterfacesAndRequests() {
+        setIncludeTestInterfaces(true)
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+
+        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.interfaceName))
+        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb3 = requestNetwork(ETH_REQUEST)
+
+        cb1.expectAvailable()
+        cb2.expectAvailable()
+        cb3.expectAvailable()
+
+        cb1.assertNotLost()
+        cb2.assertNotLost()
+        cb3.assertNotLost()
+
+        releaseNetwork(cb1)
+        releaseNetwork(cb2)
+        releaseNetwork(cb3)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 7286bf6..2b1d173 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -22,6 +22,7 @@
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
 
@@ -60,12 +61,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
 import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.apishim.common.VpnManagerShim;
 import com.android.networkstack.apishim.common.VpnProfileStateShim;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -228,22 +224,17 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
             boolean requiresValidation) throws Exception {
 
-        builderShim.setBypassable(true)
+        builder.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
         if (TestUtils.shouldTestTApis()) {
-            builderShim.setRequiresInternetValidation(requiresValidation);
+            builder.setRequiresInternetValidation(requiresValidation);
         }
-
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and does not defined in shims.
-        // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -259,14 +250,13 @@
                         ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
                         IkeSessionTestUtils.CHILD_PARAMS);
 
-        final Ikev2VpnProfileBuilderShim builderShim =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, params)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(params)
                         .setRequiresInternetValidation(requiresValidation)
                         .setProxy(TEST_PROXY_INFO)
                         .setMaxMtu(TEST_MTU)
                         .setMetered(false);
 
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -275,9 +265,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
             boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
-                        .setAuthPsk(TEST_PSK);
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 requiresValidation);
     }
@@ -285,8 +274,8 @@
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */);
@@ -294,8 +283,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -328,15 +317,8 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
-        if (TestUtils.shouldTestTApis()) {
-            assertEquals(requiresValidation, shim.isInternetValidationRequired());
-        } else {
-            try {
-                shim.isInternetValidationRequired();
-                fail("Only supported from API level 33");
-            } catch (UnsupportedApiLevelException expected) {
-            }
+        if (isAtLeastT()) {
+            assertEquals(requiresValidation, profile.isInternetValidationRequired());
         }
     }
 
@@ -348,8 +330,8 @@
 
         final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
                 IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
-        final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+        final Ikev2VpnProfile.Builder ikeProfileBuilder =
+                new Ikev2VpnProfile.Builder(expectedParams);
         // Verify the other Ike options could not be set with IkeTunnelConnectionParams.
         final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
         assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
@@ -358,10 +340,9 @@
         assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
 
-        final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+        final Ikev2VpnProfile profile = ikeProfileBuilder.build();
 
-        assertEquals(expectedParams,
-                new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+        assertEquals(expectedParams, profile.getIkeTunnelConnectionParams());
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fb720a7..f86c5cd 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -29,8 +29,20 @@
 import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
 import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
 import static android.app.usage.NetworkStats.Bucket.UID_ALL;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.AppOpsManager;
+import android.app.Instrumentation;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -40,7 +52,11 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkRequest;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
 import android.net.TrafficStats;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -48,11 +64,23 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
 import android.telephony.TelephonyManager;
-import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -62,8 +90,18 @@
 import java.net.UnknownHostException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
-public class NetworkStatsManagerTest extends InstrumentationTestCase {
+@ConnectivityModuleTest
+@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
+
     private static final String LOG_TAG = "NetworkStatsManagerTest";
     private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
     private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
@@ -164,9 +202,11 @@
             };
 
     private String mPkg;
+    private Context mContext;
     private NetworkStatsManager mNsm;
     private ConnectivityManager mCm;
     private PackageManager mPm;
+    private Instrumentation mInstrumentation;
     private long mStartTime;
     private long mEndTime;
 
@@ -224,44 +264,40 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mNsm = (NetworkStatsManager) getInstrumentation().getContext()
-                .getSystemService(Context.NETWORK_STATS_SERVICE);
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mNsm = mContext.getSystemService(NetworkStatsManager.class);
         mNsm.setPollForce(true);
 
-        mCm = (ConnectivityManager) getInstrumentation().getContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mPm = mContext.getPackageManager();
+        mPkg = mContext.getPackageName();
 
-        mPm = getInstrumentation().getContext().getPackageManager();
-
-        mPkg = getInstrumentation().getContext().getPackageName();
-
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS);
         setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow");
         mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mWriteSettingsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode);
         }
         if (mUsageStatsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode);
         }
-        super.tearDown();
     }
 
     private void setAppOpsMode(String appop, String mode) throws Exception {
         final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
-        SystemUtil.runShellCommand(command);
+        SystemUtil.runShellCommand(mInstrumentation, command);
     }
 
     private String getAppOpsMode(String appop) throws Exception {
         final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
-        String result = SystemUtil.runShellCommand(command);
+        String result = SystemUtil.runShellCommand(mInstrumentation, command);
         if (result == null) {
             Log.w(LOG_TAG, "App op " + appop + " could not be read.");
         }
@@ -269,7 +305,7 @@
     }
 
     private boolean isInForeground() throws IOException {
-        String result = SystemUtil.runShellCommand(getInstrumentation(),
+        String result = SystemUtil.runShellCommand(mInstrumentation,
                 "cmd activity get-uid-state " + Process.myUid());
         return result.contains("FOREGROUND");
     }
@@ -366,15 +402,14 @@
     private String getSubscriberId(int networkIndex) {
         int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType();
         if (ConnectivityManager.TYPE_MOBILE == networkType) {
-            TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext()
-                    .getSystemService(Context.TELEPHONY_SERVICE);
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
             return ShellIdentityUtils.invokeMethodWithShellPermissions(tm,
                     (telephonyManager) -> telephonyManager.getSubscriberId());
         }
         return "";
     }
 
-    @AppModeFull
+    @Test
     public void testDeviceSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -410,7 +445,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUserSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -446,7 +481,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Use tolerance value that large enough to make sure stats of at
@@ -522,7 +557,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -565,7 +600,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUidDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -619,7 +654,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testTagDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -726,7 +761,7 @@
                 bucket.getRxBytes(), bucket.getTxBytes()));
     }
 
-    @AppModeFull
+    @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -803,7 +838,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testCallback() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -825,6 +860,43 @@
             // storing files of >2MB in CTS.
 
             mNsm.unregisterUsageCallback(usageCallback);
+
+            // For T- devices, the registerUsageCallback invocation below will need a looper
+            // from the thread that calls into the API, which is not available in the test.
+            if (SdkLevel.isAtLeastT()) {
+                mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(),
+                        getSubscriberId(i), THRESHOLD_BYTES, usageCallback);
+                mNsm.unregisterUsageCallback(usageCallback);
+            }
+        }
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testDataMigrationUtils() throws Exception {
+        final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
+        for (final String prefix : prefixes) {
+            final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)
+                    : TimeUnit.HOURS.toMillis(2);
+
+            final NetworkStatsCollection collection =
+                    NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, duration);
+
+            final long now = System.currentTimeMillis();
+            final Set<Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory>> entries =
+                    collection.getEntries().entrySet();
+            for (final Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory> entry : entries) {
+                for (final NetworkStatsHistory.Entry historyEntry : entry.getValue().getEntries()) {
+                    // Verify all value fields are reasonable.
+                    assertTrue(historyEntry.getBucketStart() <= now);
+                    assertTrue(historyEntry.getActiveTime() <= duration);
+                    assertTrue(historyEntry.getRxBytes() >= 0);
+                    assertTrue(historyEntry.getRxPackets() >= 0);
+                    assertTrue(historyEntry.getTxBytes() >= 0);
+                    assertTrue(historyEntry.getTxPackets() >= 0);
+                    assertTrue(historyEntry.getOperations() >= 0);
+                }
+            }
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index b139a9b..33a0a83 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -52,10 +52,7 @@
 import androidx.test.runner.AndroidJUnit4
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
-import com.android.networkstack.apishim.ConstantsShim
 import com.android.networkstack.apishim.NsdShimImpl
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
@@ -65,7 +62,6 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.ServerSocket
@@ -82,6 +78,7 @@
 private const val TAG = "NsdManagerTest"
 private const val SERVICE_TYPE = "_nmt._tcp"
 private const val TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
 private const val DBG = false
 
 private val nsdShim = NsdShimImpl.newInstance()
@@ -89,10 +86,6 @@
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(AndroidJUnit4::class)
 class NsdManagerTest {
-    // NsdManager is not updatable before S, so tests do not need to be backwards compatible
-    @get:Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
-
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
 
@@ -136,6 +129,11 @@
                     nextEvent.javaClass.simpleName)
             return nextEvent
         }
+
+        inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
+            val cb = nextEvents.poll(timeoutMs)
+            assertNull(cb, "Expected no callback but got $cb")
+        }
     }
 
     private class NsdRegistrationRecord : RegistrationListener,
@@ -249,9 +247,11 @@
     fun setUp() {
         handlerThread.start()
 
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1 = createTestNetwork()
-            testNetwork2 = createTestNetwork()
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1 = createTestNetwork()
+                testNetwork2 = createTestNetwork()
+            }
         }
     }
 
@@ -290,9 +290,11 @@
 
     @After
     fun tearDown() {
-        runAsShell(MANAGE_TEST_NETWORKS) {
-            testNetwork1.close(cm)
-            testNetwork2.close(cm)
+        if (TestUtils.shouldTestTApis()) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1.close(cm)
+                testNetwork2.close(cm)
+            }
         }
         handlerThread.quitSafely()
     }
@@ -393,14 +395,17 @@
         si2.serviceName = serviceName
         si2.port = localPort
         val registrationRecord2 = NsdRegistrationRecord()
-        val registeredInfo2 = registerService(registrationRecord2, si2)
+        nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
+        val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>().serviceInfo
 
         // Expect a service record to be discovered (and filter the ones
         // that are unrelated to this test)
         val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
 
         // Resolve the service
-        val resolvedService2 = resolveService(foundInfo2)
+        val resolveRecord2 = NsdResolveRecord()
+        nsdManager.resolveService(foundInfo2, resolveRecord2)
+        val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
 
         // Check that the resolved service doesn't have any TXT records
         assertEquals(0, resolvedService2.attributes.size)
@@ -416,7 +421,7 @@
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
         // This test requires shims supporting T+ APIs (discovering on specific network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -450,7 +455,7 @@
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
         // This test requires shims supporting T+ APIs (discovering on network request)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -515,7 +520,7 @@
     @Test
     fun testNsdManager_ResolveOnNetwork() {
         // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
-        assumeTrue(ConstantsShim.VERSION > SC_V2)
+        assumeTrue(TestUtils.shouldTestTApis())
 
         val si = NsdServiceInfo()
         si.serviceType = SERVICE_TYPE
@@ -556,6 +561,90 @@
         }
     }
 
+    @Test
+    fun testNsdManager_RegisterOnNetwork() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.network = testNetwork1.network
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+        val discoveryRecord = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        val discoveryRecord3 = NsdDiscoveryRecord()
+
+        tryTest {
+            // Discover service on testNetwork1.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, Executor { it.run() }, discoveryRecord)
+            // Expect that service is found on testNetwork1
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(
+                serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+
+            // Discover service on testNetwork2.
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork2.network, Executor { it.run() }, discoveryRecord2)
+            // Expect that discovery is started then no other callbacks.
+            discoveryRecord2.expectCallback<DiscoveryStarted>()
+            discoveryRecord2.assertNoCallback()
+
+            // Discover service on all networks (not specify any network).
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
+            // Expect that service is found on testNetwork1
+            val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
+                    serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
+        val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
+        val si = NsdServiceInfo().apply {
+            serviceType = SERVICE_TYPE
+            serviceName = serviceNames
+            port = 12345 // Test won't try to connect so port does not matter
+        }
+
+        // Register the service name which contains non-standard characters.
+        val registrationRecord = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>()
+
+        tryTest {
+            // Discover that service name.
+            val discoveryRecord = NsdDiscoveryRecord()
+            nsdManager.discoverServices(
+                SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
+            )
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+
+            // Expect that resolving the service name works properly even service name contains
+            // non-standard characters.
+            val resolveRecord = NsdResolveRecord()
+            nsdManager.resolveService(foundInfo, resolveRecord)
+            val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
+            assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+        } cleanup {
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     /**
      * Register a service and return its registration record.
      */
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 25694d7..db39e6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -42,7 +42,9 @@
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
+#define PRIVATE "/sys/fs/bpf/net_private/"
 #define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD "/sys/fs/bpf/netd_shared/"
 
 class BpfExistenceTest : public ::testing::Test {
 };
@@ -95,32 +97,35 @@
     SHARED "map_dscp_policy_ipv6_socket_to_policies_map_A",
     SHARED "map_dscp_policy_ipv6_socket_to_policies_map_B",
     SHARED "map_dscp_policy_switch_comp_map",
-    SHARED "map_netd_app_uid_stats_map",
-    SHARED "map_netd_configuration_map",
-    SHARED "map_netd_cookie_tag_map",
-    SHARED "map_netd_iface_index_name_map",
-    SHARED "map_netd_iface_stats_map",
-    SHARED "map_netd_stats_map_A",
-    SHARED "map_netd_stats_map_B",
-    SHARED "map_netd_uid_counterset_map",
-    SHARED "map_netd_uid_owner_map",
-    SHARED "map_netd_uid_permission_map",
-    SHARED "prog_block_bind4_block_port",
-    SHARED "prog_block_bind6_block_port",
+    NETD "map_netd_app_uid_stats_map",
+    NETD "map_netd_configuration_map",
+    NETD "map_netd_cookie_tag_map",
+    NETD "map_netd_iface_index_name_map",
+    NETD "map_netd_iface_stats_map",
+    NETD "map_netd_stats_map_A",
+    NETD "map_netd_stats_map_B",
+    NETD "map_netd_uid_counterset_map",
+    NETD "map_netd_uid_owner_map",
+    NETD "map_netd_uid_permission_map",
     SHARED "prog_clatd_schedcls_egress4_clat_ether",
     SHARED "prog_clatd_schedcls_egress4_clat_rawip",
     SHARED "prog_clatd_schedcls_ingress6_clat_ether",
     SHARED "prog_clatd_schedcls_ingress6_clat_rawip",
+    NETD "prog_netd_cgroupskb_egress_stats",
+    NETD "prog_netd_cgroupskb_ingress_stats",
+    NETD "prog_netd_cgroupsock_inet_create",
+    NETD "prog_netd_schedact_ingress_account",
+    NETD "prog_netd_skfilter_allowlist_xtbpf",
+    NETD "prog_netd_skfilter_denylist_xtbpf",
+    NETD "prog_netd_skfilter_egress_xtbpf",
+    NETD "prog_netd_skfilter_ingress_xtbpf",
+};
+
+static const set<string> INTRODUCED_T_5_4 = {
+    SHARED "prog_block_bind4_block_port",
+    SHARED "prog_block_bind6_block_port",
     SHARED "prog_dscp_policy_schedcls_set_dscp_ether",
     SHARED "prog_dscp_policy_schedcls_set_dscp_raw_ip",
-    SHARED "prog_netd_cgroupskb_egress_stats",
-    SHARED "prog_netd_cgroupskb_ingress_stats",
-    SHARED "prog_netd_cgroupsock_inet_create",
-    SHARED "prog_netd_schedact_ingress_account",
-    SHARED "prog_netd_skfilter_allowlist_xtbpf",
-    SHARED "prog_netd_skfilter_denylist_xtbpf",
-    SHARED "prog_netd_skfilter_egress_xtbpf",
-    SHARED "prog_netd_skfilter_ingress_xtbpf",
 };
 
 static const set<string> REMOVED_T = {
@@ -162,6 +167,7 @@
 
     if (IsAtLeastT()) {
         addAll(expected, INTRODUCED_T);
+        if (android::bpf::isAtLeastKernelVersion(5, 4, 0)) addAll(expected, INTRODUCED_T_5_4);
         removeAll(expected, REMOVED_T);
 
         addAll(unexpected, REMOVED_T);
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index f324630..c327868 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -41,6 +41,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -72,6 +73,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -82,6 +84,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.lang.ref.WeakReference;
+
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
@@ -461,4 +465,49 @@
         }
         fail("expected exception of type " + throwableType);
     }
+
+    private static class MockContext extends BroadcastInterceptingContext {
+        MockContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return mock(Context.class);
+        }
+    }
+
+    private WeakReference<Context> makeConnectivityManagerAndReturnContext() {
+        // Mockito may have an internal reference to the mock, creating MockContext for testing.
+        final Context c = new MockContext(mock(Context.class));
+
+        new ConnectivityManager(c, mService);
+
+        return new WeakReference<>(c);
+    }
+
+    private void forceGC() {
+        // First GC ensures that objects are collected for finalization, then second GC ensures
+        // they're garbage-collected after being finalized.
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    @Test
+    public void testConnectivityManagerDoesNotLeakContext() throws Exception {
+        final WeakReference<Context> ref = makeConnectivityManagerAndReturnContext();
+
+        final int attempts = 100;
+        final long waitIntervalMs = 50;
+        for (int i = 0; i < attempts; i++) {
+            forceGC();
+            if (ref.get() == null) break;
+
+            Thread.sleep(waitIntervalMs);
+        }
+
+        assertNull("ConnectivityManager weak reference still not null after " + attempts
+                    + " attempts", ref.get());
+    }
 }
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 0f02850..b518a61 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -37,12 +37,15 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
+import android.annotation.NonNull;
 import android.content.res.Resources;
+import android.net.NetworkStatsCollection.Key;
 import android.os.Process;
 import android.os.UserHandle;
 import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.RecurrenceRule;
 
 import androidx.test.InstrumentationRegistry;
@@ -73,6 +76,8 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Tests for {@link NetworkStatsCollection}.
@@ -531,6 +536,86 @@
         assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
     }
 
+    private static void assertCollectionEntries(
+            @NonNull Map<Key, NetworkStatsHistory> expectedEntries,
+            @NonNull NetworkStatsCollection collection) {
+        final Map<Key, NetworkStatsHistory> actualEntries = collection.getEntries();
+        assertEquals(expectedEntries.size(), actualEntries.size());
+        for (Key expectedKey : expectedEntries.keySet()) {
+            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+            assertNotNull(actualHistory);
+            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+            actualEntries.remove(expectedKey);
+        }
+        assertEquals(0, actualEntries.size());
+    }
+
+    @Test
+    public void testRemoveHistoryBefore() {
+        final NetworkIdentity testIdent = new NetworkIdentity.Builder()
+                .setSubscriberId(TEST_IMSI).build();
+        final Key key1 = new Key(Set.of(testIdent), 0, 0, 0);
+        final Key key2 = new Key(Set.of(testIdent), 1, 0, 0);
+        final long bucketDuration = 10;
+
+        // Prepare entries for testing, with different bucket start timestamps.
+        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
+                4, 50, 5, 60);
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(20, 10, 3,
+                41, 7, 1, 0);
+        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(30, 10, 1,
+                21, 70, 4, 1);
+
+        NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry1)
+                .addEntry(entry2)
+                .build();
+        NetworkStatsHistory history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        NetworkStatsCollection collection = new NetworkStatsCollection.Builder(bucketDuration)
+                .addEntry(key1, history1)
+                .addEntry(key2, history2)
+                .build();
+
+        // Verify nothing is removed if the cutoff time is equal to bucketStart.
+        collection.removeHistoryBefore(10);
+        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
+        expectedEntries.put(key1, history1);
+        expectedEntries.put(key2, history2);
+        assertCollectionEntries(expectedEntries, collection);
+
+        // Verify entry1 will be removed if its bucket start before to cutoff timestamp.
+        collection.removeHistoryBefore(11);
+        history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .build();
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry2)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoff1Entries1 = new ArrayMap<>();
+        cutoff1Entries1.put(key1, history1);
+        cutoff1Entries1.put(key2, history2);
+        assertCollectionEntries(cutoff1Entries1, collection);
+
+        // Verify entry2 will be removed if its bucket start covers by cutoff timestamp.
+        collection.removeHistoryBefore(22);
+        history2 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry3)
+                .build();
+        final Map<Key, NetworkStatsHistory> cutoffEntries2 = new ArrayMap<>();
+        // History1 is not expected since the collection will omit empty entries.
+        cutoffEntries2.put(key2, history2);
+        assertCollectionEntries(cutoffEntries2, collection);
+
+        // Verify all entries will be removed if cutoff timestamp covers all.
+        collection.removeHistoryBefore(Long.MAX_VALUE);
+        assertEquals(0, collection.getEntries().size());
+    }
+
     /**
      * Copy a {@link Resources#openRawResource(int)} into {@link File} for
      * testing purposes.
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c5f8c00..26079a2 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -270,7 +270,7 @@
     }
 
     @Test
-    public void testRemove() throws Exception {
+    public void testRemoveStartingBefore() throws Exception {
         stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
 
         // record some data across 24 buckets
@@ -278,28 +278,28 @@
         assertEquals(24, stats.size());
 
         // try removing invalid data; should be no change
-        stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+        stats.removeBucketsStartingBefore(0 - DAY_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing far before buckets; should be no change
-        stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START - YEAR_IN_MILLIS);
         assertEquals(24, stats.size());
 
         // try removing just moments into first bucket; should be no change
-        // since that bucket contains data beyond the cutoff
-        stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS);
+        // since that bucket doesn't contain data starts before the cutoff
+        stats.removeBucketsStartingBefore(TEST_START);
         assertEquals(24, stats.size());
 
         // try removing single bucket
-        stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + HOUR_IN_MILLIS);
         assertEquals(23, stats.size());
 
         // try removing multiple buckets
-        stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+        stats.removeBucketsStartingBefore(TEST_START + (4 * HOUR_IN_MILLIS));
         assertEquals(20, stats.size());
 
         // try removing all buckets
-        stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+        stats.removeBucketsStartingBefore(TEST_START + YEAR_IN_MILLIS);
         assertEquals(0, stats.size());
     }
 
@@ -349,7 +349,7 @@
                         stats.recordData(start, end, entry);
                     } else {
                         // trim something
-                        stats.removeBucketsBefore(r.nextLong());
+                        stats.removeBucketsStartingBefore(r.nextLong());
                     }
                 }
                 assertConsistent(stats);
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 453612f..abd1825 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -29,12 +29,8 @@
 import android.net.NetworkStats.METERED_NO
 import android.net.NetworkStats.METERED_YES
 import android.net.NetworkStats.ROAMING_ALL
-import android.net.NetworkTemplate.MATCH_BLUETOOTH
-import android.net.NetworkTemplate.MATCH_CARRIER
-import android.net.NetworkTemplate.MATCH_ETHERNET
 import android.net.NetworkTemplate.MATCH_MOBILE
 import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
-import android.net.NetworkTemplate.MATCH_PROXY
 import android.net.NetworkTemplate.MATCH_WIFI
 import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
 import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -52,11 +48,9 @@
 import android.net.wifi.WifiInfo
 import android.os.Build
 import android.telephony.TelephonyManager
-import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
 import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SC_V2
 import com.android.testutils.assertParcelSane
 import org.junit.Before
 import org.junit.Test
@@ -65,7 +59,6 @@
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
@@ -555,140 +548,4 @@
             it.assertMatches(identMobileImsi3)
         }
     }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderMatchRules() {
-        // Verify unknown match rules cannot construct templates.
-        listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify hidden match rules cannot construct templates.
-        listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(it).build()
-            }
-        }
-
-        // Verify template which matches metered cellular and carrier networks with
-        // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
-        listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
-                    .setMeteredness(METERED_YES).build().let {
-                        val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
-                                arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                        assertEquals(expectedTemplate, it)
-                    }
-        }
-
-        // Verify carrier template cannot be created without IMSI.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_CARRIER).build()
-        }
-
-        // Verify template which matches metered cellular networks,
-        // regardless of IMSI. See buildTemplateMobileWildcard.
-        NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches metered cellular networks and ratType.
-        // See NetworkTemplate#buildTemplateMobileWithRatType.
-        NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
-                .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
-                .build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
-                            ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches all wifi networks,
-        // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
-        NetworkTemplate.Builder(MATCH_WIFI).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches wifi networks with the given Wifi Network Key.
-        // See buildTemplateWifi(wifiNetworkKey).
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
-                    null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches all wifi networks with the
-        // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
-        NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
-                .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
-                    val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
-                            arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
-                            METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                            OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
-                    assertEquals(expectedTemplate, it)
-                }
-
-        // Verify template which matches ethernet and bluetooth networks.
-        // See buildTemplateEthernet and buildTemplateBluetooth.
-        listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
-            NetworkTemplate.Builder(matchRule).build().let {
-                val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
-                        null /*subscriberIds*/, arrayOf<String>(),
-                        METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                        OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-                assertEquals(expectedTemplate, it)
-            }
-        }
-    }
-
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
-    @Test
-    fun testBuilderWifiNetworkKeys() {
-        // Verify template builder which generates same template with the given different
-        // sequence keys.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
-            val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
-                    setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
-            assertEquals(expectedTemplate, it)
-        }
-
-        // Verify template which matches non-wifi networks with the given key is invalid.
-        listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
-                Integer.MAX_VALUE).forEach { matchRule ->
-            assertFailsWith<IllegalArgumentException> {
-                NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
-            }
-        }
-
-        // Verify template which matches wifi networks with the given null key is invalid.
-        assertFailsWith<IllegalArgumentException> {
-            NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
-        }
-
-        // Verify template which matches wifi wildcard with the given empty key set.
-        NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
-            val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
-                    arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
-                    METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
-                    OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
-            assertEquals(expectedTemplate, it)
-        }
-    }
 }
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index 91f2cdd..6820b40 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -16,8 +16,17 @@
 
 package android.net;
 
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.os.Build;
 
@@ -29,6 +38,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
@@ -36,14 +46,14 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class QosSocketFilterTest {
-
+    private static final int TEST_NET_ID = 1777;
+    private final Network mNetwork = new Network(TEST_NET_ID);
     @Test
     public void testPortExactMatch() {
         final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
         final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
         assertTrue(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
-
     }
 
     @Test
@@ -77,5 +87,90 @@
         assertFalse(QosSocketFilter.matchesAddress(
                 new InetSocketAddress(addressA, 10), addressB, 10, 10));
     }
+
+    @Test
+    public void testAddressMatchWithAnyLocalAddresses() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("0.0.0.0");
+        assertTrue(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 10), addressB, 10, 10));
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressB, 10), addressA, 10, 10));
+    }
+
+    @Test
+    public void testProtocolMatch() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 10));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+        socketV6.connect(new InetSocketAddress("::1", socketV6.getLocalPort() + 10));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertTrue(socketFilter.matchesProtocol(IPPROTO_UDP));
+        assertTrue(socketFilter6.matchesProtocol(IPPROTO_UDP));
+        assertFalse(socketFilter.matchesProtocol(IPPROTO_TCP));
+        assertFalse(socketFilter6.matchesProtocol(IPPROTO_TCP));
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidate() throws Exception {
+        DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 7));
+        DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter6.validate());
+        socket.close();
+        socketV6.close();
+    }
+
+    @Test
+    public void testValidateUnbind() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_BOUND, socketFilter.validate());
+        socket.close();
+    }
+
+    @Test
+    public void testValidateLocalAddressChanged() throws Exception {
+        DatagramSocket socket = new DatagramSocket(null);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socket6);
+        QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+        socket.bind(new InetSocketAddress("127.0.0.1", 0));
+        socket6.bind(new InetSocketAddress("::1", 0));
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter.validate());
+        assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter6.validate());
+        socket.close();
+        socket6.close();
+    }
+
+    @Test
+    public void testValidateRemoteAddressChanged() throws Exception {
+        DatagramSocket socket;
+        socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 53137));
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 11));
+        QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+        QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+        assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+        socket.disconnect();
+        assertEquals(EX_TYPE_FILTER_SOCKET_NOT_CONNECTED, socketFilter.validate());
+        socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 13));
+        assertEquals(EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED, socketFilter.validate());
+        socket.close();
+    }
 }
 
diff --git a/tests/unit/java/android/net/QosSocketInfoTest.java b/tests/unit/java/android/net/QosSocketInfoTest.java
new file mode 100644
index 0000000..749c182
--- /dev/null
+++ b/tests/unit/java/android/net/QosSocketInfoTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class QosSocketInfoTest {
+    @Mock
+    private Network mMockNetwork = mock(Network.class);
+
+    @Test
+    public void testConstructWithSock() throws Exception {
+        ServerSocket server = new ServerSocket();
+        ServerSocket server6 = new ServerSocket();
+
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        server.bind(serverAddr);
+        server6.bind(serverAddr6);
+        Socket socket = new Socket(serverAddr.getAddress(), server.getLocalPort(),
+                clientAddr.getAddress(), clientAddr.getPort());
+        Socket socket6 = new Socket(serverAddr6.getAddress(), server6.getLocalPort(),
+                clientAddr6.getAddress(), clientAddr6.getPort());
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort())));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals(new InetSocketAddress(socket6.getLocalAddress(), socket6.getLocalPort())));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_STREAM, sockInfo6.getSocketType());
+        socket.close();
+        socket6.close();
+        server.close();
+        server6.close();
+    }
+
+    @Test
+    public void testConstructWithDatagramSock() throws Exception {
+        InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+        InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+        InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+        DatagramSocket socket = new DatagramSocket(null);
+        socket.setReuseAddress(true);
+        socket.bind(clientAddr);
+        socket.connect(serverAddr);
+        DatagramSocket socket6 = new DatagramSocket(null);
+        socket6.setReuseAddress(true);
+        socket6.bind(clientAddr);
+        socket6.connect(serverAddr);
+        QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+        QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+        assertTrue(sockInfo.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket.getLocalSocketAddress()));
+        assertTrue(sockInfo.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo.getSocketType());
+        assertTrue(sockInfo6.getLocalSocketAddress()
+                .equals((InetSocketAddress) socket6.getLocalSocketAddress()));
+        assertTrue(sockInfo6.getRemoteSocketAddress()
+                .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+        assertEquals(SOCK_DGRAM, sockInfo6.getSocketType());
+        socket.close();
+    }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 32ea9c3..ef97168 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -51,6 +51,9 @@
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -104,6 +107,10 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -136,6 +143,8 @@
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_PROFILE;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_VPN;
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -467,6 +476,9 @@
     private static final int TEST_APP_ID_2 = 104;
     private static final int TEST_WORK_PROFILE_APP_UID_2 =
             UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+    private static final int TEST_APP_ID_3 = 105;
+    private static final int TEST_APP_ID_4 = 106;
+    private static final int TEST_APP_ID_5 = 107;
 
     private static final String CLAT_PREFIX = "v4-";
     private static final String MOBILE_IFNAME = "test_rmnet_data0";
@@ -796,6 +808,32 @@
         }
     }
 
+    // This was only added in the T SDK, but this test needs to build against the R+S SDKs, too.
+    private static int toSdkSandboxUid(int appUid) {
+        final int firstSdkSandboxUid = 20000;
+        return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
+    }
+
+    // This function assumes the UID range for user 0 ([1, 99999])
+    private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
+        int start = 1;
+        Arrays.sort(excludedUids);
+        List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+        for (int excludedUid : excludedUids) {
+            if (excludedUid == start) {
+                start++;
+            } else {
+                parcels.add(new UidRangeParcel(start, excludedUid - 1));
+                start = excludedUid + 1;
+            }
+        }
+        if (start <= 99999) {
+            parcels.add(new UidRangeParcel(start, 99999));
+        }
+
+        return parcels.toArray(new UidRangeParcel[0]);
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
         waitForIdle(mCellNetworkAgent, TIMEOUT_MS);
@@ -5871,7 +5909,7 @@
     }
 
     /**
-     * Validate the callback flow CBS request without carrier privilege.
+     * Validate the service throws if request with CBS but without carrier privilege.
      */
     @Test
     public void testCBSRequestWithoutCarrierPrivilege() throws Exception {
@@ -5880,10 +5918,8 @@
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
 
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
-        // Now file the test request and expect it.
-        mCm.requestNetwork(nr, networkCallback);
-        networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
-        mCm.unregisterNetworkCallback(networkCallback);
+        // Now file the test request and expect the service throws.
+        assertThrows(SecurityException.class, () -> mCm.requestNetwork(nr, networkCallback));
     }
 
     private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
@@ -9009,10 +9045,16 @@
                 allowList);
         waitForIdle();
 
-        UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1);
-        UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999);
+        final Set<Integer> excludedUids = new ArraySet<Integer>();
+        excludedUids.add(VPN_UID);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(VPN_UID));
+        }
+        final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
         InOrder inOrder = inOrder(mMockNetd);
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
 
         // Connect a network when lockdown is active, expect to see it blocked.
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -9036,7 +9078,7 @@
         vpnUidCallback.assertNoCallback();
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
-        expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcels);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9053,13 +9095,14 @@
         vpnUidDefaultCallback.assertNoCallback();
         vpnDefaultCallbackAsUid.assertNoCallback();
 
-        // The following requires that the UID of this test package is greater than VPN_UID. This
-        // is always true in practice because a plain AOSP build with no apps installed has almost
-        // 200 packages installed.
-        final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1);
-        final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1);
-        final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999);
-        expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3);
+        excludedUids.add(uid);
+        if (SdkLevel.isAtLeastT()) {
+            // On T onwards, the corresponding SDK sandbox UID should also be excluded
+            excludedUids.add(toSdkSandboxUid(uid));
+        }
+        final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
+                excludedUids.toArray(new Integer[0]));
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9085,12 +9128,12 @@
         // Everything should now be blocked.
         mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3);
+        expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
         allowList.clear();
         mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
                 allowList);
         waitForIdle();
-        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
         defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
         assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
         vpnUidCallback.assertNoCallback();
@@ -9462,6 +9505,46 @@
         b2.expectBroadcast();
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testLockdownSetFirewallUidRule() throws Exception {
+        // For ConnectivityService#setAlwaysOnVpnPackage.
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+        // Needed to call Vpn#setAlwaysOnPackage.
+        mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        // Needed to call Vpn#isAlwaysOnPackageSupported.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        // Enable Lockdown
+        final ArrayList<String> allowList = new ArrayList<>();
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
+                true /* lockdown */, allowList);
+        waitForIdle();
+
+        // Lockdown rule is set to apps uids
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+
+        reset(mBpfNetMaps);
+
+        // Disable lockdown
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
+                allowList);
+        waitForIdle();
+
+        // Lockdown rule is removed from apps uids
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps).setUidRule(
+                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+
+        // Interface rules are not changed by Lockdown mode enable/disable
+        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+    }
+
     /**
      * Test mutable and requestable network capabilities such as
      * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -10333,7 +10416,7 @@
         verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
         assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
         assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
-        assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
+        assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
 
         mMockVpn.disconnect();
         waitForIdle();
@@ -10341,11 +10424,11 @@
         // Disconnected VPN should have interface rules removed
         verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
         assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
-        assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
     }
 
     @Test
-    public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
@@ -10355,13 +10438,34 @@
         mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
 
-        // Legacy VPN should not have interface rules set up
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        if (SdkLevel.isAtLeastT()) {
+            // On T and above, A connected Legacy VPN should have interface rules with null
+            // interface. Null Interface is a wildcard and this accepts traffic from all the
+            // interfaces. There are two expected invocations, one during the VPN initial
+            // connection, one during the VPN LinkProperties update.
+            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+                    eq(null) /* iface */, uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+                    vpnRange);
+
+            mMockVpn.disconnect();
+            waitForIdle();
+
+            // Disconnected VPN should have interface rules removed
+            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+        } else {
+            // Before T, Legacy VPN should not have interface rules.
+            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        }
     }
 
     @Test
-    public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
-            throws Exception {
+    public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
@@ -10371,8 +10475,31 @@
         mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
 
-        // IPv6 unreachable route should not be misinterpreted as a default route
-        verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        if (SdkLevel.isAtLeastT()) {
+            // IPv6 unreachable route should not be misinterpreted as a default route
+            // On T and above, A connected VPN that does not provide a default route should have
+            // interface rules with null interface. Null Interface is a wildcard and this accepts
+            // traffic from all the interfaces. There are two expected invocations, one during the
+            // VPN initial connection, one during the VPN LinkProperties update.
+            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+                    eq(null) /* iface */, uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+                    vpnRange);
+
+            mMockVpn.disconnect();
+            waitForIdle();
+
+            // Disconnected VPN should have interface rules removed
+            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+        } else {
+            // Before T, VPN with IPv6 unreachable route should not have interface rules.
+            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+        }
     }
 
     @Test
@@ -11937,16 +12064,14 @@
         mQosCallbackMockHelper.registerQosCallback(
                 mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
 
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 =
-                (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister)
-                        wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbRegister1);
 
         final int registerCallbackId = cbRegister1.mQosCallbackId;
         mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback);
-        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
-        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
-                wrapper.getCallbackHistory().poll(1000, x -> true);
+        final OnQosCallbackUnregister cbUnregister =
+                (OnQosCallbackUnregister) wrapper.getCallbackHistory().poll(1000, x -> true);
         assertNotNull(cbUnregister);
         assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
         assertNull(wrapper.getCallbackHistory().poll(200, x -> true));
@@ -12025,6 +12150,86 @@
                         && session.getSessionType() == QosSession.TYPE_NR_BEARER));
     }
 
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackAvailableOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper;
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        OnQosCallbackRegister cbRegister1 =
+                (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbRegister1);
+        final int registerCallbackId = cbRegister1.mQosCallbackId;
+
+        waitForIdle();
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                1, 2, 3, 4, 5, new ArrayList<>());
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+        waitForIdle();
+
+        final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
+        cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
+                wrapper.getCallbackHistory().poll(1000, x -> true);
+        assertNotNull(cbUnregister);
+        assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testQosCallbackLostOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+        EpsBearerQosSessionAttributes attributes =
+                sendQosSessionEvent(qosCallbackId, sessionId, true);
+        waitForIdle();
+
+        verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes));
+
+        doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+                .when(mQosCallbackMockHelper.mFilter).validate();
+
+        sendQosSessionEvent(qosCallbackId, sessionId, false);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+    }
+
+    private EpsBearerQosSessionAttributes sendQosSessionEvent(
+            int qosCallbackId, int sessionId, boolean available) {
+        if (available) {
+            final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                    1, 2, 3, 4, 5, new ArrayList<>());
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+            return attributes;
+        } else {
+            mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                    .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER);
+            return null;
+        }
+
+    }
+
     @Test
     public void testQosCallbackTooManyRequests() throws Exception {
         mQosCallbackMockHelper = new QosCallbackMockHelper();
@@ -14548,7 +14753,7 @@
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
-                NetworkCapabilities.NET_ENTERPRISE_ID_2);
+                NET_ENTERPRISE_ID_2);
         registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
                 profileNetworkPreferenceBuilder.build(), true,
@@ -14573,6 +14778,393 @@
     }
 
     /**
+     * Make sure per-profile networking preference throws exception when default preference
+     * is set along with enterprise preference.
+     */
+    @Test
+    public void testPreferenceWithInvalidPreferenceDefaultAndEnterpriseTogether()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder1.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder2.build(),
+                                profileNetworkPreferenceBuilder1.build()),
+                        r -> r.run(), listener));
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when two slices with
+     * two different apps within same user profile is configured
+     * Make sure per profile network preferences overrides with latest preference when
+     * same user preference is set twice
+     */
+    @Test
+    public void testSetPreferenceWithOverridingPreference()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        // Set preferences for testHandle to map testWorkProfileAppUid1 to
+        // NET_ENTERPRISE_ID_1 and testWorkProfileAppUid2 to NET_ENTERPRISE_ID_2.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+
+        // Set preferences for testHandle to map testWorkProfileAppUid3 to
+        // to NET_ENTERPRISE_ID_1.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder3.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb3.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb2);
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        workAgent2.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
+     * Make sure per profile network preferences behave as expected when multiple slices with
+     * multiple different apps within same user profile is configured.
+     */
+    @Test
+    public void testSetPreferenceWithMultiplePreferences()
+            throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        mServiceContext.setWorkProfile(testHandle, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback appCb1 = new TestNetworkCallback();
+        final TestNetworkCallback appCb2 = new TestNetworkCallback();
+        final TestNetworkCallback appCb3 = new TestNetworkCallback();
+        final TestNetworkCallback appCb4 = new TestNetworkCallback();
+        final TestNetworkCallback appCb5 = new TestNetworkCallback();
+
+        final int testWorkProfileAppUid1 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+        final int testWorkProfileAppUid2 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+        final int testWorkProfileAppUid3 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+        final int testWorkProfileAppUid4 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_4);
+        final int testWorkProfileAppUid5 =
+                UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_5);
+
+        registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+        registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+        registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+        registerDefaultNetworkCallbackAsUid(appCb4, testWorkProfileAppUid4);
+        registerDefaultNetworkCallbackAsUid(appCb5, testWorkProfileAppUid5);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+        final TestNetworkAgentWrapper workAgent3 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_3);
+        final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
+        final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
+
+        workAgent1.connect(true);
+        workAgent2.connect(true);
+        workAgent3.connect(true);
+        workAgent4.connect(true);
+        workAgent5.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb4.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        appCb5.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent3.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent4.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent5.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+        profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder3.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_3);
+        profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder4 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder4.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder4.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_4);
+        profileNetworkPreferenceBuilder4.setIncludedUids(new int[]{testWorkProfileAppUid4});
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder5 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder5.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder5.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_5);
+        profileNetworkPreferenceBuilder5.setIncludedUids(new int[]{testWorkProfileAppUid5});
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder1.build(),
+                        profileNetworkPreferenceBuilder2.build(),
+                        profileNetworkPreferenceBuilder3.build(),
+                        profileNetworkPreferenceBuilder4.build(),
+                        profileNetworkPreferenceBuilder5.build()),
+                r -> r.run(), listener);
+
+        listener.expectOnComplete();
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent1.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent3.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent4.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                workAgent5.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        appCb1.expectAvailableCallbacksValidated(workAgent1);
+        appCb2.expectAvailableCallbacksValidated(workAgent2);
+        appCb3.expectAvailableCallbacksValidated(workAgent3);
+        appCb4.expectAvailableCallbacksValidated(workAgent4);
+        appCb5.expectAvailableCallbacksValidated(workAgent5);
+
+        workAgent1.disconnect();
+        workAgent2.disconnect();
+        workAgent3.disconnect();
+        workAgent4.disconnect();
+        workAgent5.disconnect();
+
+        appCb1.expectCallback(CallbackEntry.LOST, workAgent1);
+        appCb2.expectCallback(CallbackEntry.LOST, workAgent2);
+        appCb3.expectCallback(CallbackEntry.LOST, workAgent3);
+        appCb4.expectCallback(CallbackEntry.LOST, workAgent4);
+        appCb5.expectCallback(CallbackEntry.LOST, workAgent5);
+
+        appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb2.assertNoCallback();
+        appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.assertNoCallback();
+        appCb5.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+                PREFERENCE_ORDER_PROFILE));
+        verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+                mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+                PREFERENCE_ORDER_PROFILE));
+
+        mSystemDefaultNetworkCallback.assertNoCallback();
+        mDefaultNetworkCallback.assertNoCallback();
+
+        // Set the preferences for testHandle to default.
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+        mCm.setProfileNetworkPreferences(testHandle,
+                List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb3,
+                appCb5);
+        appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        appCb4.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(appCb1);
+        mCm.unregisterNetworkCallback(appCb2);
+        mCm.unregisterNetworkCallback(appCb3);
+        mCm.unregisterNetworkCallback(appCb4);
+        mCm.unregisterNetworkCallback(appCb5);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    /**
      * Test that, in a given networking context, calling setPreferenceForUser to set per-profile
      * defaults on then off works as expected.
      */
@@ -14937,6 +15529,27 @@
     }
 
     @Test
+    public void testAutomotiveEthernetAllowedUids() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+        mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+
+        // In this test the automotive feature will be enabled.
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+        // Simulate a restricted ethernet network.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_ETHERNET)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mEthernetNetworkAgent, TRANSPORT_ETHERNET, agentNetCaps, true);
+    }
+
+    @Test
     public void testCbsAllowedUids() throws Exception {
         mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
         mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
@@ -14945,6 +15558,24 @@
         doReturn(true).when(mCarrierPrivilegeAuthenticator)
                 .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
 
+        // Simulate a restricted telephony network. The telephony factory is entitled to set
+        // the access UID to the service package on any of its restricted networks.
+        final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+                new LinkProperties(), agentNetCaps.build());
+        validateAllowedUids(mCellNetworkAgent, TRANSPORT_CELLULAR, agentNetCaps, false);
+    }
+
+    private void validateAllowedUids(final TestNetworkAgentWrapper testAgent,
+            @NetworkCapabilities.Transport final int transportUnderTest,
+            final NetworkCapabilities.Builder ncb, final boolean forAutomotive) throws Exception {
         final ArraySet<Integer> serviceUidSet = new ArraySet<>();
         serviceUidSet.add(TEST_PACKAGE_UID);
         final ArraySet<Integer> nonServiceUidSet = new ArraySet<>();
@@ -14955,40 +15586,34 @@
 
         final TestNetworkCallback cb = new TestNetworkCallback();
 
-        // Simulate a restricted telephony network. The telephony factory is entitled to set
-        // the access UID to the service package on any of its restricted networks.
-        final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
-
+        /* Test setting UIDs */
         // Cell gets to set the service UID as access UID
         mCm.requestNetwork(new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
+                .addTransportType(transportUnderTest)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .build(), cb);
-        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
-                new LinkProperties(), ncb.build());
-        mCellNetworkAgent.connect(true);
-        cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        testAgent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(testAgent);
         ncb.setAllowedUids(serviceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
             cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
         }
 
+        /* Test setting UIDs is rejected when expected */
+        if (forAutomotive) {
+            mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        }
+
         // ...but not to some other UID. Rejection sets UIDs to the empty set
         ncb.setAllowedUids(nonServiceUidSet);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         if (SdkLevel.isAtLeastT()) {
-            cb.expectCapabilitiesThat(mCellNetworkAgent,
+            cb.expectCapabilitiesThat(testAgent,
                     caps -> caps.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
@@ -14997,18 +15622,18 @@
 
         // ...and also not to multiple UIDs even including the service UID
         ncb.setAllowedUids(serviceUidSetPlus);
-        mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+        testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
         cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
 
-        mCellNetworkAgent.disconnect();
-        cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        testAgent.disconnect();
+        cb.expectCallback(CallbackEntry.LOST, testAgent);
         mCm.unregisterNetworkCallback(cb);
 
         // Must be unset before touching the transports, because remove and add transport types
         // check the specifier on the builder immediately, contradicting normal builder semantics
         // TODO : fix the builder
         ncb.setNetworkSpecifier(null);
-        ncb.removeTransportType(TRANSPORT_CELLULAR);
+        ncb.removeTransportType(transportUnderTest);
         ncb.addTransportType(TRANSPORT_WIFI);
         // Wifi does not get to set access UID, even to the correct UID
         mCm.requestNetwork(new NetworkRequest.Builder()
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 45f3d3c..061dc87 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -783,6 +783,23 @@
     }
 
     @Test
+    public void testSetNetworkForTunnelInterfaceFailsForNullLp() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final Network newFakeNetwork = new Network(1000);
+        final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+
+        try {
+            mIpSecService.setNetworkForTunnelInterface(
+                    tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+            fail(
+                    "Expected an IllegalArgumentException for underlying network with null"
+                            + " LinkProperties");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
     public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception {
         final IpSecTunnelInterfaceResponse createTunnelResp =
                 createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index fb821c3..ecd17ba 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,6 +30,9 @@
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -761,8 +764,8 @@
                 MOCK_APPID1);
     }
 
-    @Test
-    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+    private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+            throws Exception {
         doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
@@ -778,8 +781,8 @@
         final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
 
         // When VPN is connected, expect a rule to be set up for user app MOCK_UID11
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
@@ -787,27 +790,38 @@
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
         mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
 
         reset(mBpfNetMaps);
 
         // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
         // old UID rules then adds the new ones. Expect netd to be updated
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
 
         reset(mBpfNetMaps);
 
         // When VPN is disconnected, expect rules to be torn down
-        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
         verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
-        assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+        assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
     }
 
     @Test
-    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
+            throws Exception {
+        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+    }
+
+    private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
+            Exception {
         doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
@@ -818,12 +832,12 @@
         mPermissionMonitor.startMonitoring();
         final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
                 UidRange.createForUser(MOCK_USER2));
-        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+        mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
 
         // Newly-installed package should have uid rules added
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
-        verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+        verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
 
         // Removed package should have its uid rules removed
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,168 @@
         verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
     }
 
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
+    }
+
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
+        doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisable() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // Every app on user 0 except MOCK_UID12 are under VPN.
+        final UidRange[] vpnRange1 = {
+                new UidRange(0, MOCK_UID12 - 1),
+                new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
+        };
+
+        // Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
+        verify(mBpfNetMaps)
+                .setUidRule(
+                        eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range, expect rules to be torn down
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is under VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] vpnRange = {range};
+
+        // Add Lockdown uid range at 1st time, expect a rule to be set up
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
+        // already has the rule
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
+        // the range 2 times.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
+        // twice and we removed twice.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        mPermissionMonitor.startMonitoring();
+        // MOCK_UID11 is under VPN.
+        final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+        final UidRange[] vpnRangeDuplicates = {range, range};
+        final UidRange[] vpnRange = {range};
+
+        // Add Lockdown uid ranges which contains duplicated uid ranges
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
+        // ranges we added contains duplicated uid ranges.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+        reset(mBpfNetMaps);
+
+        // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+    }
+
+    @Test
+    public void testLockdownUidFilteringWithInstallAndUnInstall() {
+        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+                        NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+                buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+                .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+        doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
+
+        mPermissionMonitor.startMonitoring();
+        final UidRange[] vpnRange = {
+                UidRange.createForUser(MOCK_USER1),
+                UidRange.createForUser(MOCK_USER2)
+        };
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+
+        // Installing package should add Lockdown rules
+        addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+                        eq(FIREWALL_RULE_DENY));
+
+        reset(mBpfNetMaps);
+
+        // Uninstalling package should remove Lockdown rules
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+        verify(mBpfNetMaps)
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+                        eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps, never())
+                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+                        eq(FIREWALL_RULE_ALLOW));
+    }
 
     // Normal package add/remove operations will trigger multiple intent for uids corresponding to
     // each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 6da4f07..11fbcb9 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -28,9 +28,11 @@
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
 import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -86,8 +88,11 @@
 import android.net.LinkProperties;
 import android.net.LocalSocket;
 import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkProvider;
 import android.net.RouteInfo;
 import android.net.UidRangeParcel;
 import android.net.VpnManager;
@@ -104,6 +109,7 @@
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
+import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.UserHandle;
@@ -121,13 +127,16 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
+import com.android.internal.util.HexDump;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.server.IpSecService;
+import com.android.server.vcn.util.PersistableBundleUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
@@ -164,10 +173,13 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
+@IgnoreUpTo(VERSION_CODES.S_V2)
 public class VpnTest {
     private static final String TAG = "VpnTest";
 
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     // Mock users
     static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
     static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
@@ -191,14 +203,16 @@
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
-
+    private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
+            "VPN_APP_EXCLUDED_27_com.testvpn.vpn";
     /**
      * Names and UIDs for some fake packages. Important points:
      *  - UID is ordered increasing.
      *  - One pair of packages have consecutive UIDs.
      */
     static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
-    static final int[] PKG_UIDS = {66, 77, 78, 400};
+    static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
+    static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
 
     // Mock packages
     static final Map<String, Integer> mPackages = new ArrayMap<>();
@@ -311,6 +325,17 @@
         return new Range<Integer>(start, stop);
     }
 
+    private static String getPackageByteString(List<String> packages) {
+        try {
+            return HexDump.toHexString(
+                    PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
+                            packages, PersistableBundleUtils.STRING_SERIALIZER)),
+                        true /* upperCase */);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     @Test
     public void testRestrictedProfilesAreAddedToVpn() {
         setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
@@ -360,7 +385,11 @@
                 Arrays.asList(packages), null /* disallowedApplications */);
         assertEquals(rangeSet(
                 uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
-                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])),
+                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
                 allow);
 
         // Denied list
@@ -371,7 +400,11 @@
                 uidRange(userStart, userStart + PKG_UIDS[0] - 1),
                 uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
                 /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
-                uidRange(userStart + PKG_UIDS[2] + 1, userStop)),
+                uidRange(userStart + PKG_UIDS[2] + 1,
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                         Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
                 disallow);
     }
 
@@ -412,18 +445,24 @@
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Switch to another app.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
     }
 
@@ -438,17 +477,25 @@
                 PKGS[1], true, Collections.singletonList(PKGS[2])));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[]  {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         // Change allowed app list to PKGS[3].
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[1], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
 
         // Change the VPN app.
@@ -456,32 +503,52 @@
                 PKGS[0], true, Collections.singletonList(PKGS[3])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
         }));
 
         // Remove the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop),
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
 
         // Add the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[0], true, Collections.singletonList(PKGS[1])));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
 
         // Try allowing a package with a comma, should be rejected.
@@ -494,11 +561,19 @@
                 PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
         }));
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
+                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
         }));
     }
 
@@ -543,7 +618,10 @@
         };
         final UidRangeParcel[] exceptPkg0 = {
             new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
-            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop)
+            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
+                               Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
+            new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
+                               entireUser[0].stop),
         };
 
         final InOrder order = inOrder(mConnectivityManager);
@@ -711,6 +789,47 @@
         }
     }
 
+    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
+                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+        vpn.mNetworkAgent = new NetworkAgent(mContext, Looper.getMainLooper(), TAG,
+                new NetworkCapabilities.Builder().build(), new LinkProperties(), 10 /* score */,
+                new NetworkAgentConfig.Builder().build(),
+                new NetworkProvider(mContext, Looper.getMainLooper(), TAG)) {};
+        return vpn;
+    }
+
+    @Test @IgnoreUpTo(S_V2)
+    public void testSetAndGetAppExclusionList() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
+        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+        verify(mVpnProfileStore)
+                .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
+                     eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
+        assertEquals(vpn.createUserAndRestrictedProfilesRanges(
+                primaryUser.id, null, Arrays.asList(PKGS)),
+                vpn.mNetworkCapabilities.getUids());
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
+    @Test @IgnoreUpTo(S_V2)
+    public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        // Mock it to restricted profile
+        when(mUserManager.getUserInfo(anyInt())).thenReturn(restrictedProfileA);
+        // Restricted users cannot configure VPNs
+        assertThrows(SecurityException.class,
+                () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
+        assertThrows(SecurityException.class, () -> vpn.getAppExclusionList(TEST_VPN_PKG));
+    }
+
     @Test
     public void testProvisionVpnProfilePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
new file mode 100644
index 0000000..a9f80ea
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.util.ArrayMap;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class EthernetConfigStoreTest {
+    private static final LinkAddress LINKADDR = new LinkAddress("192.168.1.100/25");
+    private static final InetAddress GATEWAY = InetAddresses.parseNumericAddress("192.168.1.1");
+    private static final InetAddress DNS1 = InetAddresses.parseNumericAddress("8.8.8.8");
+    private static final InetAddress DNS2 = InetAddresses.parseNumericAddress("8.8.4.4");
+    private static final StaticIpConfiguration STATIC_IP_CONFIG =
+            new StaticIpConfiguration.Builder()
+                    .setIpAddress(LINKADDR)
+                    .setGateway(GATEWAY)
+                    .setDnsServers(new ArrayList<InetAddress>(
+                            List.of(DNS1, DNS2)))
+                    .build();
+    private static final ProxyInfo PROXY_INFO = ProxyInfo.buildDirectProxy("test", 8888);
+    private static final IpConfiguration APEX_IP_CONFIG =
+            new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+    private static final IpConfiguration LEGACY_IP_CONFIG =
+            new IpConfiguration(IpAssignment.STATIC, ProxySettings.STATIC, STATIC_IP_CONFIG,
+                    PROXY_INFO);
+
+    private EthernetConfigStore mEthernetConfigStore;
+    private File mApexTestDir;
+    private File mLegacyTestDir;
+    private File mApexConfigFile;
+    private File mLegacyConfigFile;
+
+    private void createTestDir() {
+        final Context context = InstrumentationRegistry.getContext();
+        final File baseDir = context.getFilesDir();
+        mApexTestDir = new File(baseDir.getPath() + "/apex");
+        mApexTestDir.mkdirs();
+
+        mLegacyTestDir = new File(baseDir.getPath() + "/legacy");
+        mLegacyTestDir.mkdirs();
+    }
+
+    @Before
+    public void setUp() {
+        createTestDir();
+        mEthernetConfigStore = new EthernetConfigStore();
+    }
+
+    @After
+    public void tearDown() {
+        mApexTestDir.delete();
+        mLegacyTestDir.delete();
+    }
+
+    private void assertConfigFileExist(final String filepath) {
+        assertTrue(new File(filepath).exists());
+    }
+
+    /** Wait for the delayed write operation completes. */
+    private void waitForMs(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (final InterruptedException e) {
+            fail("Thread was interrupted");
+        }
+    }
+
+    @Test
+    public void testWriteIpConfigToApexFilePathAndRead() throws Exception {
+        // Write the config file to the apex file path, pretend the config file exits and
+        // check if IP config should be read from apex file path.
+        mApexConfigFile = new File(mApexTestDir.getPath(), "test.txt");
+        mEthernetConfigStore.write("eth0", APEX_IP_CONFIG, mApexConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(APEX_IP_CONFIG, ipConfigurations.get("eth0"));
+
+        mApexConfigFile.delete();
+    }
+
+    @Test
+    public void testWriteIpConfigToLegacyFilePathAndRead() throws Exception {
+        // Write the config file to the legacy file path, pretend the config file exits and
+        // check if IP config should be read from legacy file path.
+        mLegacyConfigFile = new File(mLegacyTestDir, "test.txt");
+        mEthernetConfigStore.write("0", LEGACY_IP_CONFIG, mLegacyConfigFile.getPath());
+        waitForMs(50);
+
+        mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+        final ArrayMap<String, IpConfiguration> ipConfigurations =
+                mEthernetConfigStore.getIpConfigurations();
+        assertEquals(LEGACY_IP_CONFIG, ipConfigurations.get("0"));
+
+        // Check the same config file in apex file path is created.
+        assertConfigFileExist(mApexTestDir.getPath() + "/test.txt");
+
+        final File apexConfigFile = new File(mApexTestDir.getPath() + "/test.txt");
+        apexConfigFile.delete();
+        mLegacyConfigFile.delete();
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index dfb4fcc..568d40f 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -32,7 +32,6 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,6 +50,7 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
 import android.net.NetworkRequest;
 import android.net.StaticIpConfiguration;
 import android.net.ip.IpClientCallbacks;
@@ -99,6 +99,7 @@
     @Mock private EthernetNetworkAgent mNetworkAgent;
     @Mock private InterfaceParams mInterfaceParams;
     @Mock private Network mMockNetwork;
+    @Mock private NetworkProvider mNetworkProvider;
 
     @Before
     public void setUp() throws Exception {
@@ -112,7 +113,7 @@
     private void initEthernetNetworkFactory() {
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
-        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mNetworkProvider, mDeps);
     }
 
     private void setupNetworkAgentMock() {
@@ -239,9 +240,16 @@
         mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
                 createInterfaceCapsBuilder(transportType).build());
         assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+
+        ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
+                NetworkOfferCallback.class);
+        verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
+        captor.getValue().onNetworkNeeded(createDefaultRequest());
+
         verifyStart(ipConfig);
         clearInvocations(mDeps);
         clearInvocations(mIpClient);
+        clearInvocations(mNetworkProvider);
     }
 
     // creates a provisioned interface
@@ -281,29 +289,15 @@
         // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
         // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
         // then calling onNetworkUnwanted.
-        createAndVerifyProvisionedInterface(iface);
-
-        mNetworkAgent.getCallbacks().onNetworkUnwanted();
-        mLooper.dispatchAll();
-        verifyStop();
+        mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
+                createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
 
         clearInvocations(mIpClient);
         clearInvocations(mNetworkAgent);
     }
 
     @Test
-    public void testAcceptRequest() throws Exception {
-        initEthernetNetworkFactory();
-        createInterfaceUndergoingProvisioning(TEST_IFACE);
-        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
-
-        NetworkRequest wifiRequest = createDefaultRequestBuilder()
-                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
-        assertFalse(mNetFactory.acceptRequest(wifiRequest));
-    }
-
-    @Test
     public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
         initEthernetNetworkFactory();
         createInterfaceUndergoingProvisioning(TEST_IFACE);
@@ -378,36 +372,6 @@
     }
 
     @Test
-    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
-        initEthernetNetworkFactory();
-        createAndVerifyProvisionedInterface(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient, never()).startProvisioning(any());
-    }
-
-    @Test
-    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
-        initEthernetNetworkFactory();
-        createUnprovisionedInterface(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient).startProvisioning(any());
-
-        triggerOnProvisioningSuccess();
-        verifyNetworkAgentRegistersAndConnects();
-    }
-
-    @Test
-    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
-        initEthernetNetworkFactory();
-        createInterfaceUndergoingProvisioning(TEST_IFACE);
-        mNetFactory.needNetworkFor(createDefaultRequest());
-        verify(mIpClient, never()).startProvisioning(any());
-
-        triggerOnProvisioningSuccess();
-        verifyNetworkAgentRegistersAndConnects();
-    }
-
-    @Test
     public void testProvisioningLoss() throws Exception {
         initEthernetNetworkFactory();
         when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
@@ -441,31 +405,6 @@
     }
 
     @Test
-    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
-        initEthernetNetworkFactory();
-        createUnprovisionedInterface(TEST_IFACE);
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
-
-        mNetFactory.needNetworkFor(createDefaultRequest());
-
-        verify(mDeps, never()).makeIpClient(any(), any(), any());
-
-        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
-        // not start an IpClient when the link is down, but fixing this may make matters worse by
-        // tiggering b/197548738.
-        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
-                .build();
-        mNetFactory.needNetworkFor(specificNetRequest);
-        mNetFactory.releaseNetworkFor(specificNetRequest);
-
-        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
-        // TODO: change to once when b/191854824 is fixed.
-        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
-    }
-
-    @Test
     public void testLinkPropertiesChanged() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index dd1f1ed..aff824c 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -20,7 +20,6 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
-
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -35,8 +34,8 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.EthernetNetworkUpdateRequest;
+import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.NetworkCapabilities;
 import android.os.Handler;
@@ -111,18 +110,18 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsWhenEthNotStarted() {
+    public void testEnableInterfaceRejectsWhenEthNotStarted() {
         mEthernetServiceImpl.mStarted.set(false);
         assertThrows(IllegalStateException.class, () -> {
-            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+            mEthernetServiceImpl.enableInterface("" /* iface */, null /* listener */);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+    public void testDisableInterfaceRejectsWhenEthNotStarted() {
         mEthernetServiceImpl.mStarted.set(false);
         assertThrows(IllegalStateException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+            mEthernetServiceImpl.disableInterface("" /* iface */, null /* listener */);
         });
     }
 
@@ -134,16 +133,16 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsNullIface() {
+    public void testEnableInterfaceRejectsNullIface() {
         assertThrows(NullPointerException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(null /* iface */, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsNullIface() {
+    public void testDisableInterfaceRejectsNullIface() {
         assertThrows(NullPointerException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(null /* iface */, NULL_LISTENER);
         });
     }
 
@@ -165,22 +164,6 @@
                 eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
     }
 
-    @Test
-    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
-        toggleAutomotiveFeature(false);
-        assertThrows(UnsupportedOperationException.class, () -> {
-            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
-        });
-    }
-
-    @Test
-    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
-        toggleAutomotiveFeature(false);
-        assertThrows(UnsupportedOperationException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
-        });
-    }
-
     private void denyManageEthPermission() {
         doThrow(new SecurityException("")).when(mContext)
                 .enforceCallingOrSelfPermission(
@@ -202,18 +185,18 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+    public void testEnableInterfaceRejectsWithoutManageEthPermission() {
         denyManageEthPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+    public void testDisableInterfaceRejectsWithoutManageEthPermission() {
         denyManageEthPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
@@ -231,20 +214,20 @@
     }
 
     @Test
-    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+    public void testEnableInterfaceRejectsTestRequestWithoutTestPermission() {
         enableTestInterface();
         denyManageTestNetworksPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
     @Test
-    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+    public void testDisableInterfaceRejectsTestRequestWithoutTestPermission() {
         enableTestInterface();
         denyManageTestNetworksPermission();
         assertThrows(SecurityException.class, () -> {
-            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+            mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
         });
     }
 
@@ -258,15 +241,15 @@
     }
 
     @Test
-    public void testConnectNetwork() {
-        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    public void testEnableInterface() {
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     @Test
-    public void testDisconnectNetwork() {
-        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    public void testDisableInterface() {
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     @Test
@@ -324,23 +307,23 @@
     }
 
     @Test
-    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+    public void testEnableInterfaceForTestRequestDoesNotRequireNetPermission() {
         enableTestInterface();
         toggleAutomotiveFeature(false);
         denyManageEthPermission();
 
-        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+        mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     @Test
-    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+    public void testDisableInterfaceForTestRequestDoesNotRequireAutoOrNetPermission() {
         enableTestInterface();
         toggleAutomotiveFeature(false);
         denyManageEthPermission();
 
-        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
-        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+        mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
     }
 
     private void denyPermissions(String... permissions) {
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index b1831c4..ca988b6 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -29,22 +29,23 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.EthernetManager;
-import android.net.InetAddresses;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IEthernetServiceListener;
 import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
-import android.net.InterfaceConfigurationParcel;
 import android.net.LinkAddress;
 import android.net.NetworkCapabilities;
 import android.net.StaticIpConfiguration;
@@ -54,13 +55,13 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.connectivity.resources.R;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -351,8 +352,8 @@
     }
 
     @Test
-    public void testConnectNetworkCorrectlyCallsFactory() {
-        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+    public void testEnableInterfaceCorrectlyCallsFactory() {
+        tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
         waitForIdle();
 
         verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
@@ -360,8 +361,8 @@
     }
 
     @Test
-    public void testDisconnectNetworkCorrectlyCallsFactory() {
-        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+    public void testDisableInterfaceCorrectlyCallsFactory() {
+        tracker.disableInterface(TEST_IFACE, NULL_LISTENER);
         waitForIdle();
 
         verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
@@ -410,18 +411,24 @@
                 IpConfiguration configuration) { }
     }
 
+    private InterfaceConfigurationParcel createMockedIfaceParcel(final String ifname,
+            final String hwAddr) {
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = ifname;
+        ifaceParcel.hwAddr = hwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+        return ifaceParcel;
+    }
+
     @Test
     public void testListenEthernetStateChange() throws Exception {
-        final String testIface = "testtap123";
-        final String testHwAddr = "11:22:33:44:55:66";
-        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
-        ifaceParcel.ifName = testIface;
-        ifaceParcel.hwAddr = testHwAddr;
-        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
-
         tracker.setIncludeTestInterfaces(true);
         waitForIdle();
 
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = createMockedIfaceParcel(testIface,
+                testHwAddr);
         when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
         when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
         doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
@@ -453,4 +460,43 @@
         verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
                 anyInt(), any());
     }
+
+    @Test
+    public void testListenEthernetStateChange_unsolicitedEventListener() throws Exception {
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {});
+        doReturn(new String[] {}).when(mFactory).getAvailableInterfaces(anyBoolean());
+
+        tracker.setIncludeTestInterfaces(true);
+        tracker.start();
+
+        final ArgumentCaptor<EthernetTracker.InterfaceObserver> captor =
+                ArgumentCaptor.forClass(EthernetTracker.InterfaceObserver.class);
+        verify(mNetd, timeout(TIMEOUT_MS)).registerUnsolicitedEventListener(captor.capture());
+        final EthernetTracker.InterfaceObserver observer = captor.getValue();
+
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        reset(mFactory);
+        reset(mNetd);
+
+        final String testIface = "testtap1";
+        observer.onInterfaceAdded(testIface);
+        verify(mFactory, never()).addInterface(eq(testIface), anyString(), any(), any());
+        observer.onInterfaceRemoved(testIface);
+        verify(mFactory, never()).removeInterface(eq(testIface));
+
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel testIfaceParce =
+                createMockedIfaceParcel(testIface, testHwAddr);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(testIfaceParce);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        reset(mFactory);
+
+        final String testIface2 = "testtap2";
+        observer.onInterfaceRemoved(testIface2);
+        verify(mFactory, timeout(TIMEOUT_MS)).removeInterface(eq(testIface2));
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 5f9d1ff..e8c9637 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -32,6 +32,7 @@
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -64,6 +65,7 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -86,11 +88,19 @@
     private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
     private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
 
+    private static final int PID_SYSTEM = 1234;
+    private static final int PID_RED = 1235;
+    private static final int PID_BLUE = 1236;
+
     private static final int UID_RED = UserHandle.PER_USER_RANGE + 1;
     private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2;
     private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3;
     private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4;
 
+    private static final String PACKAGE_SYSTEM = "android";
+    private static final String PACKAGE_RED = "RED";
+    private static final String PACKAGE_BLUE = "BLUE";
+
     private static final long WAIT_TIMEOUT_MS = 500;
     private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
     private static final long BASE_BYTES = 7 * MB_IN_BYTES;
@@ -131,14 +141,15 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
         final DataUsageRequest requestByApp = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_RED , UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestByApp.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
         assertEquals(thresholdTooLowBytes, requestByApp.thresholdInBytes);
 
         // Verify the threshold requested by system uid won't be overridden.
         final DataUsageRequest requestBySystem = mStatsObservers.register(mContext, inputRequest,
-                mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                mUsageCallback, PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM,
+                NetworkStatsAccess.Level.DEVICE);
         assertTrue(requestBySystem.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
         assertEquals(1, requestBySystem.thresholdInBytes);
@@ -151,7 +162,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         assertEquals(highThresholdBytes, request.thresholdInBytes);
@@ -163,19 +174,64 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
         DataUsageRequest request1 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
         DataUsageRequest request2 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
         assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes);
     }
 
     @Test
+    public void testRegister_limit() throws Exception {
+        final DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
+
+        // Register maximum requests for red.
+        final ArrayList<DataUsageRequest> redRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
+            redRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Verify request exceeds the limit throws.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                    PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Verify another uid is not affected.
+        final ArrayList<DataUsageRequest> blueRequests = new ArrayList<>();
+        for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+            final DataUsageRequest returnedRequest =
+                    mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                            PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEVICE);
+            blueRequests.add(returnedRequest);
+            assertTrue(returnedRequest.requestId > 0);
+        }
+
+        // Again, verify request exceeds the limit throws for the 2nd uid.
+        assertThrows(IllegalStateException.class, () ->
+                mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+                        PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+        // Unregister all registered requests. Note that exceptions cannot be tested since
+        // unregister is handled in the handler thread.
+        for (final DataUsageRequest request : redRequests) {
+            mStatsObservers.unregister(request, UID_RED);
+        }
+        for (final DataUsageRequest request : blueRequests) {
+            mStatsObservers.unregister(request, UID_BLUE);
+        }
+    }
+
+    @Test
     public void testUnregister_unknownRequest_noop() throws Exception {
         DataUsageRequest unknownRequest = new DataUsageRequest(
                 123456 /* id */, sTemplateWifi, THRESHOLD_BYTES);
@@ -189,7 +245,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -209,7 +265,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEVICE);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -237,7 +293,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -261,7 +317,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -291,7 +347,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+                PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -322,7 +378,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.DEFAULT);
+                PID_RED, UID_RED, PACKAGE_SYSTEM , NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -355,7 +411,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -387,7 +443,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_BLUE, NetworkStatsAccess.Level.USER);
+                PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -420,7 +476,7 @@
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
         DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
-                UID_RED, NetworkStatsAccess.Level.USER);
+                PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);