Merge "Add CtsTetheringTestLatestSdk into mainline-presubmit"
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 44b3364..1e0adef 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -53,7 +53,9 @@
     defaults: [
         "cts_defaults",
     ],
+    enforce_default_target_sdk_version: true,
     sdk_version: "test_current",
+    min_sdk_version: "30",
     static_libs: ["CtsNetHttpTestsLib"],
     // Tag this as a cts test artifact
     test_suites: [
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 93564e4..4e4251c 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -34,25 +34,13 @@
         "--output $(out)",
 }
 
+// Library to be used in coverage tests. cronet_java_tests can't be used directly because the common
+// tests need to inherit the NetHttpTests manifest.
 android_library {
     name: "NetHttpTestsLibPreJarJar",
-    srcs: [":cronet_aml_javatests_sources"],
+    static_libs: ["cronet_java_tests"],
     sdk_version: "module_current",
     min_sdk_version: "30",
-    static_libs: [
-        "cronet_testserver_utils",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "junit",
-        "truth",
-    ],
-    libs: [
-        "android.test.base",
-        "framework-connectivity-pre-jarjar",
-        // android.net.TrafficStats apis
-        "framework-connectivity-t",
-    ],
-    lint: { test: true }
 }
 
 android_test {
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index cf3a017..a3e86de 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -5,6 +5,8 @@
 # cronet_tests.so is not jarjared and uses base classes. We can remove this when there's a
 # separate java base target to depend on.
 org\.chromium\.base\..+
+J\.cronet_tests_N(\$.+)?
+
 # Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
 org\.chromium\.net\..*Test.*(\$.+)?
 org\.chromium\.net\.NativeTestServer(\$.+)?
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index b88ec7f..4478b1e 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -195,11 +195,7 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The network stack *must* be included to ensure security of the device
-    required: [
-        "NetworkStack",
-        "privapp_allowlist_com.android.tethering",
-    ],
+    privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
     lint: { strict_updatability_linting: true },
 }
@@ -215,11 +211,7 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The network stack *must* be included to ensure security of the device
-    required: [
-        "NetworkStackNext",
-        "privapp_allowlist_com.android.tethering",
-    ],
+    privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
     lint: { strict_updatability_linting: true },
 }
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 4bfd1fb..cf9b359 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -112,10 +112,7 @@
         "ServiceConnectivityResources",
         "HalfSheetUX",
     ],
-    prebuilts: [
-        "current_sdkinfo",
-        "privapp_allowlist_com.android.tethering",
-    ],
+    prebuilts: ["current_sdkinfo"],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
     // Indicates that pre-installed version of this apex can be compressed.
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index ac9ec65..69c1aa2 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -19,10 +19,7 @@
     default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
 }
 
-prebuilt_etc {
+filegroup {
     name: "privapp_allowlist_com.android.tethering",
-    sub_dir: "permissions",
-    filename: "permissions.xml",
-    src: "permissions.xml",
-    installable: false,
+    srcs: ["permissions.xml"],
 }
\ No newline at end of file
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 9d0f6b4..6affb62 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -17,6 +17,8 @@
 package android.net.ip;
 
 import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -405,8 +407,8 @@
 
     /** Internals. */
 
-    private boolean startIPv4() {
-        return configureIPv4(true);
+    private boolean startIPv4(int scope) {
+        return configureIPv4(true, scope);
     }
 
     /**
@@ -616,7 +618,7 @@
     }
 
     private void stopIPv4() {
-        configureIPv4(false);
+        configureIPv4(false /* enabled */, CONNECTIVITY_SCOPE_GLOBAL /* not used */);
         // NOTE: All of configureIPv4() will be refactored out of existence
         // into calls to InterfaceController, shared with startIPv4().
         mInterfaceCtrl.clearIPv4Address();
@@ -627,11 +629,11 @@
         mStaticIpv4ClientAddr = null;
     }
 
-    private boolean configureIPv4(boolean enabled) {
+    private boolean configureIPv4(boolean enabled, int scope) {
         if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")");
 
         if (enabled) {
-            mIpv4Address = requestIpv4Address(true /* useLastAddress */);
+            mIpv4Address = requestIpv4Address(scope, true /* useLastAddress */);
         }
 
         if (mIpv4Address == null) {
@@ -679,12 +681,12 @@
         return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
     }
 
-    private LinkAddress requestIpv4Address(final boolean useLastAddress) {
+    private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
         if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
 
         if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
 
-        return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress);
+        return mPrivateAddressCoordinator.requestDownstreamAddress(this, scope, useLastAddress);
     }
 
     private boolean startIPv6() {
@@ -1133,8 +1135,16 @@
             sendInterfaceState(mDesiredInterfaceState);
         }
 
+        private int getScope() {
+            if (mDesiredInterfaceState == STATE_TETHERED) {
+                return CONNECTIVITY_SCOPE_GLOBAL;
+            }
+
+            return CONNECTIVITY_SCOPE_LOCAL;
+        }
+
         private void startServingInterface() {
-            if (!startIPv4()) {
+            if (!startIPv4(getScope())) {
                 mLastError = TETHER_ERROR_IFACE_CFG_ERROR;
                 return;
             }
@@ -1222,7 +1232,7 @@
             }
 
             final LinkAddress deprecatedLinkAddress = mIpv4Address;
-            mIpv4Address = requestIpv4Address(false);
+            mIpv4Address = requestIpv4Address(getScope(), false);
             if (mIpv4Address == null) {
                 mLog.e("Fail to request a new downstream prefix");
                 return;
diff --git a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
index 8a96988..5b19c4e 100644
--- a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
+++ b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
@@ -29,6 +29,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -49,6 +51,8 @@
     @NonNull
     private List<WifiClient> mLastWifiClients = Collections.emptyList();
     @NonNull
+    private List<WifiClient> mLastLocalOnlyClients = Collections.emptyList();
+    @NonNull
     private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
 
     @VisibleForTesting
@@ -72,25 +76,44 @@
      *
      * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
      * @param ipServers The IpServers used to assign addresses to clients.
-     * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
-     *                    update.
+     * @param wifiClients The list of L2-connected WiFi clients that are connected to a global
+     *                    hotspot. Null for no change since last update.
+     * @param localOnlyClients The list of L2-connected WiFi clients that are connected to localOnly
+     *                    hotspot. Null for no change since last update.
      * @return True if the list of clients changed since the last calculation.
      */
     public boolean updateConnectedClients(
-            Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
+            Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients,
+            @Nullable List<WifiClient> localOnlyClients) {
         final long now = mClock.elapsedRealtime();
 
-        if (wifiClients != null) {
-            mLastWifiClients = wifiClients;
-        }
+        if (wifiClients != null) mLastWifiClients = wifiClients;
+        if (localOnlyClients != null) mLastLocalOnlyClients = localOnlyClients;
+
         final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
+        final Set<MacAddress> localOnlyClientMacs = getClientMacs(mLastLocalOnlyClients);
 
         // Build the list of non-expired leases from all IpServers, grouped by mac address
         final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
         for (IpServer server : ipServers) {
+            final Set<MacAddress> connectedClientMacs;
+            switch (server.servingMode()) {
+                case IpServer.STATE_TETHERED:
+                    connectedClientMacs = wifiClientMacs;
+                    break;
+                case IpServer.STATE_LOCAL_ONLY:
+                    // Before T, SAP and LOHS both use wifiClientMacs because
+                    // registerLocalOnlyHotspotSoftApCallback didn't exist.
+                    connectedClientMacs = SdkLevel.isAtLeastT()
+                            ? localOnlyClientMacs : wifiClientMacs;
+                    break;
+                default:
+                    continue;
+            }
+
             for (TetheredClient client : server.getAllLeases()) {
                 if (client.getTetheringType() == TETHERING_WIFI
-                        && !wifiClientMacs.contains(client.getMacAddress())) {
+                        && !connectedClientMacs.contains(client.getMacAddress())) {
                     // Skip leases of WiFi clients that are not (or no longer) L2-connected
                     continue;
                 }
@@ -104,11 +127,8 @@
         // TODO: add IPv6 addresses from netlink
 
         // Add connected WiFi clients that do not have any known address
-        for (MacAddress client : wifiClientMacs) {
-            if (clientsMap.containsKey(client)) continue;
-            clientsMap.put(client, new TetheredClient(
-                    client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
-        }
+        addWifiClientsIfNoLeases(clientsMap, wifiClientMacs);
+        addWifiClientsIfNoLeases(clientsMap, localOnlyClientMacs);
 
         final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
         final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
@@ -117,6 +137,15 @@
         return clientsChanged;
     }
 
+    private static void addWifiClientsIfNoLeases(
+            final Map<MacAddress, TetheredClient> clientsMap, final Set<MacAddress> clientMacs) {
+        for (MacAddress mac : clientMacs) {
+            if (clientsMap.containsKey(mac)) continue;
+            clientsMap.put(mac, new TetheredClient(
+                    mac, Collections.emptyList() /* addresses */, TETHERING_WIFI));
+        }
+    }
+
     private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
         final TetheredClient aggregateClient = clientsMap.getOrDefault(
                 lease.getMacAddress(), lease);
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 41a10ae..6c0ca82 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -16,6 +16,8 @@
 package com.android.networkstack.tethering;
 
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 
@@ -34,7 +36,6 @@
 import android.net.ip.IpServer;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -77,7 +78,7 @@
     private final ConnectivityManager mConnectivityMgr;
     private final TetheringConfiguration mConfig;
     // keyed by downstream type(TetheringManager.TETHERING_*).
-    private final SparseArray<LinkAddress> mCachedAddresses;
+    private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
 
     public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
         mDownstreams = new ArraySet<>();
@@ -85,10 +86,12 @@
         mConnectivityMgr = (ConnectivityManager) context.getSystemService(
                 Context.CONNECTIVITY_SERVICE);
         mConfig = config;
-        mCachedAddresses = new SparseArray<>();
+        mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
         // Reserved static addresses for bluetooth and wifi p2p.
-        mCachedAddresses.put(TETHERING_BLUETOOTH, new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
-        mCachedAddresses.put(TETHERING_WIFI_P2P, new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
+        mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
+                new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
+        mCachedAddresses.put(new AddressKey(TETHERING_WIFI_P2P, CONNECTIVITY_SCOPE_LOCAL),
+                new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
 
         mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
             new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
@@ -166,16 +169,18 @@
      * returns null if there is no available address.
      */
     @Nullable
-    public LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+    public LinkAddress requestDownstreamAddress(final IpServer ipServer, final int scope,
+            boolean useLastAddress) {
         if (mConfig.shouldEnableWifiP2pDedicatedIp()
                 && ipServer.interfaceType() == TETHERING_WIFI_P2P) {
             return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
         }
 
+        final AddressKey addrKey = new AddressKey(ipServer.interfaceType(), scope);
         // This ensures that tethering isn't started on 2 different interfaces with the same type.
         // Once tethering could support multiple interface with the same type,
         // TetheringSoftApCallback would need to handle it among others.
-        final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
+        final LinkAddress cachedAddress = mCachedAddresses.get(addrKey);
         if (useLastAddress && cachedAddress != null
                 && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
             mDownstreams.add(ipServer);
@@ -186,7 +191,7 @@
             final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
             if (newAddress != null) {
                 mDownstreams.add(ipServer);
-                mCachedAddresses.put(ipServer.interfaceType(), newAddress);
+                mCachedAddresses.put(addrKey, newAddress);
                 return newAddress;
             }
         }
@@ -384,6 +389,34 @@
         return asIpPrefix(address);
     }
 
+    private static class AddressKey {
+        private final int mTetheringType;
+        private final int mScope;
+
+        private AddressKey(int type, int scope) {
+            mTetheringType = type;
+            mScope = scope;
+        }
+
+        @Override
+        public int hashCode() {
+            return (mTetheringType << 16) + mScope;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (!(obj instanceof AddressKey)) return false;
+            final AddressKey other = (AddressKey) obj;
+
+            return mTetheringType == other.mTetheringType && mScope == other.mScope;
+        }
+
+        @Override
+        public String toString() {
+            return "AddressKey(" + mTetheringType + ", " + mScope + ")";
+        }
+    }
+
     void dump(final IndentingPrintWriter pw) {
         pw.println("mTetheringPrefixes:");
         pw.increaseIndent();
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 4c5bf4e..e5f644e 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -58,6 +58,7 @@
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED;
+import static android.net.wifi.WifiManager.SoftApCallback;
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -479,15 +480,15 @@
                 mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
 
         final WifiManager wifiManager = getWifiManager();
-        TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
         if (wifiManager != null) {
-            wifiManager.registerSoftApCallback(mExecutor, softApCallback);
-        }
-        if (SdkLevel.isAtLeastT() && wifiManager != null) {
-            // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
-            // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
-            // or MAINLINE_NETWORK_STACK permission would also able to use this API.
-            wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+            if (SdkLevel.isAtLeastT()) {
+                // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+                // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+                // or MAINLINE_NETWORK_STACK permission can also use this API.
+                wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor,
+                        new LocalOnlyHotspotCallback());
+            }
         }
 
         startTrackDefaultNetwork();
@@ -573,26 +574,17 @@
         }
     }
 
-    private class TetheringSoftApCallback implements WifiManager.SoftApCallback {
-        // TODO: Remove onStateChanged override when this method has default on
-        // WifiManager#SoftApCallback interface.
-        // Wifi listener for state change of the soft AP
-        @Override
-        public void onStateChanged(final int state, final int failureReason) {
-            // Nothing
-        }
-
-        // Called by wifi when the number of soft AP clients changed.
-        // Currently multiple softAp would not behave well in PrivateAddressCoordinator
-        // (where it gets the address from cache), it ensure tethering only support one ipServer for
-        // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
-        // onConnectedClientsChanged should also be updated to support tracking different softAp's
-        // clients individually.
-        // TODO: Add wtf log and have check to reject request duplicated type with different
-        // interface.
+    private class TetheringSoftApCallback implements SoftApCallback {
         @Override
         public void onConnectedClientsChanged(final List<WifiClient> clients) {
-            updateConnectedClients(clients);
+            updateConnectedClients(clients, null);
+        }
+    }
+
+    private class LocalOnlyHotspotCallback implements SoftApCallback {
+        @Override
+        public void onConnectedClientsChanged(final List<WifiClient> clients) {
+            updateConnectedClients(null, clients);
         }
     }
 
@@ -1968,7 +1960,7 @@
             mIPv6TetheringCoordinator.removeActiveDownstream(who);
             mOffload.excludeDownstreamInterface(who.interfaceName());
             mForwardedDownstreams.remove(who);
-            updateConnectedClients(null /* wifiClients */);
+            maybeDhcpLeasesChanged();
 
             // If this is a Wi-Fi interface, tell WifiManager of any errors
             // or the inactive serving state.
@@ -2710,9 +2702,15 @@
         if (e != null) throw e;
     }
 
-    private void updateConnectedClients(final List<WifiClient> wifiClients) {
+    private void maybeDhcpLeasesChanged() {
+        // null means wifi clients did not change.
+        updateConnectedClients(null, null);
+    }
+
+    private void updateConnectedClients(final List<WifiClient> wifiClients,
+            final List<WifiClient> localOnlyClients) {
         if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
-                wifiClients)) {
+                wifiClients, localOnlyClients)) {
             reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
         }
     }
@@ -2731,7 +2729,7 @@
 
             @Override
             public void dhcpLeasesChanged() {
-                updateConnectedClients(null /* wifiClients */);
+                maybeDhcpLeasesChanged();
             }
 
             @Override
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index f0d9057..46e50ef 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -19,6 +19,8 @@
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
 import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
@@ -48,6 +50,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
@@ -271,8 +274,8 @@
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
         }
         reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
-        when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
-                mTestAddress);
+        when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+                anyBoolean())).thenReturn(mTestAddress);
     }
 
     @SuppressWarnings("DoNotCall") // Ignore warning for synchronous to call to Thread.run()
@@ -293,8 +296,8 @@
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
-        when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
-                mTestAddress);
+        when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+                anyBoolean())).thenReturn(mTestAddress);
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
@@ -428,7 +431,8 @@
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
         InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
         if (isAtLeastT()) {
-            inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+            inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+                    eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
             inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
                     IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         }
@@ -477,7 +481,8 @@
 
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
         InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
-        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+                eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
                 IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -498,7 +503,8 @@
 
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
         InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
-        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+                eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
                   IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -766,7 +772,8 @@
         final ArgumentCaptor<LinkProperties> lpCaptor =
                 ArgumentCaptor.forClass(LinkProperties.class);
         InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
-        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+                eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
         inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
         // One for ipv4 route, one for ipv6 link local route.
         inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
@@ -779,12 +786,13 @@
         // Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals
         // onNewPrefixRequest callback.
         final LinkAddress newAddress = new LinkAddress("192.168.100.125/24");
-        when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
-                newAddress);
+        when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+                anyBoolean())).thenReturn(newAddress);
         eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24"));
         mLooper.dispatchAll();
 
-        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(false));
+        inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+                eq(CONNECTIVITY_SCOPE_LOCAL), eq(false));
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
         verifyNoMoreInteractions(mCallback);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
index d915354..2dd9f91 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
@@ -24,19 +24,25 @@
 import android.net.TetheringManager.TETHERING_WIFI
 import android.net.ip.IpServer
 import android.net.wifi.WifiClient
+import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class ConnectedClientsTrackerTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
 
     private val server1 = mock(IpServer::class.java)
     private val server2 = mock(IpServer::class.java)
@@ -70,55 +76,122 @@
 
     @Test
     fun testUpdateConnectedClients() {
+        doReturn(IpServer.STATE_TETHERED).`when`(server1).servingMode()
+        doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
+        runUpdateConnectedClientsTest(isGlobal = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    fun testUpdateConnectedClients_LocalOnly() {
+        doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server1).servingMode()
+        doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server2).servingMode()
+        runUpdateConnectedClientsTest(isGlobal = false)
+    }
+
+    fun runUpdateConnectedClientsTest(isGlobal: Boolean) {
         doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
         doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
 
         val tracker = ConnectedClientsTracker(clock)
-        assertFalse(tracker.updateConnectedClients(servers, null))
+        assertFalse(tracker.updateConnectedClients(servers, null, null))
 
         // Obtain a lease for client 1
         doReturn(listOf(client1)).`when`(server1).allLeases
-        assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1)))
+        if (isGlobal) {
+            assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+                    wifiClients = listOf(wifiClient1)))
+        } else {
+            assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+                    localOnlyClients = listOf(wifiClient1)))
+        }
 
         // Client 2 L2-connected, no lease yet
         val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI)
-        assertSameClients(listOf(client1, client2WithoutAddr),
-                assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2)))
+        if (isGlobal) {
+            assertSameClients(listOf(client1, client2WithoutAddr), assertNewClients(
+                    tracker, servers, wifiClients = listOf(wifiClient1, wifiClient2)))
+        } else {
+            assertSameClients(listOf(client1, client2WithoutAddr), assertNewClients(
+                    tracker, servers, localOnlyClients = listOf(wifiClient1, wifiClient2)))
+        }
 
         // Client 2 lease obtained
         doReturn(listOf(client1, client2)).`when`(server1).allLeases
-        assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null))
+        assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers))
 
         // Client 3 lease obtained
         doReturn(listOf(client3)).`when`(server2).allLeases
-        assertSameClients(listOf(client1, client2, client3),
-                assertNewClients(tracker, servers, null))
+        assertSameClients(listOf(client1, client2, client3), assertNewClients(tracker, servers))
 
-        // Client 2 L2-disconnected
-        assertSameClients(listOf(client1, client3),
-                assertNewClients(tracker, servers, listOf(wifiClient1)))
-
-        // Client 1 L2-disconnected
-        assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList()))
-
-        // Client 1 comes back
-        assertSameClients(listOf(client1, client3),
-                assertNewClients(tracker, servers, listOf(wifiClient1)))
+        if (isGlobal) {
+            // Client 2 L2-disconnected
+            assertSameClients(listOf(client1, client3),
+                    assertNewClients(tracker, servers, wifiClients = listOf(wifiClient1)))
+            // Client 1 L2-disconnected
+            assertSameClients(listOf(client3), assertNewClients(tracker, servers,
+                    wifiClients = emptyList()))
+            // Client 1 comes back
+            assertSameClients(listOf(client1, client3),
+                    assertNewClients(tracker, servers, wifiClients = listOf(wifiClient1)))
+        } else {
+            // Client 2 L2-disconnected
+            assertSameClients(listOf(client1, client3),
+                    assertNewClients(tracker, servers, localOnlyClients = listOf(wifiClient1)))
+            // Client 1 L2-disconnected
+            assertSameClients(listOf(client3),
+                    assertNewClients(tracker, servers, localOnlyClients = emptyList()))
+            // Client 1 comes back
+            assertSameClients(listOf(client1, client3),
+                    assertNewClients(tracker, servers, localOnlyClients = listOf(wifiClient1)))
+        }
 
         // Leases lost, client 1 still L2-connected
         doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
         doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
         assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)),
-                assertNewClients(tracker, servers, null))
+                assertNewClients(tracker, servers))
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    fun testLocalOnlyAndTetheredHotspotClients() {
+        val tracker = ConnectedClientsTracker(clock)
+        doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server1).servingMode()
+        doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
+
+        // Client 1 connected to server1 (LOHS)
+        doReturn(listOf(client1)).`when`(server1).allLeases
+        doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+        assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+                localOnlyClients = listOf(wifiClient1)))
+
+        // Client 2 connected to server2 (wifi Tethering)
+        doReturn(listOf(client2)).`when`(server2).allLeases
+        assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers,
+                listOf(wifiClient2), listOf(wifiClient1)))
+
+        // Client 2 L2-disconnected but lease doesn't expired yet
+        assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+                wifiClients = emptyList()))
+
+        // Client 1 lease lost but still L2-connected
+        doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+        val client1WithoutAddr = TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)
+        assertSameClients(listOf(client1WithoutAddr), assertNewClients(tracker, servers))
+
+        // Client 1 L2-disconnected
+        assertSameClients(emptyList(), assertNewClients(tracker, servers,
+                localOnlyClients = emptyList()))
     }
 
     @Test
     fun testUpdateConnectedClients_LeaseExpiration() {
+        doReturn(IpServer.STATE_TETHERED).`when`(server1).servingMode()
+        doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
         val tracker = ConnectedClientsTracker(clock)
         doReturn(listOf(client1, client2)).`when`(server1).allLeases
         doReturn(listOf(client3)).`when`(server2).allLeases
         assertSameClients(listOf(client1, client2, client3), assertNewClients(
-                tracker, servers, listOf(wifiClient1, wifiClient2)))
+                tracker, servers, wifiClients = listOf(wifiClient1, wifiClient2)))
 
         clock.time += 20
         // Client 3 has no remaining lease: removed
@@ -131,15 +204,16 @@
                         // Only the "t + 30" address is left, the "t + 10" address expired
                         listOf(client2Exp30AddrInfo),
                         TETHERING_WIFI))
-        assertSameClients(expectedClients, assertNewClients(tracker, servers, null))
+        assertSameClients(expectedClients, assertNewClients(tracker, servers))
     }
 
     private fun assertNewClients(
         tracker: ConnectedClientsTracker,
         ipServers: Iterable<IpServer>,
-        wifiClients: List<WifiClient>?
+        wifiClients: List<WifiClient>? = null,
+        localOnlyClients: List<WifiClient>? = null
     ): List<TetheredClient> {
-        assertTrue(tracker.updateConnectedClients(ipServers, wifiClients))
+        assertTrue(tracker.updateConnectedClients(ipServers, wifiClients, localOnlyClients))
         return tracker.lastTetheredClients
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 55d9852..91b092a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -19,6 +19,8 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
@@ -61,6 +63,7 @@
     private static final String TEST_IFNAME = "test0";
 
     @Mock private IpServer mHotspotIpServer;
+    @Mock private IpServer mLocalHotspotIpServer;
     @Mock private IpServer mUsbIpServer;
     @Mock private IpServer mEthernetIpServer;
     @Mock private IpServer mWifiP2pIpServer;
@@ -90,6 +93,7 @@
         when(mUsbIpServer.interfaceType()).thenReturn(TETHERING_USB);
         when(mEthernetIpServer.interfaceType()).thenReturn(TETHERING_ETHERNET);
         when(mHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
+        when(mLocalHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
         when(mWifiP2pIpServer.interfaceType()).thenReturn(TETHERING_WIFI_P2P);
     }
 
@@ -104,9 +108,10 @@
         mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
     }
 
-    private LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+    private LinkAddress requestDownstreamAddress(final IpServer ipServer, int scope,
+            boolean useLastAddress) {
         final LinkAddress address = mPrivateAddressCoordinator.requestDownstreamAddress(
-                ipServer, useLastAddress);
+                ipServer, scope, useLastAddress);
         when(ipServer.getAddress()).thenReturn(address);
         return address;
     }
@@ -115,19 +120,19 @@
     public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception {
         final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress);
         final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix hotspotPrefix = asIpPrefix(address);
         assertNotEquals(hotspotPrefix, bluetoothPrefix);
 
         final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix testDupRequest = asIpPrefix(newAddress);
         assertNotEquals(hotspotPrefix, testDupRequest);
         assertNotEquals(bluetoothPrefix, testDupRequest);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
 
         final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix usbPrefix = asIpPrefix(usbAddress);
         assertNotEquals(usbPrefix, bluetoothPrefix);
         assertNotEquals(usbPrefix, hotspotPrefix);
@@ -139,25 +144,28 @@
         int fakeSubAddr = 0x2b00; // 43.0.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
         LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
 
         fakeSubAddr = 0x2d01; // 45.1.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+        actualAddress = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
 
         fakeSubAddr = 0x2eff; // 46.255.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+        actualAddress = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
 
         fakeSubAddr = 0x2f05; // 47.5.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+        actualAddress = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
     }
@@ -168,7 +176,7 @@
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
                 getSubAddress(mBluetoothAddress.getAddress().getAddress()));
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
         assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
@@ -177,7 +185,7 @@
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
                 getSubAddress(hotspotAddress.getAddress().getAddress()));
         final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix usbPrefix = asIpPrefix(usbAddress);
         assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix);
         assertNotEquals(hotspotPrefix, usbPrefix);
@@ -187,7 +195,7 @@
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
                 getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
         final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer,
-                false /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
         final IpPrefix etherPrefix = asIpPrefix(etherAddress);
         assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
         assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
@@ -200,11 +208,11 @@
         final int fakeHotspotSubAddr = 0x2b05; // 43.5
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
 
         final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
 
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
@@ -214,10 +222,10 @@
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
 
         final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals(hotspotAddress, newHotspotAddress);
         final LinkAddress newUsbAddress = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals(usbAddress, newUsbAddress);
 
         final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -257,7 +265,7 @@
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
         // - Enable hotspot with prefix 192.168.43.0/24
         final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
         assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
         // - test mobile network with null NetworkCapabilities. Ideally this should not happen
@@ -311,21 +319,21 @@
         // - Restart hotspot again and its prefix is different previous.
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
         final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
         assertNotEquals(hotspotPrefix, hotspotPrefix2);
         mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
         verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
         // - Usb tethering can be enabled and its prefix is different with conflict one.
         final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         final IpPrefix usbPrefix = asIpPrefix(usbAddr);
         assertNotEquals(predefinedPrefix, usbPrefix);
         assertNotEquals(hotspotPrefix2, usbPrefix);
         // - Disable wifi upstream, then wifi's prefix can be selected again.
         mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
         final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         final IpPrefix ethPrefix = asIpPrefix(ethAddr);
         assertEquals(predefinedPrefix, ethPrefix);
     }
@@ -335,7 +343,7 @@
         final int randomAddress = 0x8605; // 134.5
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
         final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
         final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -345,7 +353,7 @@
 
         // Check whether return address is next prefix of 192.168.134.0/24.
         final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
         final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
                 new LinkAddress("192.168.149.16/19"), null,
@@ -355,7 +363,7 @@
 
         // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
         final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
         final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
                 new LinkAddress("192.168.129.53/18"), null,
@@ -370,7 +378,7 @@
 
         // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
         final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
         final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
                 new LinkAddress("192.168.188.133/17"), null,
@@ -380,7 +388,7 @@
         // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
         // 192.168.134/24 ~ 192.168.255.255/24 is not available.
         final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
         final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
                 new LinkAddress("192.168.3.59/21"), null,
@@ -389,7 +397,7 @@
 
         // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
         final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
         final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
                 new LinkAddress("192.168.68.43/21"), null,
@@ -399,7 +407,7 @@
         // Update an upstream that does *not* conflict, check whether return the same address
         // 192.168.5/24.
         final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
         final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
                 new LinkAddress("192.168.10.97/21"), null,
@@ -408,7 +416,7 @@
 
         // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
         final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
         final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
                 new LinkAddress("192.168.0.0/17"), null,
@@ -417,7 +425,7 @@
 
         // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
         final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
     }
 
@@ -426,7 +434,7 @@
         final int randomAddress = 0x1f2b2a; // 31.43.42
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
         final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
         final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -437,7 +445,7 @@
 
         // Check whether return address is next address of prefix 192.168.128.0/17.
         final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
         final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
                 new LinkAddress("192.1.2.3/8"), null,
@@ -447,7 +455,7 @@
 
         // Check whether return address is under prefix 172.16.0.0/12.
         final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
         final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
                 new LinkAddress("172.28.123.100/14"), null,
@@ -458,12 +466,12 @@
         // 172.28.0.0 ~ 172.31.255.255 is not available.
         // Check whether return address is next address of prefix 172.16.0.0/14.
         final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
 
         // Check whether new downstream is next address of address 172.16.0.42/24.
         final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
         final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
                 new LinkAddress("172.16.0.1/24"), null,
@@ -474,7 +482,7 @@
 
         // Check whether return address is next address of prefix 172.16.1.42/24.
         final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
         final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
                 new LinkAddress("172.16.0.1/13"), null,
@@ -485,11 +493,11 @@
 
         // Check whether return address is next address of prefix 172.16.0.1/13.
         final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
         // Check whether return address is next address of prefix 172.24.0.42/24.
         final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
         final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
                 new LinkAddress("172.24.0.1/12"), null,
@@ -500,11 +508,11 @@
 
         // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
         final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
         // Check whether new downstream is next address of address 10.31.43.42/24.
         final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
     }
 
@@ -524,7 +532,7 @@
 
     private void assertReseveredWifiP2pPrefix() throws Exception {
         LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         final IpPrefix hotspotPrefix = asIpPrefix(address);
         final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress);
         assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix);
@@ -544,8 +552,23 @@
 
         // If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address.
         LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer,
-                true /* useLastAddress */);
+                CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
         assertEquals(mLegacyWifiP2pAddress, address);
         mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer);
     }
+
+    @Test
+    public void testEnableSapAndLohsConcurrently() throws Exception {
+        // 0x2b05 -> 43.5, 0x8605 -> 134.5
+        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
+
+        final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
+        assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+
+        final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
+                CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
+        assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
+                localHotspotAddress);
+    }
 }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 29a03d1..c47c572 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -73,7 +73,6 @@
 import com.android.server.connectivity.mdns.MdnsSearchOptions;
 import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
 import com.android.server.connectivity.mdns.MdnsServiceInfo;
-import com.android.server.connectivity.mdns.MdnsSocketClientBase;
 import com.android.server.connectivity.mdns.MdnsSocketProvider;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -1394,8 +1393,7 @@
         mMdnsSocketClient =
                 new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
         mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
-                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
-                handler.getLooper());
+                mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
         handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
         mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
                 new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"));
@@ -1453,9 +1451,8 @@
          */
         public MdnsDiscoveryManager makeMdnsDiscoveryManager(
                 @NonNull ExecutorProvider executorProvider,
-                @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
-                @NonNull Looper looper) {
-            return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog, looper);
+                @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog) {
+            return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
         }
 
         /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index 9a67007..866ecba 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -211,7 +211,7 @@
                         | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
     }
 
-    private void sendPacketTo(MdnsSocketClientBase requestSender, InetSocketAddress address)
+    private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
             throws IOException {
         DatagramPacket packet = packetWriter.getPacket(address);
         if (expectUnicastResponse) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 0154827..0f30f10 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -17,8 +17,6 @@
 package com.android.server.connectivity.mdns;
 
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.isRunningOnHandlerThread;
 
 import android.Manifest.permission;
 import android.annotation.NonNull;
@@ -26,7 +24,7 @@
 import android.annotation.RequiresPermission;
 import android.net.Network;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
@@ -38,6 +36,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -53,6 +52,7 @@
 
     @NonNull private final PerNetworkServiceTypeClients perNetworkServiceTypeClients;
     @NonNull private final Handler handler;
+    @Nullable private final HandlerThread handlerThread;
 
     private static class PerNetworkServiceTypeClients {
         private final ArrayMap<Pair<String, Network>, MdnsServiceTypeClient> clients =
@@ -86,15 +86,12 @@
             return list;
         }
 
-        public List<MdnsServiceTypeClient> getByMatchingNetwork(@Nullable Network network) {
+        public List<MdnsServiceTypeClient> getByNetwork(@Nullable Network network) {
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
                 final Pair<String, Network> perNetworkServiceType = clients.keyAt(i);
                 final Network serviceTypeNetwork = perNetworkServiceType.second;
-                // The serviceTypeNetwork would be null if the MdnsSocketClient is being used. This
-                // is also the case if the socket is for a tethering interface. In either of these
-                // cases, the client is expected to process any responses.
-                if (serviceTypeNetwork == null || isNetworkMatched(network, serviceTypeNetwork)) {
+                if (Objects.equals(network, serviceTypeNetwork)) {
                     list.add(clients.valueAt(i));
                 }
             }
@@ -112,17 +109,23 @@
     }
 
     public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
-            @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
-            @NonNull Looper looper) {
+            @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
         this.executorProvider = executorProvider;
         this.socketClient = socketClient;
         this.sharedLog = sharedLog;
-        perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
-        handler = new Handler(looper);
+        this.perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
+        if (socketClient.getLooper() != null) {
+            this.handlerThread = null;
+            this.handler = new Handler(socketClient.getLooper());
+        } else {
+            this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
+            this.handlerThread.start();
+            this.handler = new Handler(handlerThread.getLooper());
+        }
     }
 
     private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
-        if (isRunningOnHandlerThread(handler)) {
+        if (this.handlerThread == null) {
             function.run();
         } else {
             handler.post(function);
@@ -130,6 +133,15 @@
     }
 
     /**
+     * Do the cleanup of the MdnsDiscoveryManager
+     */
+    public void shutDown() {
+        if (this.handlerThread != null) {
+            this.handlerThread.quitSafely();
+        }
+    }
+
+    /**
      * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
      * {@code listener} for receiving mDNS service discovery responses.
      *
@@ -239,7 +251,7 @@
     private void handleOnResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
             @Nullable Network network) {
         for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
+                : perNetworkServiceTypeClients.getByNetwork(network)) {
             serviceTypeClient.processResponse(packet, interfaceIndex, network);
         }
     }
@@ -254,7 +266,7 @@
     private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
             @Nullable Network network) {
         for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
+                : perNetworkServiceTypeClients.getByNetwork(network)) {
             serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 52c8622..d9bfb26 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -17,7 +17,6 @@
 package com.android.server.connectivity.mdns;
 
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,6 +33,7 @@
 import java.net.Inet6Address;
 import java.net.InetSocketAddress;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
@@ -205,6 +205,11 @@
         mSocketProvider.unrequestSocket(callback);
     }
 
+    @Override
+    public Looper getLooper() {
+        return mHandler.getLooper();
+    }
+
     private void sendMdnsPacket(@NonNull DatagramPacket packet, @Nullable Network targetNetwork) {
         final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
                 instanceof Inet6Address;
@@ -216,7 +221,10 @@
             final Network network = activeSockets.valueAt(i);
             // Check ip capability and network before sending packet
             if (((isIpv6 && socket.hasJoinedIpv6()) || (isIpv4 && socket.hasJoinedIpv4()))
-                    && isNetworkMatched(targetNetwork, network)) {
+                    // Contrary to MdnsUtils.isNetworkMatched, only send packets targeting
+                    // the null network to interfaces that have the null network (tethering
+                    // downstream interfaces).
+                    && Objects.equals(network, targetNetwork)) {
                 try {
                     socket.send(packet);
                 } catch (IOException e) {
@@ -248,12 +256,6 @@
         }
     }
 
-    /** Sends a mDNS request packet that asks for multicast response. */
-    @Override
-    public void sendMulticastPacket(@NonNull DatagramPacket packet) {
-        sendMulticastPacket(packet, null /* network */);
-    }
-
     /**
      * Sends a mDNS request packet via given network that asks for multicast response. Null network
      * means sending packet via all networks.
@@ -263,12 +265,6 @@
         mHandler.post(() -> sendMdnsPacket(packet, network));
     }
 
-    /** Sends a mDNS request packet that asks for unicast response. */
-    @Override
-    public void sendUnicastPacket(@NonNull DatagramPacket packet) {
-        sendUnicastPacket(packet, null /* network */);
-    }
-
     /**
      * Sends a mDNS request packet via given network that asks for unicast response. Null network
      * means sending packet via all networks.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index e56bd5b..c2c0db2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -201,7 +201,7 @@
                     searchOptions.getSubtypes(),
                     searchOptions.isPassiveMode(),
                     ++currentSessionId,
-                    searchOptions.getNetwork());
+                    network);
             if (hadReply) {
                 requestTaskFuture = scheduleNextRunLocked(taskConfig);
             } else {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 783b18a..1144d16 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -195,13 +195,11 @@
     }
 
     /** Sends a mDNS request packet that asks for multicast response. */
-    @Override
     public void sendMulticastPacket(@NonNull DatagramPacket packet) {
         sendMdnsPacket(packet, multicastPacketQueue);
     }
 
     /** Sends a mDNS request packet that asks for unicast response. */
-    @Override
     public void sendUnicastPacket(DatagramPacket packet) {
         if (useSeparateSocketForUnicast) {
             sendMdnsPacket(packet, unicastPacketQueue);
@@ -210,6 +208,36 @@
         }
     }
 
+    @Override
+    public void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        if (network != null) {
+            throw new IllegalArgumentException("This socket client does not support sending to "
+                    + "specific networks");
+        }
+        sendMulticastPacket(packet);
+    }
+
+    @Override
+    public void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        if (network != null) {
+            throw new IllegalArgumentException("This socket client does not support sending to "
+                    + "specific networks");
+        }
+        sendUnicastPacket(packet);
+    }
+
+    @Override
+    public void notifyNetworkRequested(
+            @NonNull MdnsServiceBrowserListener listener,
+            @Nullable Network network,
+            @NonNull SocketCreationCallback socketCreationCallback) {
+        if (network != null) {
+            throw new IllegalArgumentException("This socket client does not support requesting "
+                    + "specific networks");
+        }
+        socketCreationCallback.onSocketCreated(null);
+    }
+
     private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
         if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
             LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index c614cd3..deadc58 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
+import android.os.Looper;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -38,39 +39,34 @@
     /*** Set callback for receiving mDns response */
     void setCallback(@Nullable Callback callback);
 
-    /*** Sends a mDNS request packet that asks for multicast response. */
-    void sendMulticastPacket(@NonNull DatagramPacket packet);
+    /**
+     * Send a mDNS request packet via given network that asks for multicast response.
+     *
+     * <p>The socket client may use a null network to identify some or all interfaces, in which case
+     * passing null sends the packet to these.
+     */
+    void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
 
     /**
-     * Sends a mDNS request packet via given network that asks for multicast response. Null network
-     * means sending packet via all networks.
+     * Send a mDNS request packet via given network that asks for unicast response.
+     *
+     * <p>The socket client may use a null network to identify some or all interfaces, in which case
+     * passing null sends the packet to these.
      */
-    default void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        throw new UnsupportedOperationException(
-                "This socket client doesn't support per-network sending");
-    }
-
-    /*** Sends a mDNS request packet that asks for unicast response. */
-    void sendUnicastPacket(@NonNull DatagramPacket packet);
-
-    /**
-     * Sends a mDNS request packet via given network that asks for unicast response. Null network
-     * means sending packet via all networks.
-     */
-    default void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
-        throw new UnsupportedOperationException(
-                "This socket client doesn't support per-network sending");
-    }
+    void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
 
     /*** Notify that the given network is requested for mdns discovery / resolution */
-    default void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
-            @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback) {
-        socketCreationCallback.onSocketCreated(network);
-    }
+    void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
+            @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback);
 
     /*** Notify that the network is unrequested */
     default void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) { }
 
+    /*** Gets looper that used by the socket client */
+    default Looper getLooper() {
+        return null;
+    }
+
     /*** Callback for mdns response  */
     interface Callback {
         /*** Receive a mdns response */
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index 391ceac..e063af7 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -27,6 +27,7 @@
     min_sdk_version: "30",
     static_libs: [
         "connectivity_native_aidl_interface-V1-ndk",
+        "libmodules-utils-build",
     ],
     export_include_dirs: ["include"],
     cflags: [
diff --git a/service/libconnectivity/src/connectivity_native.cpp b/service/libconnectivity/src/connectivity_native.cpp
index a476498..f39bb0a 100644
--- a/service/libconnectivity/src/connectivity_native.cpp
+++ b/service/libconnectivity/src/connectivity_native.cpp
@@ -17,6 +17,7 @@
 #include "connectivity_native.h"
 
 #include <android/binder_manager.h>
+#include <android-modules-utils/sdk_level.h>
 #include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
 
 using aidl::android::net::connectivity::aidl::IConnectivityNative;
@@ -44,6 +45,7 @@
 }
 
 int AConnectivityNative_blockPortForBind(in_port_t port) {
+    if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
     std::shared_ptr<IConnectivityNative> c = getBinder();
     if (!c) {
         return EAGAIN;
@@ -52,6 +54,7 @@
 }
 
 int AConnectivityNative_unblockPortForBind(in_port_t port) {
+    if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
     std::shared_ptr<IConnectivityNative> c = getBinder();
     if (!c) {
         return EAGAIN;
@@ -60,6 +63,7 @@
 }
 
 int AConnectivityNative_unblockAllPortsForBind() {
+    if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
     std::shared_ptr<IConnectivityNative> c = getBinder();
     if (!c) {
         return EAGAIN;
@@ -68,6 +72,7 @@
 }
 
 int AConnectivityNative_getPortsBlockedForBind(in_port_t *ports, size_t *count) {
+    if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
     std::shared_ptr<IConnectivityNative> c = getBinder();
     if (!c) {
         return EAGAIN;
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f579093..98ad861 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -109,7 +109,6 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.ActivityManager.UidFrozenStateChangedCallback;
@@ -1279,6 +1278,18 @@
             return Binder.getCallingUid();
         }
 
+        public boolean isAtLeastS() {
+            return SdkLevel.isAtLeastS();
+        }
+
+        public boolean isAtLeastT() {
+            return SdkLevel.isAtLeastT();
+        }
+
+        public boolean isAtLeastU() {
+            return SdkLevel.isAtLeastU();
+        }
+
         /**
          * Get system properties to use in ConnectivityService.
          */
@@ -1391,7 +1402,7 @@
         @Nullable
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context, @NonNull final TelephonyManager tm) {
-            if (SdkLevel.isAtLeastT()) {
+            if (isAtLeastT()) {
                 return new CarrierPrivilegeAuthenticator(context, tm);
             } else {
                 return null;
@@ -1499,6 +1510,14 @@
         }
 
         /**
+         * As above but with a UID.
+         * @see CompatChanges#isChangeEnabled(long, int)
+         */
+        public boolean isChangeEnabled(final long changeId, final int uid) {
+            return CompatChanges.isChangeEnabled(changeId, uid);
+        }
+
+        /**
          * Call {@link InetDiagMessage#destroyLiveTcpSockets(Set, Set)}
          *
          * @param ranges target uid ranges
@@ -1711,7 +1730,7 @@
             // Even if it could, running on S would at least require mocking out the BPF map,
             // otherwise the unit tests will fail on pre-T devices where the seccomp filter blocks
             // the bpf syscall. http://aosp/1907693
-            if (SdkLevel.isAtLeastT()) {
+            if (mDeps.isAtLeastT()) {
                 mDscpPolicyTracker = new DscpPolicyTracker();
             }
         } catch (ErrnoException e) {
@@ -1721,13 +1740,13 @@
         mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
                 mContext);
 
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             mCdmps = new CompanionDeviceManagerProxyService(context);
         } else {
             mCdmps = null;
         }
 
-        if (SdkLevel.isAtLeastU()
+        if (mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION)) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
@@ -3195,8 +3214,6 @@
         sendStickyBroadcast(makeGeneralIntent(info, bcastType));
     }
 
-    // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
-    @SuppressLint("NewApi")
     // TODO: Set the mini sdk to 31 and remove @TargetApi annotation when b/205923322 is addressed.
     @TargetApi(Build.VERSION_CODES.S)
     private void sendStickyBroadcast(Intent intent) {
@@ -3232,7 +3249,7 @@
     private void applyMostRecentPolicyForConnectivityAction(BroadcastOptions options,
             NetworkInfo info) {
         // Delivery group policy APIs are only available on U+.
-        if (!SdkLevel.isAtLeastU()) return;
+        if (!mDeps.isAtLeastU()) return;
 
         final BroadcastOptionsShim optsShim = mDeps.makeBroadcastOptionsShim(options);
         try {
@@ -3310,7 +3327,7 @@
         }
 
         // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             mBpfNetMaps.setPullAtomCallback(mContext);
         }
         // Wait PermissionMonitor to finish the permission update. Then MultipathPolicyTracker won't
@@ -4520,7 +4537,7 @@
     @VisibleForTesting
     boolean shouldIgnoreValidationFailureAfterRoam(NetworkAgentInfo nai) {
         // T+ devices should use unregisterAfterReplacement.
-        if (SdkLevel.isAtLeastT()) return false;
+        if (mDeps.isAtLeastT()) return false;
 
         // If the network never roamed, return false. The check below is not sufficient if time
         // since boot is less than blockTimeOut, though that's extremely unlikely to happen.
@@ -4659,7 +4676,7 @@
             // for an unnecessarily long time.
             destroyNativeNetwork(nai);
         }
-        if (!nai.isCreated() && !SdkLevel.isAtLeastT()) {
+        if (!nai.isCreated() && !mDeps.isAtLeastT()) {
             // Backwards compatibility: send onNetworkDestroyed even if network was never created.
             // This can never run if the code above runs because shouldDestroyNativeNetwork is
             // false if the network was never created.
@@ -4743,7 +4760,7 @@
     }
 
     private void checkNrisConsistency(final NetworkRequestInfo nri) {
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             for (final NetworkRequestInfo n : mNetworkRequests.values()) {
                 if (n.mBinder != null && n.mBinder == nri.mBinder) {
                     // Temporary help to debug b/194394697 ; TODO : remove this function when the
@@ -6333,7 +6350,7 @@
                     + Arrays.toString(ranges) + "): netd command failed: " + e);
         }
 
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
         }
 
@@ -7127,7 +7144,7 @@
             @NonNull final NetworkCapabilities networkCapabilities) {
         // This check is added to fix the linter error for "current min is 30", which is not going
         // to happen because Connectivity service always run in S+.
-        if (!SdkLevel.isAtLeastS()) {
+        if (!mDeps.isAtLeastS()) {
             Log.wtf(TAG, "Connectivity service should always run in at least SDK S");
             return;
         }
@@ -8564,7 +8581,7 @@
         // On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
         // wildcard to allow apps to receive packets on all interfaces. This is required to accept
         // incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
-        return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
+        return mDeps.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8807,21 +8824,19 @@
         // else not handled
     }
 
-    // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
-    @SuppressLint("NewApi")
     private void sendIntent(PendingIntent pendingIntent, Intent intent) {
         mPendingIntentWakeLock.acquire();
         try {
             if (DBG) log("Sending " + pendingIntent);
             final BroadcastOptions options = BroadcastOptions.makeBasic();
-            if (SdkLevel.isAtLeastT()) {
+            if (mDeps.isAtLeastT()) {
                 // Explicitly disallow the receiver from starting activities, to prevent apps from
                 // utilizing the PendingIntent as a backdoor to do this.
                 options.setPendingIntentBackgroundActivityLaunchAllowed(false);
             }
             pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */,
                     null /* requiredPermission */,
-                    SdkLevel.isAtLeastT() ? options.toBundle() : null);
+                    mDeps.isAtLeastT() ? options.toBundle() : null);
         } catch (PendingIntent.CanceledException e) {
             if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
             mPendingIntentWakeLock.release();
@@ -9100,7 +9115,7 @@
 
     private void updateProfileAllowedNetworks() {
         // Netd command is not implemented before U.
-        if (!SdkLevel.isAtLeastU()) return;
+        if (!mDeps.isAtLeastU()) return;
 
         ensureRunningOnConnectivityServiceThread();
         final ArrayList<NativeUidRangeConfig> configs = new ArrayList<>();
@@ -9836,7 +9851,7 @@
             // in the absence of a satisfactory, scalable solution which follows an easy/standard
             // process to check the interface version, just use an SDK check. NetworkStack will
             // always be new enough when running on T+.
-            if (SdkLevel.isAtLeastT()) {
+            if (mDeps.isAtLeastT()) {
                 networkAgent.networkMonitor().notifyNetworkConnected(params);
             } else {
                 networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
@@ -11312,7 +11327,7 @@
         if (um.isManagedProfile(profile.getIdentifier())) {
             return true;
         }
-        if (SdkLevel.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
+        if (mDeps.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
         return false;
     }
 
@@ -11594,7 +11609,7 @@
 
     private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
         // Rate-limiting cannot run correctly before T because the BPF program is not loaded.
-        if (!SdkLevel.isAtLeastT()) return false;
+        if (!mDeps.isAtLeastT()) return false;
 
         final NetworkCapabilities agentCaps = networkAgent.networkCapabilities;
         // Only test networks (they cannot hold NET_CAPABILITY_INTERNET) and networks that provide
@@ -12101,7 +12116,7 @@
             throw new IllegalStateException(e);
         }
 
-        if (SdkLevel.isAtLeastU() && enable) {
+        if (mDeps.isAtLeastU() && enable) {
             try {
                 closeSocketsForFirewallChainLocked(chain);
             } catch (ErrnoException | SocketException | InterruptedIOException e) {
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e434649..904e4bd 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -167,6 +167,7 @@
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.FunctionalUtils.ignoreExceptions;
+import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
 import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
 import static com.android.testutils.MiscAsserts.assertContainsAll;
 import static com.android.testutils.MiscAsserts.assertContainsExactly;
@@ -376,7 +377,6 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -869,7 +869,7 @@
         @Override
         public void sendStickyBroadcast(Intent intent, Bundle options) {
             // Verify that delivery group policy APIs were used on U.
-            if (SdkLevel.isAtLeastU() && CONNECTIVITY_ACTION.equals(intent.getAction())) {
+            if (mDeps.isAtLeastU() && CONNECTIVITY_ACTION.equals(intent.getAction())) {
                 final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO,
                         NetworkInfo.class);
                 try {
@@ -1085,7 +1085,7 @@
         }
 
         private void onValidationRequested() throws Exception {
-            if (SdkLevel.isAtLeastT()) {
+            if (mDeps.isAtLeastT()) {
                 verify(mNetworkMonitor).notifyNetworkConnectedParcel(any());
             } else {
                 verify(mNetworkMonitor).notifyNetworkConnected(any(), any());
@@ -1932,6 +1932,7 @@
 
     class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
         final ConnectivityResources mConnRes;
+        final ArraySet<Pair<Long, Integer>> mEnabledChangeIds = new ArraySet<>();
 
         ConnectivityServiceDependencies(final Context mockResContext) {
             mConnRes = new ConnectivityResources(mockResContext);
@@ -1997,7 +1998,7 @@
         @Override
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context, @NonNull final TelephonyManager tm) {
-            return SdkLevel.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
+            return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
         }
 
         @Override
@@ -2096,6 +2097,61 @@
             }
         }
 
+        public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
+            final Pair<Long, Integer> data = new Pair<>(changeId, uid);
+            // mEnabledChangeIds is read on the handler thread and maybe the test thread, so
+            // make sure both threads see it before continuing.
+            visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+                if (enabled) {
+                    mEnabledChangeIds.add(data);
+                } else {
+                    mEnabledChangeIds.remove(data);
+                }
+            });
+        }
+
+        @Override
+        public boolean isChangeEnabled(final long changeId, final int uid) {
+            return mEnabledChangeIds.contains(new Pair<>(changeId, uid));
+        }
+
+        // In AOSP, build version codes are all over the place (e.g. at the time of this writing
+        // U == V). Define custom ones.
+        private static final int VERSION_UNMOCKED = -1;
+        private static final int VERSION_R = 1;
+        private static final int VERSION_S = 2;
+        private static final int VERSION_T = 3;
+        private static final int VERSION_U = 4;
+        private static final int VERSION_V = 5;
+        private static final int VERSION_MAX = VERSION_V;
+        private int mSdkLevel = VERSION_UNMOCKED;
+
+        private void setBuildSdk(final int sdkLevel) {
+            if (sdkLevel > VERSION_MAX) {
+                throw new IllegalArgumentException("setBuildSdk must not be called with"
+                        + " Build.VERSION constants but Dependencies.VERSION_* constants");
+            }
+            visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> mSdkLevel = sdkLevel);
+        }
+
+        @Override
+        public boolean isAtLeastS() {
+            return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastS()
+                    : mSdkLevel >= VERSION_S;
+        }
+
+        @Override
+        public boolean isAtLeastT() {
+            return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastT()
+                    : mSdkLevel >= VERSION_T;
+        }
+
+        @Override
+        public boolean isAtLeastU() {
+            return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastU()
+                    : mSdkLevel >= VERSION_U;
+        }
+
         @Override
         public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
             return mBpfNetMaps;
@@ -5933,7 +5989,7 @@
         doReturn(0).when(mResources).getInteger(R.integer.config_activelyPreferBadWifi);
         mPolicyTracker.reevaluate();
         waitForIdle();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             // U+ ignore the setting and always actively prefers bad wifi
             assertTrue(mService.mNetworkRanker.getConfiguration().activelyPreferBadWifi());
         } else {
@@ -6103,7 +6159,7 @@
     @Test
     public void testPreferBadWifi_doNotAvoid_doNotPrefer() throws Exception {
         // Starting with U this mode is no longer supported and can't actually be tested
-        assumeFalse(SdkLevel.isAtLeastU());
+        assumeFalse(mDeps.isAtLeastU());
         doTestPreferBadWifi(false /* avoidBadWifi */, false /* preferBadWifi */,
                 timeout -> timeout < 14_000);
     }
@@ -9772,7 +9828,7 @@
 
         final Set<Integer> excludedUids = new ArraySet<Integer>();
         excludedUids.add(VPN_UID);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             // On T onwards, the corresponding SDK sandbox UID should also be excluded
             excludedUids.add(toSdkSandboxUid(VPN_UID));
         }
@@ -9820,7 +9876,7 @@
         vpnDefaultCallbackAsUid.assertNoCallback();
 
         excludedUids.add(uid);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             // On T onwards, the corresponding SDK sandbox UID should also be excluded
             excludedUids.add(toSdkSandboxUid(uid));
         }
@@ -10554,7 +10610,7 @@
 
     private void verifyClatdStart(@Nullable InOrder inOrder, @NonNull String iface, int netId,
             @NonNull String nat64Prefix) throws Exception {
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             verifyWithOrder(inOrder, mClatCoordinator)
                 .clatStart(eq(iface), eq(netId), eq(new IpPrefix(nat64Prefix)));
         } else {
@@ -10564,7 +10620,7 @@
 
     private void verifyNeverClatdStart(@Nullable InOrder inOrder, @NonNull String iface)
             throws Exception {
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             verifyNeverWithOrder(inOrder, mClatCoordinator).clatStart(eq(iface), anyInt(), any());
         } else {
             verifyNeverWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), anyString());
@@ -10573,7 +10629,7 @@
 
     private void verifyClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
             throws Exception {
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             verifyWithOrder(inOrder, mClatCoordinator).clatStop();
         } else {
             verifyWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
@@ -10582,7 +10638,7 @@
 
     private void verifyNeverClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
             throws Exception {
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             verifyNeverWithOrder(inOrder, mClatCoordinator).clatStop();
         } else {
             verifyNeverWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
@@ -10775,7 +10831,7 @@
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
         }
 
@@ -10815,7 +10871,7 @@
         assertRoutesAdded(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, true);
         }
 
@@ -10834,7 +10890,7 @@
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
         verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_MOBILE_IFNAME);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
         }
 
@@ -10845,13 +10901,13 @@
         verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
         } else {
             verify(mMockNetd, never()).setNetworkAllowlist(any());
         }
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(MOBILE_IFNAME, false);
         }
 
@@ -10885,7 +10941,7 @@
         // assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
         assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(MOBILE_IFNAME, true);
         }
 
@@ -10898,20 +10954,20 @@
         networkCallback.assertNoCallback();
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
         }
 
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verify(mMockNetd).setNetworkAllowlist(any());
         } else {
             verify(mMockNetd, never()).setNetworkAllowlist(any());
         }
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             verifyWakeupModifyInterface(MOBILE_IFNAME, false);
         }
 
@@ -11352,7 +11408,7 @@
         mMockVpn.establish(lp, uid, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, uid);
 
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             // On T and above, VPN should have rules for null interface. Null Interface is a
             // wildcard and this accepts traffic from all the interfaces.
             // There are two expected invocations, one during the VPN initial
@@ -12341,7 +12397,7 @@
 
     @Test
     public void testUnderlyingNetworksWillBeSetInNetworkAgentInfoConstructor() throws Exception {
-        assumeTrue(SdkLevel.isAtLeastT());
+        assumeTrue(mDeps.isAtLeastT());
         final Network network1 = new Network(100);
         final Network network2 = new Network(101);
         final List<Network> underlyingNetworks = new ArrayList<>();
@@ -16234,7 +16290,7 @@
                 mCellAgent.getNetwork().netId,
                 toUidRangeStableParcels(allowedRanges),
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{config1User});
         } else {
             inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16252,7 +16308,7 @@
                 mCellAgent.getNetwork().netId,
                 toUidRangeStableParcels(allowedRanges),
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{config2Users});
         } else {
             inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16283,7 +16339,7 @@
                 mCellAgent.getNetwork().netId,
                 allowAllUidRangesParcel,
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellAllAllowedConfig});
         } else {
@@ -16302,7 +16358,7 @@
         // making the order of the list undeterministic. Thus, verify this in order insensitive way.
         final ArgumentCaptor<NativeUidRangeConfig[]> configsCaptor = ArgumentCaptor.forClass(
                 NativeUidRangeConfig[].class);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
             assertContainsAll(List.of(configsCaptor.getValue()),
                     List.of(cellAllAllowedConfig, enterpriseAllAllowedConfig));
@@ -16336,7 +16392,7 @@
                 mCellAgent.getNetwork().netId,
                 excludeAppRangesParcel,
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
             assertContainsAll(List.of(configsCaptor.getValue()),
                     List.of(cellExcludeAppConfig, enterpriseAllAllowedConfig));
@@ -16348,7 +16404,7 @@
         mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
                 r -> r.run(), listener);
         listener.expectOnComplete();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
             assertContainsAll(List.of(configsCaptor.getValue()),
                     List.of(cellAllAllowedConfig, enterpriseAllAllowedConfig));
@@ -16360,7 +16416,7 @@
         // disconnects.
         enterpriseAgent.disconnect();
         waitForIdle();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellAllAllowedConfig});
         } else {
@@ -16385,7 +16441,7 @@
                 List.of(prefBuilder.build()),
                 r -> r.run(), listener);
         listener.expectOnComplete();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{});
         } else {
             inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16413,7 +16469,7 @@
                 mCellAgent.getNetwork().netId,
                 excludeAppRangesParcel,
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellExcludeAppConfig});
         } else {
@@ -16437,7 +16493,7 @@
         // making the order of the list undeterministic. Thus, verify this in order insensitive way.
         final ArgumentCaptor<NativeUidRangeConfig[]> configsCaptor = ArgumentCaptor.forClass(
                 NativeUidRangeConfig[].class);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
             assertContainsAll(List.of(configsCaptor.getValue()),
                     List.of(enterpriseAllAllowedConfig, cellExcludeAppConfig));
@@ -16448,7 +16504,7 @@
         // Verify issuing with cellular set only when enterprise network disconnects.
         enterpriseAgent.disconnect();
         waitForIdle();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellExcludeAppConfig});
         } else {
@@ -16457,7 +16513,7 @@
 
         mCellAgent.disconnect();
         waitForIdle();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{});
         } else {
             inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16513,7 +16569,7 @@
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
         profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         final TestOnCompleteListener listener = new TestOnCompleteListener();
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             mCm.setProfileNetworkPreferences(testHandle,
                     List.of(profileNetworkPreferenceBuilder.build()),
                     r -> r.run(), listener);
@@ -16621,7 +16677,7 @@
                 agent.getNetwork().getNetId(),
                 intToUidRangeStableParcels(uids),
                 preferenceOrder);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids200Parcel);
         }
 
@@ -16629,7 +16685,7 @@
         uids.add(400);
         nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
         } else {
             cb.assertNoCallback();
@@ -16640,13 +16696,13 @@
                 agent.getNetwork().getNetId(),
                 intToUidRangeStableParcels(uids),
                 preferenceOrder);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids300400Parcel);
         }
 
         nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
             inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids200Parcel);
         } else {
@@ -16657,7 +16713,7 @@
         uids.add(600);
         nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
         } else {
             cb.assertNoCallback();
@@ -16666,7 +16722,7 @@
                 agent.getNetwork().getNetId(),
                 intToUidRangeStableParcels(uids),
                 preferenceOrder);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids600Parcel);
             inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids300400Parcel);
         }
@@ -16674,7 +16730,7 @@
         uids.clear();
         nc.setAllowedUids(uids);
         agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(agent, c -> c.getAllowedUids().isEmpty());
             inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids600Parcel);
         } else {
@@ -16727,7 +16783,7 @@
         // Cell gets to set the service UID as access UID
         ncb.setAllowedUids(serviceUidSet);
         mEthernetAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
-        if (SdkLevel.isAtLeastT() && hasAutomotiveFeature) {
+        if (mDeps.isAtLeastT() && hasAutomotiveFeature) {
             cb.expectCaps(mEthernetAgent, c -> c.getAllowedUids().equals(serviceUidSet));
         } else {
             // S and no automotive feature must ignore access UIDs.
@@ -16780,7 +16836,7 @@
         cb.expectAvailableThenValidatedCallbacks(mCellAgent);
         ncb.setAllowedUids(serviceUidSet);
         mCellAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(mCellAgent, c -> c.getAllowedUids().equals(serviceUidSet));
         } else {
             // S must ignore access UIDs.
@@ -16790,7 +16846,7 @@
         // ...but not to some other UID. Rejection sets UIDs to the empty set
         ncb.setAllowedUids(nonServiceUidSet);
         mCellAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             cb.expectCaps(mCellAgent, c -> c.getAllowedUids().isEmpty());
         } else {
             // S must ignore access UIDs.
@@ -17712,14 +17768,14 @@
 
     @Test
     public void testIgnoreValidationAfterRoamEnabled() throws Exception {
-        final boolean enabled = !SdkLevel.isAtLeastT();
+        final boolean enabled = !mDeps.isAtLeastT();
         doTestIgnoreValidationAfterRoam(5_000, enabled);
     }
 
     @Test
     public void testShouldIgnoreValidationFailureAfterRoam() {
         // Always disabled on T+.
-        assumeFalse(SdkLevel.isAtLeastT());
+        assumeFalse(mDeps.isAtLeastT());
 
         NetworkAgentInfo nai = fakeWifiNai(new NetworkCapabilities());
 
@@ -17973,7 +18029,7 @@
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         mCellAgent.connect(false /* validated */);
 
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             final String expectedPrefix = makeNflogPrefix(MOBILE_IFNAME,
                     mCellAgent.getNetwork().getNetworkHandle());
             verify(mMockNetd).wakeupAddInterface(MOBILE_IFNAME, expectedPrefix, PACKET_WAKEUP_MARK,
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 955be12..1997215 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -179,7 +179,7 @@
                 anyInt(), anyString(), anyString(), anyString(), anyInt());
         doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
         doReturn(mDiscoveryManager).when(mDeps)
-                .makeMdnsDiscoveryManager(any(), any(), any(), any());
+                .makeMdnsDiscoveryManager(any(), any(), any());
         doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any());
         doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any());
         mService = makeService();
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 89776e2..aeaca06 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -18,6 +18,9 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -62,21 +65,24 @@
     private static final String SERVICE_TYPE_2 = "_test._tcp.local";
     private static final Network NETWORK_1 = Mockito.mock(Network.class);
     private static final Network NETWORK_2 = Mockito.mock(Network.class);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1 =
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK =
             Pair.create(SERVICE_TYPE_1, null);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_1 =
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NETWORK_1 =
             Pair.create(SERVICE_TYPE_1, NETWORK_1);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2 =
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK =
             Pair.create(SERVICE_TYPE_2, null);
-    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_2 =
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_1 =
+            Pair.create(SERVICE_TYPE_2, NETWORK_1);
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_2 =
             Pair.create(SERVICE_TYPE_2, NETWORK_2);
 
     @Mock private ExecutorProvider executorProvider;
     @Mock private MdnsSocketClientBase socketClient;
-    @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
-    @Mock private MdnsServiceTypeClient mockServiceTypeClientOne1;
-    @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
-    @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo2;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientType1NullNetwork;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientType1Network1;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientType2NullNetwork;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientType2Network1;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientType2Network2;
 
     @Mock MdnsServiceBrowserListener mockListenerOne;
     @Mock MdnsServiceBrowserListener mockListenerTwo;
@@ -92,21 +98,28 @@
         thread = new HandlerThread("MdnsDiscoveryManagerTests");
         thread.start();
         handler = new Handler(thread.getLooper());
-        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog,
-                    thread.getLooper()) {
+        doReturn(thread.getLooper()).when(socketClient).getLooper();
+        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
+                sharedLog) {
                     @Override
                     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
                             @Nullable Network network) {
                         final Pair<String, Network> perNetworkServiceType =
                                 Pair.create(serviceType, network);
-                        if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1)) {
-                            return mockServiceTypeClientOne;
-                        } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1_1)) {
-                            return mockServiceTypeClientOne1;
-                        } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_2)) {
-                            return mockServiceTypeClientTwo;
-                        } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_2_2)) {
-                            return mockServiceTypeClientTwo2;
+                        if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK)) {
+                            return mockServiceTypeClientType1NullNetwork;
+                        } else if (perNetworkServiceType.equals(
+                                PER_NETWORK_SERVICE_TYPE_1_NETWORK_1)) {
+                            return mockServiceTypeClientType1Network1;
+                        } else if (perNetworkServiceType.equals(
+                                PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK)) {
+                            return mockServiceTypeClientType2NullNetwork;
+                        } else if (perNetworkServiceType.equals(
+                                PER_NETWORK_SERVICE_TYPE_2_NETWORK_1)) {
+                            return mockServiceTypeClientType2Network1;
+                        } else if (perNetworkServiceType.equals(
+                                PER_NETWORK_SERVICE_TYPE_2_NETWORK_2)) {
+                            return mockServiceTypeClientType2Network2;
                         }
                         return null;
                     }
@@ -143,11 +156,12 @@
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options);
         runOnHandler(() -> callback.onSocketCreated(null /* network */));
-        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
+        verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
 
-        when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
+        when(mockServiceTypeClientType1NullNetwork.stopSendAndReceive(mockListenerOne))
+                .thenReturn(true);
         runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
-        verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+        verify(mockServiceTypeClientType1NullNetwork).stopSendAndReceive(mockListenerOne);
         verify(socketClient).stopDiscovery();
     }
 
@@ -158,16 +172,16 @@
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options);
         runOnHandler(() -> callback.onSocketCreated(null /* network */));
-        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
+        verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
         runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
-        verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options);
+        verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
 
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
                 SERVICE_TYPE_2, mockListenerTwo, options);
         runOnHandler(() -> callback2.onSocketCreated(null /* network */));
-        verify(mockServiceTypeClientTwo).startSendAndReceive(mockListenerTwo, options);
+        verify(mockServiceTypeClientType2NullNetwork).startSendAndReceive(mockListenerTwo, options);
         runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
-        verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options);
+        verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options);
     }
 
     @Test
@@ -177,103 +191,106 @@
         final SocketCreationCallback callback = expectSocketCreationCallback(
                 SERVICE_TYPE_1, mockListenerOne, options1);
         runOnHandler(() -> callback.onSocketCreated(null /* network */));
-        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options1);
+        verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
+                mockListenerOne, options1);
         runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
-        verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options1);
+        verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options1);
 
         final MdnsSearchOptions options2 =
                 MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
                 SERVICE_TYPE_2, mockListenerTwo, options2);
         runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
-        verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options2);
+        verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options2);
 
         final MdnsPacket responseForServiceTypeOne = createMdnsPacket(SERVICE_TYPE_1);
         final int ifIndex = 1;
         runOnHandler(() -> discoveryManager.onResponseReceived(
                 responseForServiceTypeOne, ifIndex, null /* network */));
-        verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne, ifIndex,
-                null /* network */);
-        verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeOne, ifIndex,
-                null /* network */);
-        verify(mockServiceTypeClientTwo2).processResponse(responseForServiceTypeOne, ifIndex,
-                null /* network */);
+        // Packets for network null are only processed by the ServiceTypeClient for network null
+        verify(mockServiceTypeClientType1NullNetwork).processResponse(responseForServiceTypeOne,
+                ifIndex, null /* network */);
+        verify(mockServiceTypeClientType1Network1, never()).processResponse(any(), anyInt(), any());
+        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(), any());
 
         final MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
         runOnHandler(() -> discoveryManager.onResponseReceived(
                 responseForServiceTypeTwo, ifIndex, NETWORK_1));
-        verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeTwo, ifIndex,
-                NETWORK_1);
-        verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeTwo, ifIndex,
-                NETWORK_1);
-        verify(mockServiceTypeClientTwo2, never()).processResponse(responseForServiceTypeTwo,
+        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(any(), anyInt(),
+                eq(NETWORK_1));
+        verify(mockServiceTypeClientType1Network1).processResponse(responseForServiceTypeTwo,
                 ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(),
+                eq(NETWORK_1));
 
         final MdnsPacket responseForSubtype =
                 createMdnsPacket("subtype._sub._googlecast._tcp.local");
         runOnHandler(() -> discoveryManager.onResponseReceived(
                 responseForSubtype, ifIndex, NETWORK_2));
-        verify(mockServiceTypeClientOne).processResponse(responseForSubtype, ifIndex, NETWORK_2);
-        verify(mockServiceTypeClientOne1, never()).processResponse(
+        verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(
+                any(), anyInt(), eq(NETWORK_2));
+        verify(mockServiceTypeClientType1Network1, never()).processResponse(
+                any(), anyInt(), eq(NETWORK_2));
+        verify(mockServiceTypeClientType2Network2).processResponse(
                 responseForSubtype, ifIndex, NETWORK_2);
-        verify(mockServiceTypeClientTwo2).processResponse(responseForSubtype, ifIndex, NETWORK_2);
     }
 
     @Test
     public void testSocketCreatedAndDestroyed() throws IOException {
         // Create a ServiceTypeClient for SERVICE_TYPE_1 and NETWORK_1
-        final MdnsSearchOptions options1 =
+        final MdnsSearchOptions network1Options =
                 MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
         final SocketCreationCallback callback = expectSocketCreationCallback(
-                SERVICE_TYPE_1, mockListenerOne, options1);
+                SERVICE_TYPE_1, mockListenerOne, network1Options);
         runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
-        verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options1);
+        verify(mockServiceTypeClientType1Network1).startSendAndReceive(
+                mockListenerOne, network1Options);
 
-        // Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_2
-        final MdnsSearchOptions options2 =
-                MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
+        // Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_1
         final SocketCreationCallback callback2 = expectSocketCreationCallback(
-                SERVICE_TYPE_2, mockListenerTwo, options2);
-        runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
-        verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options2);
+                SERVICE_TYPE_2, mockListenerTwo, network1Options);
+        runOnHandler(() -> callback2.onSocketCreated(NETWORK_1));
+        verify(mockServiceTypeClientType2Network1).startSendAndReceive(
+                mockListenerTwo, network1Options);
 
         // Receive a response, it should be processed on both clients.
         final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
         final int ifIndex = 1;
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, null /* network */));
-        verify(mockServiceTypeClientOne1).processResponse(response, ifIndex, null /* network */);
-        verify(mockServiceTypeClientTwo2).processResponse(response, ifIndex, null /* network */);
+                response, ifIndex, NETWORK_1));
+        verify(mockServiceTypeClientType1Network1).processResponse(response, ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientType2Network1).processResponse(response, ifIndex, NETWORK_1);
 
-        // The client for NETWORK_1 receives the callback that the NETWORK_1 has been destroyed,
+        // The first callback receives a notification that the network has been destroyed,
         // mockServiceTypeClientOne1 should send service removed notifications and remove from the
         // list of clients.
         runOnHandler(() -> callback.onAllSocketsDestroyed(NETWORK_1));
-        verify(mockServiceTypeClientOne1).notifySocketDestroyed();
+        verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
 
-        // Receive a response again, it should be processed only on mockServiceTypeClientTwo2.
-        // Because the mockServiceTypeClientOne1 is removed from the list of clients, it is no
-        // longer able to process responses.
+        // Receive a response again, it should be processed only on
+        // mockServiceTypeClientType2Network1. Because the mockServiceTypeClientType1Network1 is
+        // removed from the list of clients, it is no longer able to process responses.
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, null /* network */));
-        verify(mockServiceTypeClientOne1, times(1))
-                .processResponse(response, ifIndex, null /* network */);
-        verify(mockServiceTypeClientTwo2, times(2))
-                .processResponse(response, ifIndex, null /* network */);
+                response, ifIndex, NETWORK_1));
+        // Still times(1) as a response was received once previously
+        verify(mockServiceTypeClientType1Network1, times(1))
+                .processResponse(response, ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientType2Network1, times(2))
+                .processResponse(response, ifIndex, NETWORK_1);
 
-        // The client for NETWORK_2 receives the callback that the NETWORK_1 has been destroyed,
+        // The client for NETWORK_1 receives the callback that the NETWORK_2 has been destroyed,
         // mockServiceTypeClientTwo2 shouldn't send any notifications.
-        runOnHandler(() -> callback2.onAllSocketsDestroyed(NETWORK_1));
-        verify(mockServiceTypeClientTwo2, never()).notifySocketDestroyed();
+        runOnHandler(() -> callback2.onAllSocketsDestroyed(NETWORK_2));
+        verify(mockServiceTypeClientType2Network1, never()).notifySocketDestroyed();
 
-        // Receive a response again, mockServiceTypeClientTwo2 is still in the list of clients, it's
-        // still able to process responses.
+        // Receive a response again, mockServiceTypeClientType2Network1 is still in the list of
+        // clients, it's still able to process responses.
         runOnHandler(() -> discoveryManager.onResponseReceived(
-                response, ifIndex, null /* network */));
-        verify(mockServiceTypeClientOne1, times(1))
-                .processResponse(response, ifIndex, null /* network */);
-        verify(mockServiceTypeClientTwo2, times(3))
-                .processResponse(response, ifIndex, null /* network */);
+                response, ifIndex, NETWORK_1));
+        verify(mockServiceTypeClientType1Network1, times(1))
+                .processResponse(response, ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientType2Network1, times(3))
+                .processResponse(response, ifIndex, NETWORK_1);
     }
 
     private MdnsPacket createMdnsPacket(String serviceType) {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index c6137c6..87ba5d7 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -38,6 +38,7 @@
 import android.os.HandlerThread;
 
 import com.android.net.module.util.HexDump;
+import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -66,7 +67,7 @@
     @Mock private MdnsInterfaceSocket mSocket;
     @Mock private MdnsServiceBrowserListener mListener;
     @Mock private MdnsSocketClientBase.Callback mCallback;
-    @Mock private MdnsSocketClientBase.SocketCreationCallback mSocketCreationCallback;
+    @Mock private SocketCreationCallback mSocketCreationCallback;
     private MdnsMultinetworkSocketClient mSocketClient;
     private Handler mHandler;
 
@@ -113,22 +114,36 @@
                 InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
         final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
                 InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
-        doReturn(true).when(mSocket).hasJoinedIpv4();
-        doReturn(true).when(mSocket).hasJoinedIpv6();
-        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+        final MdnsInterfaceSocket tetherIfaceSock1 = mock(MdnsInterfaceSocket.class);
+        final MdnsInterfaceSocket tetherIfaceSock2 = mock(MdnsInterfaceSocket.class);
+        for (MdnsInterfaceSocket socket : List.of(mSocket, tetherIfaceSock1, tetherIfaceSock2)) {
+            doReturn(true).when(socket).hasJoinedIpv4();
+            doReturn(true).when(socket).hasJoinedIpv6();
+            doReturn(createEmptyNetworkInterface()).when(socket).getInterface();
+        }
+
         // Notify socket created
         callback.onSocketCreated(mNetwork, mSocket, List.of());
         verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+        callback.onSocketCreated(null, tetherIfaceSock1, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(null);
+        callback.onSocketCreated(null, tetherIfaceSock2, List.of());
+        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
 
         // Send packet to IPv4 with target network and verify sending has been called.
         mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket).send(ipv4Packet);
+        verify(tetherIfaceSock1, never()).send(any());
+        verify(tetherIfaceSock2, never()).send(any());
 
         // Send packet to IPv6 without target network and verify sending has been called.
-        mSocketClient.sendMulticastPacket(ipv6Packet);
+        mSocketClient.sendMulticastPacket(ipv6Packet, null);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
-        verify(mSocket).send(ipv6Packet);
+        verify(mSocket, never()).send(ipv6Packet);
+        verify(tetherIfaceSock1).send(ipv6Packet);
+        verify(tetherIfaceSock2).send(ipv6Packet);
     }
 
     @Test
@@ -181,61 +196,86 @@
 
     @Test
     public void testSocketRemovedAfterNetworkUnrequested() throws IOException {
-        // Request a socket
-        final SocketCallback callback = expectSocketCallback(mListener, mNetwork);
+        // Request sockets on all networks
+        final SocketCallback callback = expectSocketCallback(mListener, null);
         final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
                 InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+
+        // Notify 3 socket created, including 2 tethered interfaces (null network)
+        final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+        final MdnsInterfaceSocket socket3 = mock(MdnsInterfaceSocket.class);
         doReturn(true).when(mSocket).hasJoinedIpv4();
         doReturn(true).when(mSocket).hasJoinedIpv6();
-        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
-        // Notify socket created
-        callback.onSocketCreated(mNetwork, mSocket, List.of());
-        verify(mSocketCreationCallback).onSocketCreated(mNetwork);
-
-        // Send IPv4 packet and verify sending has been called.
-        mSocketClient.sendMulticastPacket(ipv4Packet);
-        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
-        verify(mSocket).send(ipv4Packet);
-
-        // Request another socket with null network
-        final MdnsServiceBrowserListener listener2 = mock(MdnsServiceBrowserListener.class);
-        final Network network2 = mock(Network.class);
-        final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
-        final SocketCallback callback2 = expectSocketCallback(listener2, null);
         doReturn(true).when(socket2).hasJoinedIpv4();
         doReturn(true).when(socket2).hasJoinedIpv6();
+        doReturn(true).when(socket3).hasJoinedIpv4();
+        doReturn(true).when(socket3).hasJoinedIpv6();
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
         doReturn(createEmptyNetworkInterface()).when(socket2).getInterface();
-        // Notify socket created for two networks.
-        callback2.onSocketCreated(mNetwork, mSocket, List.of());
-        callback2.onSocketCreated(network2, socket2, List.of());
-        verify(mSocketCreationCallback, times(2)).onSocketCreated(mNetwork);
-        verify(mSocketCreationCallback).onSocketCreated(network2);
+        doReturn(createEmptyNetworkInterface()).when(socket3).getInterface();
 
-        // Send IPv4 packet and verify sending to two sockets.
-        mSocketClient.sendMulticastPacket(ipv4Packet);
+        callback.onSocketCreated(mNetwork, mSocket, List.of());
+        callback.onSocketCreated(null, socket2, List.of());
+        callback.onSocketCreated(null, socket3, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+        verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+        // Send IPv4 packet on the non-null Network and verify sending has been called.
+        mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
-        verify(mSocket, times(2)).send(ipv4Packet);
-        verify(socket2).send(ipv4Packet);
+        verify(mSocket).send(ipv4Packet);
+        verify(socket2, never()).send(any());
+        verify(socket3, never()).send(any());
 
-        // Unrequest another socket
+        // Request another socket with null network, get the same interfaces
+        final SocketCreationCallback socketCreationCb2 = mock(SocketCreationCallback.class);
+        final MdnsServiceBrowserListener listener2 = mock(MdnsServiceBrowserListener.class);
+
+        // requestSocket is called a second time
+        final ArgumentCaptor<SocketCallback> callback2Captor =
+                ArgumentCaptor.forClass(SocketCallback.class);
+        mHandler.post(() -> mSocketClient.notifyNetworkRequested(
+                listener2, null, socketCreationCb2));
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+        verify(mProvider, times(2)).requestSocket(eq(null), callback2Captor.capture());
+        final SocketCallback callback2 = callback2Captor.getAllValues().get(1);
+
+        // Notify socket created for all networks.
+        callback2.onSocketCreated(mNetwork, mSocket, List.of());
+        callback2.onSocketCreated(null, socket2, List.of());
+        callback2.onSocketCreated(null, socket3, List.of());
+        verify(socketCreationCb2).onSocketCreated(mNetwork);
+        verify(socketCreationCb2, times(2)).onSocketCreated(null);
+
+        // Send IPv4 packet to null network and verify sending to the 2 tethered interface sockets.
+        mSocketClient.sendMulticastPacket(ipv4Packet, null);
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+        // ipv4Packet still sent only once on mSocket: times(1) matches the packet sent earlier on
+        // mNetwork
+        verify(mSocket, times(1)).send(ipv4Packet);
+        verify(socket2).send(ipv4Packet);
+        verify(socket3).send(ipv4Packet);
+
+        // Unregister the second request
         mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(listener2));
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
 
-        // Send IPv4 packet again and verify only sending via mSocket
-        mSocketClient.sendMulticastPacket(ipv4Packet);
+        // Send IPv4 packet again and verify it's still sent a second time
+        mSocketClient.sendMulticastPacket(ipv4Packet, null);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
-        verify(mSocket, times(3)).send(ipv4Packet);
-        verify(socket2).send(ipv4Packet);
+        verify(socket2, times(2)).send(ipv4Packet);
+        verify(socket3, times(2)).send(ipv4Packet);
 
-        // Unrequest remaining socket
+        // Unrequest remaining sockets
         mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
 
         // Send IPv4 packet and verify no more sending.
-        mSocketClient.sendMulticastPacket(ipv4Packet);
+        mSocketClient.sendMulticastPacket(ipv4Packet, null);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
-        verify(mSocket, times(3)).send(ipv4Packet);
-        verify(socket2).send(ipv4Packet);
+        verify(mSocket, times(1)).send(ipv4Packet);
+        verify(socket2, times(2)).send(ipv4Packet);
+        verify(socket3, times(2)).send(ipv4Packet);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 42d296b..f15e8ff 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -952,7 +952,7 @@
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         // Send twice for IPv4 and IPv6
         inOrder.verify(mockSocketClient, times(2)).sendUnicastPacket(srvTxtQueryCaptor.capture(),
-                eq(null) /* network */);
+                eq(mockNetwork));
 
         final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
                 new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
@@ -987,7 +987,7 @@
                 ArgumentCaptor.forClass(DatagramPacket.class);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         inOrder.verify(mockSocketClient, times(2)).sendMulticastPacket(addressQueryCaptor.capture(),
-                eq(null) /* network */);
+                eq(mockNetwork));
 
         final MdnsPacket addressQueryPacket = MdnsPacket.parse(
                 new MdnsPacketReader(addressQueryCaptor.getValue()));
@@ -1255,17 +1255,17 @@
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         if (expectsUnicastResponse) {
             verify(mockSocketClient).sendUnicastPacket(
-                    expectedIPv4Packets[index], null /* network */);
+                    expectedIPv4Packets[index], mockNetwork);
             if (multipleSocketDiscovery) {
                 verify(mockSocketClient).sendUnicastPacket(
-                        expectedIPv6Packets[index], null /* network */);
+                        expectedIPv6Packets[index], mockNetwork);
             }
         } else {
             verify(mockSocketClient).sendMulticastPacket(
-                    expectedIPv4Packets[index], null /* network */);
+                    expectedIPv4Packets[index], mockNetwork);
             if (multipleSocketDiscovery) {
                 verify(mockSocketClient).sendMulticastPacket(
-                        expectedIPv6Packets[index], null /* network */);
+                        expectedIPv6Packets[index], mockNetwork);
             }
         }
     }