Merge "[MS44.2] Add unit test for API to query tagged UID summary"
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6847c74..bc1d002 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -12,6 +12,7 @@
 maze@google.com
 nuccachen@google.com
 paulhu@google.com
+prohr@google.com
 reminv@google.com
 satk@google.com
 waynema@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 90312a4..6996ad9 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -28,6 +28,10 @@
   "postsubmit": [
     {
       "name": "TetheringPrivilegedTests"
+    },
+    // TODO: move to presubmit when known green.
+    {
+      "name": "bpf_existence_test"
     }
   ],
   "mainline-presubmit": [
@@ -54,6 +58,10 @@
     },
     {
       "name": "TetheringCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    // TODO: move to mainline-presubmit when known green.
+    {
+      "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "imports": [
@@ -61,6 +69,9 @@
       "path": "frameworks/base/core/java/android/net"
     },
     {
+      "path": "frameworks/opt/net/ethernet"
+    },
+    {
       "path": "packages/modules/NetworkStack"
     },
     {
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index bb435d8..2921e78 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,6 +21,7 @@
 java_defaults {
     name: "TetheringApiLevel",
     sdk_version: "module_current",
+    target_sdk_version: "31",
     min_sdk_version: "30",
 }
 
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index e6444f3..6deb345 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -19,14 +19,16 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.networkstack.tethering"
           android:sharedUserId="android.uid.networkstack">
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
 
     <!-- Permissions must be defined here, and not in the base manifest, as the tethering
          running in the system server process does not need any permission, and having
          privileged permissions added would cause crashes on startup unless they are also
-         added to the privileged permissions allowlist for that package. -->
+         added to the privileged permissions allowlist for that package. EntitlementManager
+         would set exact alarm but declare SCHEDULE_EXACT_ALARM is not necessary here because
+         privilege application would be in the allowlist. -->
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
     <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index a2bd1a5..d2188d1 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -48,7 +48,7 @@
 // Use with NetworkStackJarJarRules.
 android_library {
     name: "TetheringIntegrationTestsLatestSdkLib",
-    target_sdk_version: "30",
+    target_sdk_version: "31",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
@@ -128,7 +128,7 @@
     name: "TetheringCoverageTests",
     platform_apis: true,
     min_sdk_version: "30",
-    target_sdk_version: "30",
+    target_sdk_version: "31",
     test_suites: ["device-tests", "mts-tethering"],
     test_config: "AndroidTest_Coverage.xml",
     defaults: ["libnetworkstackutilsjni_deps"],
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index e51d531..18fd63b 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -22,7 +22,7 @@
     name: "MtsTetheringTestLatestSdk",
 
     min_sdk_version: "30",
-    target_sdk_version: "30",
+    target_sdk_version: "31",
 
     libs: [
         "android.test.base",
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 5150d39..ecd1a39 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -87,7 +87,7 @@
     static_libs: [
         "TetheringApiStableLib",
     ],
-    target_sdk_version: "30",
+    target_sdk_version: "31",
     visibility: [
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/framework/aidl-export/android/net/ProfileNetworkPreference.aidl b/framework/aidl-export/android/net/ProfileNetworkPreference.aidl
new file mode 100644
index 0000000..d7f2402
--- /dev/null
+++ b/framework/aidl-export/android/net/ProfileNetworkPreference.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable ProfileNetworkPreference;
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 81a1e5d..fea880a 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -19,11 +19,14 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
     method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network);
     method public void systemReady();
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkAllowList(int, boolean);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkDenyList(int, boolean);
     field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
     field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION";
     field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
@@ -149,6 +152,19 @@
     method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
   }
 
+  public final class ProfileNetworkPreference implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getPreference();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
+  }
+
+  public static final class ProfileNetworkPreference.Builder {
+    ctor public ProfileNetworkPreference.Builder();
+    method @NonNull public android.net.ProfileNetworkPreference build();
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
+  }
+
   public final class TestNetworkInterface implements android.os.Parcelable {
     ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
     method public int describeContents();
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index c21bcfa..465595f 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1101,7 +1101,7 @@
             PROFILE_NETWORK_PREFERENCE_DEFAULT,
             PROFILE_NETWORK_PREFERENCE_ENTERPRISE
     })
-    public @interface ProfileNetworkPreference {
+    public @interface ProfileNetworkPreferencePolicy {
     }
 
     /**
@@ -5461,6 +5461,8 @@
      * @param listener an optional listener to listen for completion of the operation.
      * @throws IllegalArgumentException if {@code profile} is not a valid user profile.
      * @throws SecurityException if missing the appropriate permissions.
+     * @deprecated Use {@link #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
+     * instead as it provides a more flexible API with more options.
      * @hide
      */
     // This function is for establishing per-profile default networking and can only be called by
@@ -5470,8 +5472,45 @@
     @SuppressLint({"UserHandle"})
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    @Deprecated
     public void setProfileNetworkPreference(@NonNull final UserHandle profile,
-            @ProfileNetworkPreference final int preference,
+            @ProfileNetworkPreferencePolicy final int preference,
+            @Nullable @CallbackExecutor final Executor executor,
+            @Nullable final Runnable listener) {
+
+        ProfileNetworkPreference.Builder preferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        preferenceBuilder.setPreference(preference);
+        setProfileNetworkPreferences(profile,
+                List.of(preferenceBuilder.build()), executor, listener);
+    }
+
+    /**
+     * Set a list of default network selection policies for a user profile.
+     *
+     * Calling this API with a user handle defines the entire policy for that user handle.
+     * It will overwrite any setting previously set for the same user profile,
+     * and not affect previously set settings for other handles.
+     *
+     * Call this API with an empty list to remove settings for this user profile.
+     *
+     * See {@link ProfileNetworkPreference} for more details on each preference
+     * parameter.
+     *
+     * @param profile the user profile for which the preference is being set.
+     * @param profileNetworkPreferences the list of profile network preferences for the
+     *        provided profile.
+     * @param executor an executor to execute the listener on. Optional if listener is null.
+     * @param listener an optional listener to listen for completion of the operation.
+     * @throws IllegalArgumentException if {@code profile} is not a valid user profile.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void setProfileNetworkPreferences(
+            @NonNull final UserHandle profile,
+            @NonNull List<ProfileNetworkPreference>  profileNetworkPreferences,
             @Nullable @CallbackExecutor final Executor executor,
             @Nullable final Runnable listener) {
         if (null != listener) {
@@ -5489,7 +5528,7 @@
             };
         }
         try {
-            mService.setProfileNetworkPreference(profile, preference, proxy);
+            mService.setProfileNetworkPreferences(profile, profileNetworkPreferences, proxy);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -5511,4 +5550,48 @@
     public static Range<Integer> getIpSecNetIdRange() {
         return new Range(TUN_INTF_NETID_START, TUN_INTF_NETID_START + TUN_INTF_NETID_RANGE - 1);
     }
+
+    /**
+     * Allow target application using metered network.
+     *
+     * @param uid uid of target app
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
+        try {
+            mService.updateMeteredNetworkAllowList(uid, add);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (IllegalStateException ie) {
+            throw ie;
+        }
+    }
+
+    /**
+     * Disallow target application using metered network.
+     *
+     * @param uid uid of target app
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
+        try {
+            mService.updateMeteredNetworkDenyList(uid, add);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (IllegalStateException ie) {
+            throw ie;
+        }
+    }
 }
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 50ec781..5740d85 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -36,6 +36,7 @@
 import android.net.NetworkState;
 import android.net.NetworkStateSnapshot;
 import android.net.OemNetworkPreferences;
+import android.net.ProfileNetworkPreference;
 import android.net.ProxyInfo;
 import android.net.UidRange;
 import android.net.QosSocketInfo;
@@ -218,7 +219,8 @@
     void setOemNetworkPreference(in OemNetworkPreferences preference,
             in IOnCompleteListener listener);
 
-    void setProfileNetworkPreference(in UserHandle profile, int preference,
+    void setProfileNetworkPreferences(in UserHandle profile,
+            in List<ProfileNetworkPreference>  preferences,
             in IOnCompleteListener listener);
 
     int getRestrictBackgroundStatusByCaller();
@@ -228,4 +230,8 @@
     void unofferNetwork(in INetworkOfferCallback callback);
 
     void setTestAllowBadWifiUntil(long timeMs);
+
+    void updateMeteredNetworkAllowList(int uid, boolean add);
+
+    void updateMeteredNetworkDenyList(int uid, boolean add);
 }
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
new file mode 100644
index 0000000..d580209
--- /dev/null
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.ConnectivityManager.ProfileNetworkPreferencePolicy;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Network preferences to be set for the user profile
+ * {@link ProfileNetworkPreferencePolicy}.
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class ProfileNetworkPreference implements Parcelable {
+    private final @ProfileNetworkPreferencePolicy int mPreference;
+
+    private ProfileNetworkPreference(int preference) {
+        mPreference = preference;
+    }
+
+    private ProfileNetworkPreference(Parcel in) {
+        mPreference = in.readInt();
+    }
+
+    public int getPreference() {
+        return mPreference;
+    }
+
+    @Override
+    public String toString() {
+        return "ProfileNetworkPreference{"
+                + "mPreference=" + getPreference()
+                + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        final ProfileNetworkPreference that = (ProfileNetworkPreference) o;
+        return mPreference == that.mPreference;
+    }
+
+    @Override
+    public int hashCode() {
+        return (mPreference);
+    }
+
+    /**
+     * Builder used to create {@link ProfileNetworkPreference} objects.
+     * Specify the preferred Network preference
+     */
+    public static final class Builder {
+        private @ProfileNetworkPreferencePolicy int mPreference =
+                PROFILE_NETWORK_PREFERENCE_DEFAULT;
+
+        /**
+         * Constructs an empty Builder with PROFILE_NETWORK_PREFERENCE_DEFAULT profile preference
+         */
+        public Builder() {}
+
+        /**
+         * Set the profile network preference
+         * See the documentation for the individual preferences for a description of the supported
+         * behaviors. Default value is PROFILE_NETWORK_PREFERENCE_DEFAULT.
+         * @param preference  the desired network preference to use
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setPreference(@ProfileNetworkPreferencePolicy int preference) {
+            mPreference = preference;
+            return this;
+        }
+        /**
+         * Returns an instance of {@link ProfileNetworkPreference} created from the
+         * fields set on this builder.
+         */
+        @NonNull
+        public ProfileNetworkPreference  build() {
+            return new ProfileNetworkPreference(mPreference);
+        }
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        dest.writeInt(mPreference);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<ProfileNetworkPreference> CREATOR =
+            new Creator<ProfileNetworkPreference>() {
+                @Override
+                public ProfileNetworkPreference[] newArray(int size) {
+                    return new ProfileNetworkPreference[size];
+                }
+
+                @Override
+                public ProfileNetworkPreference  createFromParcel(
+                        @NonNull android.os.Parcel in) {
+                    return new ProfileNetworkPreference(in);
+                }
+            };
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a59b5d6..578fabe 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -166,6 +166,7 @@
 import android.net.NetworkWatchlistManager;
 import android.net.OemNetworkPreferences;
 import android.net.PrivateDnsConfigParcel;
+import android.net.ProfileNetworkPreference;
 import android.net.ProxyInfo;
 import android.net.QosCallbackException;
 import android.net.QosFilter;
@@ -258,7 +259,7 @@
 import com.android.server.connectivity.NetworkOffer;
 import com.android.server.connectivity.NetworkRanker;
 import com.android.server.connectivity.PermissionMonitor;
-import com.android.server.connectivity.ProfileNetworkPreferences;
+import com.android.server.connectivity.ProfileNetworkPreferenceList;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
 
@@ -4041,11 +4042,11 @@
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
                         INetd.PERMISSION_NONE,
                         (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
-                        getVpnType(nai));
+                        getVpnType(nai), /*excludeLocalRoutes=*/ false);
             } else {
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
-                        VpnManager.TYPE_VPN_NONE);
+                        VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
             }
             mNetd.networkCreate(config);
             mDnsResolver.createNetworkCache(nai.network.getNetId());
@@ -5046,9 +5047,10 @@
                     break;
                 }
                 case EVENT_SET_PROFILE_NETWORK_PREFERENCE: {
-                    final Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener> arg =
-                            (Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener>)
-                                    msg.obj;
+                    final Pair<List<ProfileNetworkPreferenceList.Preference>,
+                            IOnCompleteListener> arg =
+                            (Pair<List<ProfileNetworkPreferenceList.Preference>,
+                                    IOnCompleteListener>) msg.obj;
                     handleSetProfileNetworkPreference(arg.first, arg.second);
                     break;
                 }
@@ -5671,7 +5673,8 @@
     private void onUserRemoved(@NonNull final UserHandle user) {
         mPermissionMonitor.onUserRemoved(user);
         // If there was a network preference for this user, remove it.
-        handleSetProfileNetworkPreference(new ProfileNetworkPreferences.Preference(user, null),
+        handleSetProfileNetworkPreference(
+                List.of(new ProfileNetworkPreferenceList.Preference(user, null)),
                 null /* listener */);
         if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
             handleSetOemNetworkPreference(mOemNetworkPreferences, null);
@@ -6605,7 +6608,8 @@
     // Current per-profile network preferences. This object follows the same threading rules as
     // the OEM network preferences above.
     @NonNull
-    private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences();
+    private ProfileNetworkPreferenceList mProfileNetworkPreferences =
+            new ProfileNetworkPreferenceList();
 
     // A set of UIDs that should use mobile data preferentially if available. This object follows
     // the same threading rules as the OEM network preferences above.
@@ -10103,19 +10107,26 @@
      * See the documentation for the individual preferences for a description of the supported
      * behaviors.
      *
-     * @param profile the profile concerned.
-     * @param preference the preference for this profile, as one of the PROFILE_NETWORK_PREFERENCE_*
-     *                   constants.
+     * @param profile the user profile for whih the preference is being set.
+     * @param preferences the list of profile network preferences for the
+     *        provided profile.
      * @param listener an optional listener to listen for completion of the operation.
      */
     @Override
-    public void setProfileNetworkPreference(@NonNull final UserHandle profile,
-            @ConnectivityManager.ProfileNetworkPreference final int preference,
+    public void setProfileNetworkPreferences(
+            @NonNull final UserHandle profile,
+            @NonNull List<ProfileNetworkPreference> preferences,
             @Nullable final IOnCompleteListener listener) {
+        Objects.requireNonNull(preferences);
         Objects.requireNonNull(profile);
+
+        if (preferences.size() == 0) {
+            preferences.add((new ProfileNetworkPreference.Builder()).build());
+        }
+
         PermissionUtils.enforceNetworkStackPermission(mContext);
         if (DBG) {
-            log("setProfileNetworkPreference " + profile + " to " + preference);
+            log("setProfileNetworkPreferences " + profile + " to " + preferences);
         }
         if (profile.getIdentifier() < 0) {
             throw new IllegalArgumentException("Must explicitly specify a user handle ("
@@ -10126,23 +10137,29 @@
             throw new IllegalArgumentException("Profile must be a managed profile");
         }
 
-        final NetworkCapabilities nc;
-        switch (preference) {
-            case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
-                nc = null;
-                break;
-            case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
-                final UidRange uids = UidRange.createForUser(profile);
-                nc = createDefaultNetworkCapabilitiesForUidRange(uids);
-                nc.addCapability(NET_CAPABILITY_ENTERPRISE);
-                nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
-                break;
-            default:
-                throw new IllegalArgumentException(
-                        "Invalid preference in setProfileNetworkPreference");
+        final List<ProfileNetworkPreferenceList.Preference> preferenceList =
+                new ArrayList<ProfileNetworkPreferenceList.Preference>();
+        for (final ProfileNetworkPreference preference : preferences) {
+            final NetworkCapabilities nc;
+            switch (preference.getPreference()) {
+                case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
+                    nc = null;
+                    break;
+                case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                    final UidRange uids = UidRange.createForUser(profile);
+                    nc = createDefaultNetworkCapabilitiesForUidRange(uids);
+                    nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+                    nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Invalid preference in setProfileNetworkPreferences");
+            }
+            preferenceList.add(
+                    new ProfileNetworkPreferenceList.Preference(profile, nc));
         }
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
-                new Pair<>(new ProfileNetworkPreferences.Preference(profile, nc), listener)));
+                new Pair<>(preferenceList, listener)));
     }
 
     private void validateNetworkCapabilitiesOfProfileNetworkPreference(
@@ -10152,9 +10169,9 @@
     }
 
     private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
-            @NonNull final ProfileNetworkPreferences prefs) {
+            @NonNull final ProfileNetworkPreferenceList prefs) {
         final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
-        for (final ProfileNetworkPreferences.Preference pref : prefs.preferences) {
+        for (final ProfileNetworkPreferenceList.Preference pref : prefs.preferences) {
             // The NRI for a user should be comprised of two layers:
             // - The request for the capabilities
             // - The request for the default network, for fallback. Create an image of it to
@@ -10175,11 +10192,12 @@
     }
 
     private void handleSetProfileNetworkPreference(
-            @NonNull final ProfileNetworkPreferences.Preference preference,
+            @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
             @Nullable final IOnCompleteListener listener) {
-        validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
-
-        mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
+        for (final ProfileNetworkPreferenceList.Preference preference : preferenceList) {
+            validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
+            mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
+        }
         removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_PROFILE);
         addPerAppDefaultNetworkRequests(
                 createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences));
@@ -10562,4 +10580,34 @@
             return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
         }
     }
+
+    @Override
+    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
+        enforceNetworkStackOrSettingsPermission();
+
+        try {
+            if (add) {
+                mNetd.bandwidthAddNiceApp(uid);
+            } else {
+                mNetd.bandwidthRemoveNiceApp(uid);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
+        enforceNetworkStackOrSettingsPermission();
+
+        try {
+            if (add) {
+                mNetd.bandwidthAddNaughtyApp(uid);
+            } else {
+                mNetd.bandwidthRemoveNaughtyApp(uid);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
similarity index 89%
rename from service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
rename to service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
index dd2815d..b345ede 100644
--- a/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -30,7 +30,7 @@
  *
  * A given profile can only have one preference.
  */
-public class ProfileNetworkPreferences {
+public class ProfileNetworkPreferenceList {
     /**
      * A single preference, as it applies to a given user profile.
      */
@@ -53,11 +53,11 @@
 
     @NonNull public final List<Preference> preferences;
 
-    public ProfileNetworkPreferences() {
+    public ProfileNetworkPreferenceList() {
         preferences = Collections.EMPTY_LIST;
     }
 
-    private ProfileNetworkPreferences(@NonNull final List<Preference> list) {
+    private ProfileNetworkPreferenceList(@NonNull final List<Preference> list) {
         preferences = Collections.unmodifiableList(list);
     }
 
@@ -68,7 +68,7 @@
      * preference. Passing a Preference object containing a null capabilities object is equivalent
      * to (and indeed, implemented as) removing the preference for this user.
      */
-    public ProfileNetworkPreferences plus(@NonNull final Preference pref) {
+    public ProfileNetworkPreferenceList plus(@NonNull final Preference pref) {
         final ArrayList<Preference> newPrefs = new ArrayList<>();
         for (final Preference existingPref : preferences) {
             if (!existingPref.user.equals(pref.user)) {
@@ -78,7 +78,7 @@
         if (null != pref.capabilities) {
             newPrefs.add(pref);
         }
-        return new ProfileNetworkPreferences(newPrefs);
+        return new ProfileNetworkPreferenceList(newPrefs);
     }
 
     public boolean isEmpty() {
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f72a458..b684068 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -25,6 +25,9 @@
         "cts-tradefed",
         "tradefed",
     ],
+    static_libs: [
+        "modules-utils-build-testing",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 63572c3..12e7d33 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -18,12 +18,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test_helper_app {
-    name: "CtsHostsideNetworkTestsApp",
-    defaults: [
-        "cts_support_defaults",
-        "framework-connectivity-test-defaults",
-    ],
+java_defaults {
+    name: "CtsHostsideNetworkTestsAppDefaults",
     platform_apis: true,
     static_libs: [
         "CtsHostsideNetworkTestsAidl",
@@ -48,3 +44,28 @@
         "sts",
     ],
 }
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+        "CtsHostsideNetworkTestsAppDefaults",
+    ],
+    static_libs: [
+        "NetworkStackApiStableShims",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsAppNext",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+        "CtsHostsideNetworkTestsAppDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
+    static_libs: [
+        "NetworkStackApiCurrentShims",
+    ],
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index 855abf7..449454e 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -18,17 +18,26 @@
 
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.IpPrefix;
 import android.net.Network;
+import android.net.NetworkUtils;
 import android.net.ProxyInfo;
 import android.net.VpnService;
 import android.os.ParcelFileDescriptor;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
 
 import java.io.IOException;
 import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 public class MyVpnService extends VpnService {
 
@@ -67,39 +76,62 @@
                 (underlyingNetworks != null) ? underlyingNetworks.toArray(new Network[0]) : null);
     }
 
-    private void start(String packageName, Intent intent) {
-        Builder builder = new Builder();
+    private String parseIpAndMaskListArgument(String packageName, Intent intent, String argName,
+            BiConsumer<InetAddress, Integer> consumer) {
+        final String addresses = intent.getStringExtra(packageName + "." + argName);
 
-        String addresses = intent.getStringExtra(packageName + ".addresses");
-        if (addresses != null) {
-            String[] addressArray = addresses.split(",");
-            for (int i = 0; i < addressArray.length; i++) {
-                String[] prefixAndMask = addressArray[i].split("/");
-                try {
-                    InetAddress address = InetAddress.getByName(prefixAndMask[0]);
-                    int prefixLength = Integer.parseInt(prefixAndMask[1]);
-                    builder.addAddress(address, prefixLength);
-                } catch (UnknownHostException|NumberFormatException|
-                         ArrayIndexOutOfBoundsException e) {
-                    continue;
-                }
-            }
+        if (TextUtils.isEmpty(addresses)) {
+            return null;
         }
 
-        String routes = intent.getStringExtra(packageName + ".routes");
-        if (routes != null) {
-            String[] routeArray = routes.split(",");
-            for (int i = 0; i < routeArray.length; i++) {
-                String[] prefixAndMask = routeArray[i].split("/");
+        final String[] addressesArray = addresses.split(",");
+        for (String address : addressesArray) {
+            final Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+            consumer.accept(ipAndMask.first, ipAndMask.second);
+        }
+
+        return addresses;
+    }
+
+    private String parseIpPrefixListArgument(String packageName, Intent intent, String argName,
+            Consumer<IpPrefix> consumer) {
+        return parseIpAndMaskListArgument(packageName, intent, argName,
+                (inetAddress, prefixLength) -> consumer.accept(
+                        new IpPrefix(inetAddress, prefixLength)));
+    }
+
+    private void start(String packageName, Intent intent) {
+        Builder builder = new Builder();
+        VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+
+        final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
+                builder::addAddress);
+
+        String addedRoutes;
+        if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
+                false)) {
+            addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
                 try {
-                    InetAddress address = InetAddress.getByName(prefixAndMask[0]);
-                    int prefixLength = Integer.parseInt(prefixAndMask[1]);
-                    builder.addRoute(address, prefixLength);
-                } catch (UnknownHostException|NumberFormatException|
-                         ArrayIndexOutOfBoundsException e) {
-                    continue;
+                    vpnServiceBuilderShim.addRoute(builder, prefix);
+                } catch (UnsupportedApiLevelException e) {
+                    throw new RuntimeException(e);
                 }
-            }
+            });
+        } else {
+            addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
+                    builder::addRoute);
+        }
+
+        String excludedRoutes = null;
+        if (SdkLevel.isAtLeastT()) {
+            excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
+                    (prefix) -> {
+                        try {
+                            vpnServiceBuilderShim.excludeRoute(builder, prefix);
+                        } catch (UnsupportedApiLevelException e) {
+                            throw new RuntimeException(e);
+                        }
+                    });
         }
 
         String allowed = intent.getStringExtra(packageName + ".allowedapplications");
@@ -152,7 +184,8 @@
 
         Log.i(TAG, "Establishing VPN,"
                 + " addresses=" + addresses
-                + " routes=" + routes
+                + " addedRoutes=" + addedRoutes
+                + " excludedRoutes=" + excludedRoutes
                 + " allowedApplications=" + allowed
                 + " disallowedApplications=" + disallowed);
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 65ea4b1..916b566 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -298,28 +298,54 @@
         mActivity.startService(intent);
     }
 
-    private void establishVpn(String[] addresses, String[] routes, String allowedApplications,
-            String disallowedApplications, @Nullable ProxyInfo proxyInfo,
-            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+    private void establishVpn(String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo, @Nullable ArrayList<Network> underlyingNetworks,
+            boolean isAlwaysMetered, boolean addRoutesByIpPrefix)
             throws Exception {
         final Intent intent = new Intent(mActivity, MyVpnService.class)
                 .putExtra(mPackageName + ".cmd", MyVpnService.CMD_CONNECT)
                 .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
                 .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+                .putExtra(mPackageName + ".excludedRoutes", TextUtils.join(",", excludedRoutes))
                 .putExtra(mPackageName + ".allowedapplications", allowedApplications)
                 .putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
                 .putExtra(mPackageName + ".httpProxy", proxyInfo)
                 .putParcelableArrayListExtra(
                         mPackageName + ".underlyingNetworks", underlyingNetworks)
-                .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+                .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered)
+                .putExtra(mPackageName + ".addRoutesByIpPrefix", addRoutesByIpPrefix);
         mActivity.startService(intent);
     }
 
     // TODO: Consider replacing arguments with a Builder.
     private void startVpn(
-        String[] addresses, String[] routes, String allowedApplications,
-        String disallowedApplications, @Nullable ProxyInfo proxyInfo,
-        @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
+            String[] addresses, String[] routes, String allowedApplications,
+            String disallowedApplications, @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+            throws Exception {
+        startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+                disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered);
+    }
+
+    private void startVpn(
+            String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+            throws Exception {
+        startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+                disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered,
+                false /* addRoutesByIpPrefix */);
+    }
+
+    private void startVpn(
+            String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered,
+            boolean addRoutesByIpPrefix)
+            throws Exception {
         prepareVpn();
 
         // Register a callback so we will be notified when our VPN comes up.
@@ -340,8 +366,8 @@
         mCM.registerNetworkCallback(request, mCallback);  // Unregistered in tearDown.
 
         // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
-        establishVpn(addresses, routes, allowedApplications, disallowedApplications, proxyInfo,
-                underlyingNetworks, isAlwaysMetered);
+        establishVpn(addresses, routes, excludedRoutes, allowedApplications, disallowedApplications,
+                proxyInfo, underlyingNetworks, isAlwaysMetered, addRoutesByIpPrefix);
         synchronized (mLock) {
             if (mNetwork == null) {
                  Log.i(TAG, "bf mLock");
@@ -561,6 +587,12 @@
     }
 
     private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+        checkUdpEcho(to, expectedFrom, expectedFrom != null);
+    }
+
+    private void checkUdpEcho(String to, String expectedFrom,
+            boolean expectConnectionOwnerIsVisible)
+            throws IOException {
         DatagramSocket s;
         InetAddress address = InetAddress.getByName(to);
         if (address instanceof Inet6Address) {  // http://b/18094870
@@ -584,7 +616,7 @@
         try {
             if (expectedFrom != null) {
                 s.send(p);
-                checkConnectionOwnerUidUdp(s, true);
+                checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
                 s.receive(p);
                 MoreAsserts.assertEquals(data, p.getData());
             } else {
@@ -593,7 +625,7 @@
                     s.receive(p);
                     fail("Received unexpected reply");
                 } catch (IOException expected) {
-                    checkConnectionOwnerUidUdp(s, false);
+                    checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
                 }
             }
         } finally {
@@ -601,19 +633,38 @@
         }
     }
 
+    private void checkTrafficOnVpn(String destination) throws Exception {
+        final InetAddress address = InetAddress.getByName(destination);
+
+        if (address instanceof Inet6Address) {
+            checkUdpEcho(destination, "2001:db8:1:2::ffe");
+            checkTcpReflection(destination, "2001:db8:1:2::ffe");
+            checkPing(destination);
+        } else {
+            checkUdpEcho(destination, "192.0.2.2");
+            checkTcpReflection(destination, "192.0.2.2");
+        }
+
+    }
+
+    private void checkNoTrafficOnVpn(String destination) throws IOException {
+        checkUdpEcho(destination, null);
+        checkTcpReflection(destination, null);
+    }
+
     private void checkTrafficOnVpn() throws Exception {
-        checkUdpEcho("192.0.2.251", "192.0.2.2");
-        checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
-        checkPing("2001:db8:dead:beef::f00");
-        checkTcpReflection("192.0.2.252", "192.0.2.2");
-        checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+        checkTrafficOnVpn("192.0.2.251");
+        checkTrafficOnVpn("2001:db8:dead:beef::f00");
     }
 
     private void checkNoTrafficOnVpn() throws Exception {
-        checkUdpEcho("192.0.2.251", null);
-        checkUdpEcho("2001:db8:dead:beef::f00", null);
-        checkTcpReflection("192.0.2.252", null);
-        checkTcpReflection("2001:db8:dead:beef::f00", null);
+        checkNoTrafficOnVpn("192.0.2.251");
+        checkNoTrafficOnVpn("2001:db8:dead:beef::f00");
+    }
+
+    private void checkTrafficBypassesVpn(String destination) throws Exception {
+        checkUdpEcho(destination, null, true /* expectVpnOwnedConnection */);
+        checkTcpReflection(destination, null);
     }
 
     private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
@@ -842,7 +893,7 @@
 
     @Test
     public void testDefault() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
         if (!SdkLevel.isAtLeastS() && (
                 SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
                         || SystemProperties.getInt("service.adb.tcp.port", -1) > -1)) {
@@ -935,7 +986,7 @@
 
     @Test
     public void testAppAllowed() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
 
         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
 
@@ -956,7 +1007,7 @@
 
     @Test
     public void testAppDisallowed() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
 
         FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
         FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -974,9 +1025,9 @@
         }
         Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
-                 new String[] {"192.0.2.0/24", "2001:db8::/32"},
-                 "", disallowedApps, null, null /* underlyingNetworks */,
-                 false /* isAlwaysMetered */);
+                new String[] {"192.0.2.0/24", "2001:db8::/32"},
+                "", disallowedApps, null, null /* underlyingNetworks */,
+                false /* isAlwaysMetered */);
 
         assertSocketStillOpen(localFd, TEST_HOST);
         assertSocketStillOpen(remoteFd, TEST_HOST);
@@ -989,8 +1040,76 @@
     }
 
     @Test
+    public void testExcludedRoutes() throws Exception {
+        assumeTrue(supportedHardware());
+        assumeTrue(SdkLevel.isAtLeastT());
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+        // Excluded routes should bypass VPN.
+        checkTrafficBypassesVpn("192.0.2.1");
+        checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+        // Other routes should go through VPN, since default routes are included.
+        checkTrafficOnVpn("198.51.100.1");
+        checkTrafficOnVpn("2002:db8::1");
+    }
+
+    @Test
+    public void testIncludedRoutes() throws Exception {
+        assumeTrue(supportedHardware());
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* routes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+        // Included routes should go through VPN.
+        checkTrafficOnVpn("192.0.2.1");
+        checkTrafficOnVpn("2001:db8:dead:beef::f00");
+        // Other routes should bypass VPN, since default routes are not included.
+        checkTrafficBypassesVpn("198.51.100.1");
+        checkTrafficBypassesVpn("2002:db8::1");
+    }
+
+    @Test
+    public void testInterleavedRoutes() throws Exception {
+        assumeTrue(supportedHardware());
+        assumeTrue(SdkLevel.isAtLeastT());
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"0.0.0.0/0", "192.0.2.0/32", "::/0", "2001:db8::/128"} /* routes */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */,
+                true /* addRoutesByIpPrefix */);
+
+        // Excluded routes should bypass VPN.
+        checkTrafficBypassesVpn("192.0.2.1");
+        checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+
+        // Included routes inside excluded routes should go through VPN, since the longest common
+        // prefix precedes.
+        checkTrafficOnVpn("192.0.2.0");
+        checkTrafficOnVpn("2001:db8::");
+
+        // Other routes should go through VPN, since default routes are included.
+        checkTrafficOnVpn("198.51.100.1");
+        checkTrafficOnVpn("2002:db8::1");
+    }
+
+    @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
 
         DatagramSocket s;
         InetAddress address = InetAddress.getByName("localhost");
@@ -1012,7 +1131,7 @@
 
     @Test
     public void testSetProxy() throws  Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
         // Receiver for the proxy change broadcast.
         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
@@ -1052,7 +1171,7 @@
 
     @Test
     public void testSetProxyDisallowedApps() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
 
         String disallowedApps = mPackageName;
@@ -1078,7 +1197,7 @@
 
     @Test
     public void testNoProxy() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
         proxyBroadcastReceiver.register();
@@ -1113,7 +1232,7 @@
 
     @Test
     public void testBindToNetworkWithProxy() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
         String allowedApps = mPackageName;
         Network initialNetwork = mCM.getActiveNetwork();
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1355,7 +1474,7 @@
     }
 
     private void maybeExpectVpnTransportInfo(Network network) {
-        if (!SdkLevel.isAtLeastS()) return;
+        assumeTrue(SdkLevel.isAtLeastS());
         final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
         assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
         final TransportInfo ti = vpnNc.getTransportInfo();
@@ -1407,7 +1526,7 @@
      */
     @Test
     public void testDownloadWithDownloadManagerDisallowed() throws Exception {
-        if (!supportedHardware()) return;
+        assumeTrue(supportedHardware());
 
         // Start a VPN with DownloadManager package in disallowed list.
         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 89c79d3..cc07fd1 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -20,6 +20,7 @@
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.modules.utils.build.testing.DeviceSdkLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.CollectingTestListener;
@@ -42,6 +43,7 @@
     protected static final String TAG = "HostsideNetworkTests";
     protected static final String TEST_PKG = "com.android.cts.net.hostside";
     protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+    protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
     protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
     protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
 
@@ -65,8 +67,12 @@
         assertNotNull(mAbi);
         assertNotNull(mCtsBuild);
 
+        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
+        String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT
+                : TEST_APK;
+
         uninstallPackage(TEST_PKG, false);
-        installPackage(TEST_APK);
+        installPackage(testApk);
     }
 
     @Override
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 180015d..3821f87 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -104,4 +104,16 @@
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
                 "testDownloadWithDownloadManagerDisallowed");
     }
+
+    public void testExcludedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testExcludedRoutes");
+    }
+
+    public void testIncludedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testIncludedRoutes");
+    }
+
+    public void testInterleavedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
deleted file mode 100644
index 2bcfdc3..0000000
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.java
+++ /dev/null
@@ -1,594 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.cts;
-
-import android.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.Arrays;
-import java.util.Random;
-import java.util.List;
-import java.util.ArrayList;
-
-@AppModeFull(reason = "Socket cannot bind in instant app mode")
-public class NsdManagerTest extends AndroidTestCase {
-
-    private static final String TAG = "NsdManagerTest";
-    private static final String SERVICE_TYPE = "_nmt._tcp";
-    private static final int TIMEOUT = 2000;
-
-    private static final boolean DBG = false;
-
-    NsdManager mNsdManager;
-
-    NsdManager.RegistrationListener mRegistrationListener;
-    NsdManager.DiscoveryListener mDiscoveryListener;
-    NsdManager.ResolveListener mResolveListener;
-    private NsdServiceInfo mResolvedService;
-
-    public NsdManagerTest() {
-        initRegistrationListener();
-        initDiscoveryListener();
-        initResolveListener();
-    }
-
-    private void initRegistrationListener() {
-        mRegistrationListener = new NsdManager.RegistrationListener() {
-            @Override
-            public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onRegistrationFailed", errorCode);
-            }
-
-            @Override
-            public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onUnregistrationFailed", errorCode);
-            }
-
-            @Override
-            public void onServiceRegistered(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceRegistered", serviceInfo);
-            }
-
-            @Override
-            public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceUnregistered", serviceInfo);
-            }
-        };
-    }
-
-    private void initDiscoveryListener() {
-        mDiscoveryListener = new NsdManager.DiscoveryListener() {
-            @Override
-            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
-                setEvent("onStartDiscoveryFailed", errorCode);
-            }
-
-            @Override
-            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
-                setEvent("onStopDiscoveryFailed", errorCode);
-            }
-
-            @Override
-            public void onDiscoveryStarted(String serviceType) {
-                NsdServiceInfo info = new NsdServiceInfo();
-                info.setServiceType(serviceType);
-                setEvent("onDiscoveryStarted", info);
-            }
-
-            @Override
-            public void onDiscoveryStopped(String serviceType) {
-                NsdServiceInfo info = new NsdServiceInfo();
-                info.setServiceType(serviceType);
-                setEvent("onDiscoveryStopped", info);
-            }
-
-            @Override
-            public void onServiceFound(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceFound", serviceInfo);
-            }
-
-            @Override
-            public void onServiceLost(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceLost", serviceInfo);
-            }
-        };
-    }
-
-    private void initResolveListener() {
-        mResolveListener = new NsdManager.ResolveListener() {
-            @Override
-            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onResolveFailed", errorCode);
-            }
-
-            @Override
-            public void onServiceResolved(NsdServiceInfo serviceInfo) {
-                mResolvedService = serviceInfo;
-                setEvent("onServiceResolved", serviceInfo);
-            }
-        };
-    }
-
-
-
-    private final class EventData {
-        EventData(String callbackName, NsdServiceInfo info) {
-            mCallbackName = callbackName;
-            mSucceeded = true;
-            mErrorCode = 0;
-            mInfo = info;
-        }
-        EventData(String callbackName, int errorCode) {
-            mCallbackName = callbackName;
-            mSucceeded = false;
-            mErrorCode = errorCode;
-            mInfo = null;
-        }
-        private final String mCallbackName;
-        private final boolean mSucceeded;
-        private final int mErrorCode;
-        private final NsdServiceInfo mInfo;
-    }
-
-    private final List<EventData> mEventCache = new ArrayList<EventData>();
-
-    private void setEvent(String callbackName, int errorCode) {
-        if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
-        EventData eventData = new EventData(callbackName, errorCode);
-        synchronized (mEventCache) {
-            mEventCache.add(eventData);
-            mEventCache.notify();
-        }
-    }
-
-    private void setEvent(String callbackName, NsdServiceInfo info) {
-        if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
-        EventData eventData = new EventData(callbackName, info);
-        synchronized (mEventCache) {
-            mEventCache.add(eventData);
-            mEventCache.notify();
-        }
-    }
-
-    void clearEventCache() {
-        synchronized(mEventCache) {
-            mEventCache.clear();
-        }
-    }
-
-    int eventCacheSize() {
-        synchronized(mEventCache) {
-            return mEventCache.size();
-        }
-    }
-
-    private int mWaitId = 0;
-    private EventData waitForCallback(String callbackName) {
-
-        synchronized(mEventCache) {
-
-            mWaitId ++;
-            if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
-
-            try {
-                long startTime = android.os.SystemClock.uptimeMillis();
-                long elapsedTime = 0;
-                int index = 0;
-                while (elapsedTime < TIMEOUT ) {
-                    // first check if we've received that event
-                    for (; index < mEventCache.size(); index++) {
-                        EventData e = mEventCache.get(index);
-                        if (e.mCallbackName.equals(callbackName)) {
-                            if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
-                            return e;
-                        }
-                    }
-
-                    // Not yet received, just wait
-                    mEventCache.wait(TIMEOUT - elapsedTime);
-                    elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
-                }
-                // we exited the loop because of TIMEOUT; fail the call
-                if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
-                return null;
-            } catch (InterruptedException e) {
-                return null;                       // wait timed out!
-            }
-        }
-    }
-
-    private EventData waitForNewEvents() throws InterruptedException {
-        if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
-
-        long startTime = android.os.SystemClock.uptimeMillis();
-        long elapsedTime = 0;
-        synchronized (mEventCache) {
-            int index = mEventCache.size();
-            while (elapsedTime < TIMEOUT ) {
-                // first check if we've received that event
-                for (; index < mEventCache.size(); index++) {
-                    EventData e = mEventCache.get(index);
-                    return e;
-                }
-
-                // Not yet received, just wait
-                mEventCache.wait(TIMEOUT - elapsedTime);
-                elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
-            }
-        }
-
-        return null;
-    }
-
-    private String mServiceName;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        if (DBG) Log.d(TAG, "Setup test ...");
-        mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
-
-        Random rand = new Random();
-        mServiceName = new String("NsdTest");
-        for (int i = 0; i < 4; i++) {
-            mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
-        }
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        if (DBG) Log.d(TAG, "Tear down test ...");
-        super.tearDown();
-    }
-
-    public void testNDSManager() throws Exception {
-        EventData lastEvent = null;
-
-        if (DBG) Log.d(TAG, "Starting test ...");
-
-        NsdServiceInfo si = new NsdServiceInfo();
-        si.setServiceType(SERVICE_TYPE);
-        si.setServiceName(mServiceName);
-
-        byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
-        String String256 = "1_________2_________3_________4_________5_________6_________" +
-                 "7_________8_________9_________10________11________12________13________" +
-                 "14________15________16________17________18________19________20________" +
-                 "21________22________23________24________25________123456";
-
-        // Illegal attributes
-        try {
-            si.setAttribute(null, (String) null);
-            fail("Could set null key");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("", (String) null);
-            fail("Could set empty key");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(String256, (String) null);
-            fail("Could set key with 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("key", String256.substring(3));
-            fail("Could set key+value combination with more than 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("key", String256.substring(4));
-            fail("Could set key+value combination with 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(new String(new byte[]{0x19}), (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("=", (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(new String(new byte[]{0x7F}), (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        // Allowed attributes
-        si.setAttribute("booleanAttr", (String) null);
-        si.setAttribute("keyValueAttr", "value");
-        si.setAttribute("keyEqualsAttr", "=");
-        si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
-        si.setAttribute("binaryDataAttr", testByteArray);
-        si.setAttribute("nullBinaryDataAttr", (byte[]) null);
-        si.setAttribute("emptyBinaryDataAttr", new byte[]{});
-        si.setAttribute("longkey", String256.substring(9));
-
-        ServerSocket socket;
-        int localPort;
-
-        try {
-            socket = new ServerSocket(0);
-            localPort = socket.getLocalPort();
-            si.setPort(localPort);
-        } catch (IOException e) {
-            if (DBG) Log.d(TAG, "Could not open a local socket");
-            assertTrue(false);
-            return;
-        }
-
-        if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
-
-        clearEventCache();
-
-        mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-        lastEvent = waitForCallback("onServiceRegistered");                 // id = 1
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(eventCacheSize() == 1);
-
-        // We may not always get the name that we tried to register;
-        // This events tells us the name that was registered.
-        String registeredName = lastEvent.mInfo.getServiceName();
-        si.setServiceName(registeredName);
-
-        clearEventCache();
-
-        mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
-                mDiscoveryListener);
-
-        // Expect discovery started
-        lastEvent = waitForCallback("onDiscoveryStarted");                  // id = 2
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        // Remove this event, so accounting becomes easier later
-        synchronized (mEventCache) {
-            mEventCache.remove(lastEvent);
-        }
-
-        // Expect a service record to be discovered (and filter the ones
-        // that are unrelated to this test)
-        boolean found = false;
-        for (int i = 0; i < 32; i++) {
-
-            lastEvent = waitForCallback("onServiceFound");                  // id = 3
-            if (lastEvent == null) {
-                // no more onServiceFound events are being reported!
-                break;
-            }
-
-            assertTrue(lastEvent.mSucceeded);
-
-            if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                    lastEvent.mInfo.getServiceName());
-
-            if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
-                // Save it, as it will get overwritten with new serviceFound events
-                si = lastEvent.mInfo;
-                found = true;
-            }
-
-            // Remove this event from the event cache, so it won't be found by subsequent
-            // calls to waitForCallback
-            synchronized (mEventCache) {
-                mEventCache.remove(lastEvent);
-            }
-        }
-
-        assertTrue(found);
-
-        // We've removed all serviceFound events, and we've removed the discoveryStarted
-        // event as well, so now the event cache should be empty!
-        assertTrue(eventCacheSize() == 0);
-
-        // Resolve the service
-        clearEventCache();
-        mNsdManager.resolveService(si, mResolveListener);
-        lastEvent = waitForCallback("onServiceResolved");                   // id = 4
-
-        assertNotNull(mResolvedService);
-
-        // Check Txt attributes
-        assertEquals(8, mResolvedService.getAttributes().size());
-        assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
-        assertNull(mResolvedService.getAttributes().get("booleanAttr"));
-        assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
-        assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
-        assertEquals(" value ", new String(mResolvedService.getAttributes()
-                .get(" whiteSpaceKeyValueAttr ")));
-        assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
-                .get("longkey")));
-        assertTrue(Arrays.equals(testByteArray,
-                mResolvedService.getAttributes().get("binaryDataAttr")));
-        assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
-        assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
-        assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
-        assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
-                String.valueOf(lastEvent.mInfo.getPort()));
-
-        assertTrue(lastEvent.mInfo.getPort() == localPort);
-        assertTrue(eventCacheSize() == 1);
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        // Unregister the service
-        mNsdManager.unregisterService(mRegistrationListener);
-        lastEvent = waitForCallback("onServiceUnregistered");               // id = 5
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        // Expect a callback for service lost
-        lastEvent = waitForCallback("onServiceLost");                       // id = 6
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
-        // Register service again to see if we discover it
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        si = new NsdServiceInfo();
-        si.setServiceType(SERVICE_TYPE);
-        si.setServiceName(mServiceName);
-        si.setPort(localPort);
-
-        // Create a new registration listener and register same service again
-        initRegistrationListener();
-
-        mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-
-        lastEvent = waitForCallback("onServiceRegistered");                 // id = 7
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        registeredName = lastEvent.mInfo.getServiceName();
-
-        // Expect a record to be discovered
-        // Expect a service record to be discovered (and filter the ones
-        // that are unrelated to this test)
-        found = false;
-        for (int i = 0; i < 32; i++) {
-
-            lastEvent = waitForCallback("onServiceFound");                  // id = 8
-            if (lastEvent == null) {
-                // no more onServiceFound events are being reported!
-                break;
-            }
-
-            assertTrue(lastEvent.mSucceeded);
-
-            if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                    lastEvent.mInfo.getServiceName());
-
-            if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
-                // Save it, as it will get overwritten with new serviceFound events
-                si = lastEvent.mInfo;
-                found = true;
-            }
-
-            // Remove this event from the event cache, so it won't be found by subsequent
-            // calls to waitForCallback
-            synchronized (mEventCache) {
-                mEventCache.remove(lastEvent);
-            }
-        }
-
-        assertTrue(found);
-
-        // Resolve the service
-        clearEventCache();
-        mNsdManager.resolveService(si, mResolveListener);
-        lastEvent = waitForCallback("onServiceResolved");                   // id = 9
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                lastEvent.mInfo.getServiceName());
-
-        assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
-        assertNotNull(mResolvedService);
-
-        // Check that we don't have any TXT records
-        assertEquals(0, mResolvedService.getAttributes().size());
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        mNsdManager.stopServiceDiscovery(mDiscoveryListener);
-        lastEvent = waitForCallback("onDiscoveryStopped");                  // id = 10
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(checkCacheSize(1));
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        mNsdManager.unregisterService(mRegistrationListener);
-
-        lastEvent =  waitForCallback("onServiceUnregistered");              // id = 11
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(checkCacheSize(1));
-    }
-
-    boolean checkCacheSize(int size) {
-        synchronized (mEventCache) {
-            int cacheSize = mEventCache.size();
-            if (cacheSize != size) {
-                Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
-                for (int i = 0; i < cacheSize; i++) {
-                    EventData e = mEventCache.get(i);
-                    String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
-                    Log.d(TAG, "eventName is " + e.mCallbackName + sname);
-                }
-            }
-            return (cacheSize == size);
-        }
-    }
-
-    boolean checkForAdditionalEvents() {
-        try {
-            EventData e = waitForNewEvents();
-            if (e != null) {
-                String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
-                Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
-            }
-            return (e == null);
-        }
-        catch (InterruptedException ex) {
-            return false;
-        }
-    }
-}
-
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
new file mode 100644
index 0000000..9307c27
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts
+
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StartDiscoveryFailed
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StopDiscoveryFailed
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.DiscoveryListener
+import android.net.nsd.NsdManager.RegistrationListener
+import android.net.nsd.NsdManager.ResolveListener
+import android.net.nsd.NsdServiceInfo
+import android.platform.test.annotations.AppModeFull
+import android.util.Log
+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 org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.ServerSocket
+import java.nio.charset.StandardCharsets
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val TAG = "NsdManagerTest"
+private const val SERVICE_TYPE = "_nmt._tcp"
+private const val TIMEOUT_MS = 2000L
+private const val DBG = false
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+@RunWith(AndroidJUnit4::class)
+class NsdManagerTest {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+    private val serviceName = "NsdTest%04d".format(Random().nextInt(1000))
+
+    private interface NsdEvent
+    private open class NsdRecord<T : NsdEvent> private constructor(
+        private val history: ArrayTrackRecord<T>
+    ) : TrackRecord<T> by history {
+        constructor() : this(ArrayTrackRecord())
+
+        val nextEvents = history.newReadHead()
+
+        inline fun <reified V : NsdEvent> expectCallbackEventually(
+            crossinline predicate: (V) -> Boolean = { true }
+        ): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
+                ?: fail("Callback for ${V::class.java.simpleName} not seen after $TIMEOUT_MS ms")
+
+        inline fun <reified V : NsdEvent> expectCallback(): V {
+            val nextEvent = nextEvents.poll(TIMEOUT_MS)
+            assertNotNull(nextEvent, "No callback received after $TIMEOUT_MS ms")
+            assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
+                    nextEvent.javaClass.simpleName)
+            return nextEvent
+        }
+    }
+
+    private class NsdRegistrationRecord : RegistrationListener,
+            NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+        sealed class RegistrationEvent : NsdEvent {
+            abstract val serviceInfo: NsdServiceInfo
+
+            data class RegistrationFailed(
+                override val serviceInfo: NsdServiceInfo,
+                val errorCode: Int
+            ) : RegistrationEvent()
+
+            data class UnregistrationFailed(
+                override val serviceInfo: NsdServiceInfo,
+                val errorCode: Int
+            ) : RegistrationEvent()
+
+            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo)
+                : RegistrationEvent()
+            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo)
+                : RegistrationEvent()
+        }
+
+        override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
+            add(RegistrationFailed(si, err))
+        }
+
+        override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
+            add(UnregistrationFailed(si, err))
+        }
+
+        override fun onServiceRegistered(si: NsdServiceInfo) {
+            add(ServiceRegistered(si))
+        }
+
+        override fun onServiceUnregistered(si: NsdServiceInfo) {
+            add(ServiceUnregistered(si))
+        }
+    }
+
+    private class NsdDiscoveryRecord : DiscoveryListener,
+            NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+        sealed class DiscoveryEvent : NsdEvent {
+            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
+                : DiscoveryEvent()
+
+            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int)
+                : DiscoveryEvent()
+
+            data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
+            data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
+            data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+            data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+        }
+
+        override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
+            add(StartDiscoveryFailed(serviceType, err))
+        }
+
+        override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
+            add(StopDiscoveryFailed(serviceType, err))
+        }
+
+        override fun onDiscoveryStarted(serviceType: String) {
+            add(DiscoveryStarted(serviceType))
+        }
+
+        override fun onDiscoveryStopped(serviceType: String) {
+            add(DiscoveryStopped(serviceType))
+        }
+
+        override fun onServiceFound(si: NsdServiceInfo) {
+            add(ServiceFound(si))
+        }
+
+        override fun onServiceLost(si: NsdServiceInfo) {
+            add(ServiceLost(si))
+        }
+
+        fun waitForServiceDiscovered(serviceName: String): NsdServiceInfo {
+            return expectCallbackEventually<ServiceFound> {
+                it.serviceInfo.serviceName == serviceName
+            }.serviceInfo
+        }
+    }
+
+    private class NsdResolveRecord : ResolveListener,
+            NsdRecord<NsdResolveRecord.ResolveEvent>() {
+        sealed class ResolveEvent : NsdEvent {
+            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int)
+                : ResolveEvent()
+
+            data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        }
+
+        override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
+            add(ResolveFailed(si, err))
+        }
+
+        override fun onServiceResolved(si: NsdServiceInfo) {
+            add(ServiceResolved(si))
+        }
+    }
+
+    @Test
+    fun testNsdManager() {
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = serviceName
+        // Test binary data with various bytes
+        val testByteArray = byteArrayOf(-128, 127, 2, 1, 0, 1, 2)
+        // Test string data with 256 characters (25 blocks of 10 characters + 6)
+        val string256 = "1_________2_________3_________4_________5_________6_________" +
+                "7_________8_________9_________10________11________12________13________" +
+                "14________15________16________17________18________19________20________" +
+                "21________22________23________24________25________123456"
+
+        // Illegal attributes
+        listOf(
+                Triple(null, null, "null key"),
+                Triple("", null, "empty key"),
+                Triple(string256, null, "key with 256 characters"),
+                Triple("key", string256.substring(3),
+                        "key+value combination with more than 255 characters"),
+                Triple("key", string256.substring(4), "key+value combination with 255 characters"),
+                Triple("\u0019", null, "key with invalid character"),
+                Triple("=", null, "key with invalid character"),
+                Triple("\u007f", null, "key with invalid character")
+        ).forEach {
+            assertFailsWith<IllegalArgumentException>(
+                    "Setting invalid ${it.third} unexpectedly succeeded") {
+                si.setAttribute(it.first, it.second)
+            }
+        }
+
+        // Allowed attributes
+        si.setAttribute("booleanAttr", null as String?)
+        si.setAttribute("keyValueAttr", "value")
+        si.setAttribute("keyEqualsAttr", "=")
+        si.setAttribute(" whiteSpaceKeyValueAttr ", " value ")
+        si.setAttribute("binaryDataAttr", testByteArray)
+        si.setAttribute("nullBinaryDataAttr", null as ByteArray?)
+        si.setAttribute("emptyBinaryDataAttr", byteArrayOf())
+        si.setAttribute("longkey", string256.substring(9))
+        val socket = ServerSocket(0)
+        val localPort = socket.localPort
+        si.port = localPort
+        if (DBG) Log.d(TAG, "Port = $localPort")
+
+        val registrationRecord = NsdRegistrationRecord()
+        val registeredInfo = registerService(registrationRecord, si)
+
+        val discoveryRecord = NsdDiscoveryRecord()
+        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+
+        // Expect discovery started
+        discoveryRecord.expectCallback<DiscoveryStarted>()
+
+        // Expect a service record to be discovered
+        val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
+
+        val resolvedService = resolveService(foundInfo)
+
+        // Check Txt attributes
+        assertEquals(8, resolvedService.attributes.size)
+        assertTrue(resolvedService.attributes.containsKey("booleanAttr"))
+        assertNull(resolvedService.attributes["booleanAttr"])
+        assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
+        assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
+        assertEquals(" value ",
+                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+        assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
+        assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
+        assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
+        assertNull(resolvedService.attributes["nullBinaryDataAttr"])
+        assertTrue(resolvedService.attributes.containsKey("emptyBinaryDataAttr"))
+        assertNull(resolvedService.attributes["emptyBinaryDataAttr"])
+        assertEquals(localPort, resolvedService.port)
+
+        // Unregister the service
+        nsdManager.unregisterService(registrationRecord)
+        registrationRecord.expectCallback<ServiceUnregistered>()
+
+        // Expect a callback for service lost
+        discoveryRecord.expectCallbackEventually<ServiceLost> {
+            it.serviceInfo.serviceName == serviceName
+        }
+
+        // Register service again to see if NsdManager can discover it
+        val si2 = NsdServiceInfo()
+        si2.serviceType = SERVICE_TYPE
+        si2.serviceName = serviceName
+        si2.port = localPort
+        val registrationRecord2 = NsdRegistrationRecord()
+        val registeredInfo2 = registerService(registrationRecord2, si2)
+
+        // 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)
+
+        // Check that the resolved service doesn't have any TXT records
+        assertEquals(0, resolvedService2.attributes.size)
+
+        nsdManager.stopServiceDiscovery(discoveryRecord)
+
+        discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+
+        nsdManager.unregisterService(registrationRecord2)
+        registrationRecord2.expectCallback<ServiceUnregistered>()
+    }
+
+    /**
+     * Register a service and return its registration record.
+     */
+    private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, record)
+        // We may not always get the name that we tried to register;
+        // This events tells us the name that was registered.
+        val cb = record.expectCallback<ServiceRegistered>()
+        return cb.serviceInfo
+    }
+
+    private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
+        val record = NsdResolveRecord()
+        nsdManager.resolveService(discoveredInfo, record)
+        val resolvedCb = record.expectCallback<ServiceResolved>()
+        assertEquals(discoveredInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+
+        return resolvedCb.serviceInfo
+    }
+}
+
+private fun ByteArray?.utf8ToString(): String {
+    if (this == null) return ""
+    return String(this, StandardCharsets.UTF_8)
+}
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
new file mode 100644
index 0000000..a56f76e
--- /dev/null
+++ b/tests/mts/Android.bp
@@ -0,0 +1,36 @@
+// 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"],
+}
+
+cc_test {
+    name: "bpf_existence_test",
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+    require_root: true,
+    static_libs: [
+        "libbase",
+        "libmodules-utils-build",
+    ],
+    srcs: [
+        "bpf_existence_test.cpp",
+    ],
+    compile_multilib: "first",
+    min_sdk_version: "29",  // Ensure test runs on Q and above.
+}
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
new file mode 100644
index 0000000..5e3df57
--- /dev/null
+++ b/tests/mts/bpf_existence_test.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright 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.
+ *
+ * bpf_existence_test.cpp - checks that the device runs expected BPF programs
+ */
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <android-base/strings.h>
+#include <android-base/properties.h>
+#include <android-modules-utils/sdk_level.h>
+
+#include <gtest/gtest.h>
+
+using std::find;
+using std::string;
+using std::vector;
+
+using android::modules::sdklevel::IsAtLeastR;
+using android::modules::sdklevel::IsAtLeastS;
+using android::modules::sdklevel::IsAtLeastT;
+
+class BpfExistenceTest : public ::testing::Test {
+};
+
+static const vector<string> INTRODUCED_R = {
+    "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_ether",
+    "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_rawip",
+};
+
+static const vector<string> INTRODUCED_S = {
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream4_ether",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream4_rawip",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_rawip",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream4_ether",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream4_rawip",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream6_ether",
+    "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream6_rawip",
+};
+
+static const vector<string> REMOVED_S = {
+    "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_ether",
+    "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_rawip",
+};
+
+static const vector<string> INTRODUCED_T = {
+};
+
+static const vector<string> REMOVED_T = {
+};
+
+void addAll(vector<string>* a, const vector<string>& b) {
+    a->insert(a->end(), b.begin(), b.end());
+}
+
+void removeAll(vector<string>* a, const vector<string> b) {
+    for (const auto& toRemove : b) {
+        auto iter = find(a->begin(), a->end(), toRemove);
+        while (iter != a->end()) {
+            a->erase(iter);
+            iter = find(a->begin(), a->end(), toRemove);
+        }
+    }
+}
+
+void getFileLists(vector<string>* expected, vector<string>* unexpected) {
+    unexpected->clear();
+    expected->clear();
+
+    addAll(unexpected, INTRODUCED_R);
+    addAll(unexpected, INTRODUCED_S);
+    addAll(unexpected, INTRODUCED_T);
+
+    if (IsAtLeastR()) {
+        addAll(expected, INTRODUCED_R);
+        removeAll(unexpected, INTRODUCED_R);
+        // Nothing removed in R.
+    }
+
+    if (IsAtLeastS()) {
+        addAll(expected, INTRODUCED_S);
+        removeAll(expected, REMOVED_S);
+
+        addAll(unexpected, REMOVED_S);
+        removeAll(unexpected, INTRODUCED_S);
+    }
+
+    // Nothing added or removed in SCv2.
+
+    if (IsAtLeastT()) {
+        addAll(expected, INTRODUCED_T);
+        removeAll(expected, REMOVED_T);
+
+        addAll(unexpected, REMOVED_T);
+        removeAll(unexpected, INTRODUCED_T);
+    }
+}
+
+void checkFiles() {
+    vector<string> mustExist;
+    vector<string> mustNotExist;
+
+    getFileLists(&mustExist, &mustNotExist);
+
+    for (const auto& file : mustExist) {
+        EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
+    }
+    for (const auto& file : mustNotExist) {
+        int ret = access(file.c_str(), R_OK);
+        int err = errno;
+        EXPECT_EQ(-1, ret) << file << " unexpectedly exists";
+        if (ret == -1) {
+            EXPECT_EQ(ENOENT, err) << " accessing " << file << " failed with errno " << err;
+        }
+    }
+}
+
+TEST_F(BpfExistenceTest, TestPrograms) {
+    // Pre-flight check to ensure test has been updated.
+    uint64_t buildVersionSdk = android::base::GetUintProperty<uint64_t>("ro.build.version.sdk", 0);
+    ASSERT_NE(0, buildVersionSdk) << "Unable to determine device SDK version";
+    if (buildVersionSdk > 33 && buildVersionSdk != 10000) {
+            FAIL() << "Unknown OS version " << buildVersionSdk << ", please update this test";
+    }
+
+    // Only unconfined root is guaranteed to be able to access everything in /sys/fs/bpf.
+    ASSERT_EQ(0, getuid()) << "This test must run as root.";
+
+    checkFiles();
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c47604c..8d91552 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3386,12 +3386,12 @@
 
     private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) {
         return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
-                /*secure=*/ false, VpnManager.TYPE_VPN_NONE);
+                /*secure=*/ false, VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
     }
 
     private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) {
         return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE,
-                secure, vpnType);
+                secure, vpnType, /*excludeLocalRoutes=*/ false);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index d82a756..40587c5 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -41,7 +41,6 @@
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
 import static android.net.NetworkStats.SET_FOREGROUND;
-import static android.net.NetworkStats.STATS_PER_UID;
 import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
@@ -86,7 +85,7 @@
 import android.content.Intent;
 import android.database.ContentObserver;
 import android.net.DataUsageRequest;
-import android.net.INetworkManagementEventObserver;
+import android.net.INetd;
 import android.net.INetworkStatsSession;
 import android.net.LinkProperties;
 import android.net.Network;
@@ -96,6 +95,7 @@
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.TetherStatsParcel;
 import android.net.TetheringManager;
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
@@ -105,7 +105,6 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
-import android.os.INetworkManagementService;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Messenger;
@@ -120,6 +119,7 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.server.net.NetworkStatsService.AlertObserver;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -184,7 +184,7 @@
     private MockContext mServiceContext;
     private @Mock TelephonyManager mTelephonyManager;
     private static @Mock WifiInfo sWifiInfo;
-    private @Mock INetworkManagementService mNetManager;
+    private @Mock INetd mNetd;
     private @Mock TetheringManager mTetheringManager;
     private @Mock NetworkStatsFactory mStatsFactory;
     private @Mock NetworkStatsSettings mSettings;
@@ -196,7 +196,7 @@
 
     private NetworkStatsService mService;
     private INetworkStatsSession mSession;
-    private INetworkManagementEventObserver mNetworkObserver;
+    private AlertObserver mAlertObserver;
     private ContentObserver mContentObserver;
     private Handler mHandler;
     private TetheringManager.TetheringEventCallback mTetheringEventCallback;
@@ -243,6 +243,20 @@
             return currentTimeMillis();
         }
     };
+
+    @NonNull
+    private static TetherStatsParcel buildTetherStatsParcel(String iface, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int ifIndex) {
+        TetherStatsParcel parcel = new TetherStatsParcel();
+        parcel.iface = iface;
+        parcel.rxBytes = rxBytes;
+        parcel.rxPackets = rxPackets;
+        parcel.txBytes = txBytes;
+        parcel.txPackets = txPackets;
+        parcel.ifIndex = ifIndex;
+        return parcel;
+    }
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -258,7 +272,7 @@
 
         mHandlerThread = new HandlerThread("HandlerThread");
         final NetworkStatsService.Dependencies deps = makeDependencies();
-        mService = new NetworkStatsService(mServiceContext, mNetManager, mAlarmManager, wakeLock,
+        mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
                 mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
                 getBaseDir(mStatsDir), deps);
 
@@ -283,11 +297,11 @@
         mSession = mService.openSession();
         assertNotNull("openSession() failed", mSession);
 
-        // Catch INetworkManagementEventObserver during systemReady().
-        ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
-                ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
-        verify(mNetManager).registerObserver(networkObserver.capture());
-        mNetworkObserver = networkObserver.getValue();
+        // Catch AlertObserver during systemReady().
+        final ArgumentCaptor<AlertObserver> alertObserver =
+                ArgumentCaptor.forClass(AlertObserver.class);
+        verify(mNetd).registerUnsolicitedEventListener(alertObserver.capture());
+        mAlertObserver = alertObserver.getValue();
 
         // Catch TetheringEventCallback during systemReady().
         ArgumentCaptor<TetheringManager.TetheringEventCallback> tetheringEventCbCaptor =
@@ -328,7 +342,7 @@
         mServiceContext = null;
         mStatsDir = null;
 
-        mNetManager = null;
+        mNetd = null;
         mSettings = null;
 
         mSession.close();
@@ -1026,13 +1040,15 @@
                 new UnderlyingNetworkInfo[0]);
 
         NetworkStats.Entry uidStats = new NetworkStats.Entry(
-                TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+                TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
         // Stacked on matching interface
         NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry(
-                stackedIface, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+                stackedIface, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
+        TetherStatsParcel tetherStatsParcel1 =
+                buildTetherStatsParcel(stackedIface, 1024L, 8L, 512L, 4L, 0);
         // Different interface
-        NetworkStats.Entry tetheredStats2 = new NetworkStats.Entry(
-                "otherif", UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+        TetherStatsParcel tetherStatsParcel2 =
+                buildTetherStatsParcel("otherif", 1024L, 8L, 512L, 4L, 0);
 
         final String[] ifaceFilter = new String[] { TEST_IFACE };
         final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE };
@@ -1044,10 +1060,8 @@
         when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL)))
                 .thenReturn(new NetworkStats(getElapsedRealtime(), 1)
                         .insertEntry(uidStats));
-        when(mNetManager.getNetworkStatsTethering(STATS_PER_UID))
-                .thenReturn(new NetworkStats(getElapsedRealtime(), 2)
-                        .insertEntry(tetheredStats1)
-                        .insertEntry(tetheredStats2));
+        final TetherStatsParcel[] tetherStatsParcels =  {tetherStatsParcel1, tetherStatsParcel2};
+        when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
 
         NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
 
@@ -1249,12 +1263,11 @@
         final NetworkStats localUidStats = new NetworkStats(now, 1)
                 .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L);
         // Software per-uid tethering traffic.
-        final NetworkStats tetherSwUidStats = new NetworkStats(now, 1)
-                .insertEntry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1408L, 10L, 256L, 1L,
-                        0L);
+        final TetherStatsParcel[] tetherStatsParcels =
+                {buildTetherStatsParcel(TEST_IFACE, 1408L, 10L, 256L, 1L, 0)};
 
         expectNetworkStatsSummary(swIfaceStats);
-        expectNetworkStatsUidDetail(localUidStats, tetherSwUidStats);
+        expectNetworkStatsUidDetail(localUidStats, tetherStatsParcels);
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -1748,16 +1761,17 @@
     }
 
     private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
-        expectNetworkStatsUidDetail(detail, new NetworkStats(0L, 0));
+        final TetherStatsParcel[] tetherStatsParcels = {};
+        expectNetworkStatsUidDetail(detail, tetherStatsParcels);
     }
 
-    private void expectNetworkStatsUidDetail(NetworkStats detail, NetworkStats tetherStats)
-            throws Exception {
+    private void expectNetworkStatsUidDetail(NetworkStats detail,
+            TetherStatsParcel[] tetherStatsParcels) throws Exception {
         when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL))
                 .thenReturn(detail);
 
         // also include tethering details, since they are folded into UID
-        when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)).thenReturn(tetherStats);
+        when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
     }
 
     private void expectDefaultSettings() throws Exception {