Merge "Update the SDK version to 34 in ConnectivityTestsLatestSdkDefaults"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index d2f6d6a..c1d6a89 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -224,6 +224,9 @@
           "exclude-annotation": "androidx.test.filters.LargeTest"
         }
       ]
+    },
+    {
+        "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "mainline-postsubmit": [
@@ -231,6 +234,10 @@
     {
       "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "keywords": ["sim"]
+    },
+    {
+        "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+        "keywords": ["sim"]
     }
   ],
   "imports": [
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/netd/Android.bp b/netd/Android.bp
index 473460d..4325d89 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -35,6 +35,9 @@
         "BpfHandler.cpp",
         "NetdUpdatable.cpp",
     ],
+    static_libs: [
+        "libmodules-utils-build",
+    ],
     shared_libs: [
         "libbase",
         "liblog",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 8081d12..6409374 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -21,6 +21,7 @@
 #include <linux/bpf.h>
 
 #include <android-base/unique_fd.h>
+#include <android-modules-utils/sdk_level.h>
 #include <bpf/WaitForProgsLoaded.h>
 #include <log/log.h>
 #include <netdutils/UidConstants.h>
@@ -74,9 +75,11 @@
 }
 
 static Status initPrograms(const char* cg2_path) {
+    if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
+
     unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
     if (cg_fd == -1) {
-        int ret = errno;
+        const int ret = errno;
         ALOGE("Failed to open the cgroup directory: %s", strerror(ret));
         return statusFromErrno(ret, "Open the cgroup directory failed");
     }
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/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 06ca914..d3cbf82 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.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.isRunningOnHandlerThread;
 
 import android.Manifest.permission;
 import android.annotation.NonNull;
@@ -25,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;
@@ -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 =
@@ -109,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);
@@ -127,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.
      *
@@ -236,11 +251,19 @@
     private void handleOnResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
             @Nullable Network network) {
         for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByNetwork(network)) {
+                : getMdnsServiceTypeClient(network)) {
             serviceTypeClient.processResponse(packet, interfaceIndex, network);
         }
     }
 
+    private List<MdnsServiceTypeClient> getMdnsServiceTypeClient(@Nullable Network network) {
+        if (socketClient.supportsRequestingSpecificNetworks()) {
+            return perNetworkServiceTypeClients.getByNetwork(network);
+        } else {
+            return perNetworkServiceTypeClients.getByNetwork(null);
+        }
+    }
+
     @Override
     public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
             @Nullable Network network) {
@@ -251,7 +274,7 @@
     private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
             @Nullable Network network) {
         for (MdnsServiceTypeClient serviceTypeClient
-                : perNetworkServiceTypeClients.getByNetwork(network)) {
+                : getMdnsServiceTypeClient(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 d6f2d06..73e4497 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -205,6 +205,16 @@
         mSocketProvider.unrequestSocket(callback);
     }
 
+    @Override
+    public Looper getLooper() {
+        return mHandler.getLooper();
+    }
+
+    @Override
+    public boolean supportsRequestingSpecificNetworks() {
+        return true;
+    }
+
     private void sendMdnsPacket(@NonNull DatagramPacket packet, @Nullable Network targetNetwork) {
         final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
                 instanceof Inet6Address;
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 1144d16..b982644 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -238,6 +238,11 @@
         socketCreationCallback.onSocketCreated(null);
     }
 
+    @Override
+    public boolean supportsRequestingSpecificNetworks() {
+        return false;
+    }
+
     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 22cf326..e0762f9 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;
@@ -61,6 +62,14 @@
     /*** 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;
+    }
+
+    /** Returns whether the socket client support requesting per network */
+    boolean supportsRequestingSpecificNetworks();
+
     /*** 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 51df1dd..c95295c 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -861,8 +861,7 @@
 
     // A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
     // the world when it changes.
-    @VisibleForTesting
-    protected final ProxyTracker mProxyTracker;
+    private final ProxyTracker mProxyTracker;
 
     final private SettingsObserver mSettingsObserver;
 
@@ -1278,6 +1277,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.
          */
@@ -1390,7 +1401,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;
@@ -1498,6 +1509,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
@@ -1710,7 +1729,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) {
@@ -1720,13 +1739,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() {
@@ -1846,6 +1865,13 @@
         mHandler.sendEmptyMessage(EVENT_INGRESS_RATE_LIMIT_CHANGED);
     }
 
+    @VisibleForTesting
+    void simulateUpdateProxyInfo(@Nullable final Network network,
+            @NonNull final ProxyInfo proxyInfo) {
+        Message.obtain(mHandler, EVENT_PROXY_HAS_CHANGED,
+                new Pair<>(network, proxyInfo)).sendToTarget();
+    }
+
     private void handleAlwaysOnNetworkRequest(
             NetworkRequest networkRequest, String settingName, boolean defaultValue) {
         final boolean enable = toBool(Settings.Global.getInt(
@@ -3229,7 +3255,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 {
@@ -3307,7 +3333,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
@@ -4517,7 +4543,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.
@@ -4656,7 +4682,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.
@@ -4740,7 +4766,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
@@ -6330,7 +6356,7 @@
                     + Arrays.toString(ranges) + "): netd command failed: " + e);
         }
 
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
         }
 
@@ -7124,7 +7150,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;
         }
@@ -8561,7 +8587,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) {
@@ -8809,14 +8835,14 @@
         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();
@@ -9095,7 +9121,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<>();
@@ -9831,7 +9857,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,
@@ -11307,7 +11333,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;
     }
 
@@ -11589,7 +11615,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
@@ -12096,7 +12122,7 @@
             throw new IllegalStateException(e);
         }
 
-        if (SdkLevel.isAtLeastU() && enable) {
+        if (mDeps.isAtLeastU() && enable) {
             try {
                 closeSocketsForFirewallChainLocked(chain);
             } catch (ErrnoException | SocketException | InterruptedIOException e) {
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
index bc0929c..b3cbb2a 100644
--- a/service/src/com/android/server/connectivity/ProxyTracker.java
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -330,7 +330,7 @@
     public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
         synchronized (mProxyLock) {
             if (Objects.equals(mDefaultProxy, proxyInfo)) return;
-            if (proxyInfo != null &&  !proxyInfo.isValid()) {
+            if (proxyInfo != null && !proxyInfo.isValid()) {
                 if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
                 return;
             }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index a281aed..6b21dac 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -18,9 +18,7 @@
 
 import static android.app.job.JobScheduler.RESULT_SUCCESS;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
-import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
-import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS;
+import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
 
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
@@ -119,10 +117,6 @@
     protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
     protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
 
-    // TODO: Update BatteryManager.BATTERY_PLUGGED_ANY as @TestApi
-    public static final int BATTERY_PLUGGED_ANY =
-            BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS;
-
     private static final String NETWORK_STATUS_SEPARATOR = "\\|";
     private static final int SECOND_IN_MS = 1000;
     static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
diff --git a/tests/cts/tethering/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml
index 9b33afe..dd5b23e 100644
--- a/tests/cts/tethering/AndroidTestTemplate.xml
+++ b/tests/cts/tethering/AndroidTestTemplate.xml
@@ -20,6 +20,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e434649..c79c295 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);
         }
 
@@ -11276,6 +11332,212 @@
         assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
     }
 
+    /*
+     * Note for maintainers about how PAC proxies are implemented in Android.
+     *
+     * Generally, a proxy is just a hostname and a port to which requests are sent, instead of
+     * sending them directly. Most HTTP libraries know to use this protocol, and the Java
+     * language has properties to help handling these :
+     *   https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+     * Unfortunately these properties are very old and do not take multi-networking into account.
+     *
+     * A PAC proxy consists of a javascript file stored on a server, and the client is expected to
+     * download the file and interpret the javascript for each HTTP request to know where to direct
+     * it. The device must therefore run code (namely, a javascript interpreter) to interpret the
+     * PAC file correctly. Most HTTP libraries do not know how to do this, since they do not
+     * embark a javascript interpreter (and it would be generally unreasonable for them to do
+     * so). Some apps, notably browsers, do know how to do this, but they are the exception rather
+     * than the rule.
+     * So to support most apps, Android embarks the javascript interpreter. When a network is
+     * configured to have a PAC proxy, Android will first set the ProxyInfo object to an object
+     * that contains the PAC URL (to communicate that to apps that know how to use it), then
+     * download the PAC file and start a native process which will open a server on localhost,
+     * and uses the interpreter inside WebView to interpret the PAC file. This server takes
+     * a little bit of time to start and will listen on a random port. When the port is known,
+     * the framework receives a notification and it updates the ProxyInfo in LinkProperties
+     * as well as send a broadcast to inform apps. This new ProxyInfo still contains the PAC URL,
+     * but it also contains "localhost" as the host and the port that the server listens to as
+     * the port. This will let HTTP libraries that don't have a javascript interpreter work,
+     * because they'll send the requests to this server running on localhost on the correct port,
+     * and this server will do what is appropriate with each request according to the PAC file.
+     *
+     * Note that at the time of this writing, Android does not support starting multiple local
+     * PAC servers, though this would be possible in theory by just starting multiple instances
+     * of this process running their server on different ports. As a stopgap measure, Android
+     * keeps one local server which is always the one for the default network. If a non-default
+     * network has a PAC proxy, it will have a LinkProperties with a port of -1, which means it
+     * could still be used by apps that know how to use the PAC URL directly, but not by HTTP
+     * libs that don't know how to do that. When a network with a PAC proxy becomes the default,
+     * the local server is started. When a network without a PAC proxy becomes the default, the
+     * local server is stopped if it is running (and the LP for the old default network should
+     * be reset to have a port of -1).
+     *
+     * In principle, each network can be configured with a different proxy (typically in the
+     * network settings for a Wi-Fi network). These end up exposed in the LinkProperties of the
+     * relevant network.
+     * Android also exposes ConnectivityManager#getDefaultProxy, which is simply the proxy for
+     * the default network. This was retrofitted from a time where Android did not support multiple
+     * concurrent networks, hence the difficult architecture.
+     * Note that there is also a "global" proxy that can be set by the DPM. When this is set,
+     * it overrides the proxy settings for every single network at the same time – that is, the
+     * system behaves as if the global proxy is the configured proxy for each network.
+     *
+     * Generally there are four ways for apps to look up the proxy.
+     * - Looking up the ProxyInfo in the LinkProperties for a network.
+     * - Listening to the PROXY_CHANGED_ACTION broadcast
+     * - Calling ConnectivityManager#getDefaultProxy, or ConnectivityManager#getProxyForNetwork
+     *   which can be used to retrieve the proxy for any given network or the default network by
+     *   passing null.
+     * - Reading the standard JVM properties (http{s,}.proxy{Host,Port}). See the Java
+     *   distribution documentation for details on how these are supposed to work :
+     *    https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+     *   In Android, these are set by ActivityThread in each process in response to the broadcast.
+     *   Many apps actually use these, and it's important they work because it's part of the
+     *   Java standard, meaning they need to be set for existing Java libs to work on Android.
+     */
+    @Test
+    public void testPacProxy() throws Exception {
+        final Uri pacUrl = Uri.parse("https://pac-url");
+        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellAgent.connect(true);
+
+        final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, wifiCallback);
+
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiAgent.connect(true);
+        wifiCallback.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+
+        final ProxyInfo initialProxyInfo = ProxyInfo.buildPacProxy(pacUrl);
+        final LinkProperties testLinkProperties = new LinkProperties();
+        testLinkProperties.setHttpProxy(initialProxyInfo);
+        mWiFiAgent.sendLinkProperties(testLinkProperties);
+        wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+
+        // At first the local PAC proxy server is unstarted (see the description of what the local
+        // server is in the comment at the top of this method). It will contain the PAC URL and a
+        // port of -1 because it is unstarted. Check that all ways of getting that proxy info
+        // returns the same object that was initially created.
+        final ProxyInfo unstartedDefaultProxyInfo = mService.getProxyForNetwork(null);
+        final ProxyInfo unstartedWifiProxyInfo = mService.getProxyForNetwork(
+                mWiFiAgent.getNetwork());
+        final LinkProperties unstartedLp =
+                mService.getLinkProperties(mWiFiAgent.getNetwork());
+
+        assertEquals(initialProxyInfo, unstartedDefaultProxyInfo);
+        assertEquals(initialProxyInfo, unstartedWifiProxyInfo);
+        assertEquals(initialProxyInfo, unstartedLp.getHttpProxy());
+
+        // Make sure the cell network is unaffected. The LP are per-network and each network has
+        // its own configuration. The default proxy and broadcast are system-wide, and the JVM
+        // properties are per-process, and therefore can have only one value at any given time,
+        // so the code sets them to the proxy of the default network (TODO : really, since the
+        // default process is per-network, the JVM properties (http.proxyHost family – see
+        // the comment at the top of the method for details about these) also should be per-network
+        // and even the broadcast contents should be but none of this is implemented). The LP are
+        // still per-network, and a process that wants to use a non-default network is supposed to
+        // look up the proxy in its LP and it has to be correct.
+        assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+        assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+
+        // Simulate PacManager sending the notification that the local server has started
+        final ProxyInfo servingProxyInfo = new ProxyInfo(pacUrl, 2097);
+        final ExpectedBroadcast b1 = expectProxyChangeAction(servingProxyInfo);
+        mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+//      wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+        b1.expectBroadcast();
+
+        final ProxyInfo startedDefaultProxyInfo = mService.getProxyForNetwork(null);
+        final ProxyInfo startedWifiProxyInfo = mService.getProxyForNetwork(
+                mWiFiAgent.getNetwork());
+        final LinkProperties startedLp = mService.getLinkProperties(mWiFiAgent.getNetwork());
+        // TODO : activate these tests when b/138810051 is fixed.
+//      assertEquals(servingProxyInfo, startedDefaultProxyInfo);
+//      assertEquals(servingProxyInfo, startedWifiProxyInfo);
+//      assertEquals(servingProxyInfo, startedLp.getHttpProxy());
+//      // Make sure the cell network is still unaffected
+//      assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+//      assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+//      final Uri ethPacUrl = Uri.parse("https://ethernet-pac-url");
+//      final TestableNetworkCallback ethernetCallback = new TestableNetworkCallback();
+//      final NetworkRequest ethernetRequest = new NetworkRequest.Builder()
+//              .addTransportType(TRANSPORT_ETHERNET).build();
+//      mEthernetAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+//      mCm.registerNetworkCallback(ethernetRequest, ethernetCallback);
+//      mEthernetAgent.connect(true);
+//      ethernetCallback.expectAvailableThenValidatedCallbacks(mEthernetAgent);
+//      // Wifi is no longer the default, so it should get a callback to LP changed with a PAC
+//      // proxy but a port of -1 (since the local proxy doesn't serve wifi now)
+//      wifiCallback.expect(LINK_PROPERTIES_CHANGED, mWiFiAgent,
+//              lp -> lp.getLp().getHttpProxy().getPort() == -1
+//                      && lp.getLp().getHttpProxy().isPacProxy());
+//      wifiCallback.expect(CallbackEntry.LOSING, mWiFiAgent);
+//
+//      final ProxyInfo ethProxy = ProxyInfo.buildPacProxy(ethPacUrl);
+//      final LinkProperties ethLinkProperties = new LinkProperties();
+//      ethLinkProperties.setHttpProxy(ethProxy);
+//      mEthernetAgent.sendLinkProperties(ethLinkProperties);
+//      ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+//      // Default network is Ethernet
+//      assertEquals(ethProxy, mService.getProxyForNetwork(null));
+//      assertEquals(ethProxy, mService.getProxyForNetwork(mEthernetAgent.getNetwork()));
+//      // Proxy info for WiFi ideally should be the old one with the old server still running,
+//      // but as the PAC code only handles one server at any given time, this can't work. Wifi
+//      // having null as a proxy also won't work (apps using WiFi will try to access the network
+//      // without proxy) but is not as bad as having the new proxy (that would send requests
+//      // over the default network).
+//      assertEquals(initialProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+//      assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+//      final ProxyInfo servingEthProxy = new ProxyInfo(pacUrl, 2099);
+//      final ExpectedBroadcast b2 = expectProxyChangeAction(servingEthProxy);
+//      final ExpectedBroadcast b3 = expectProxyChangeAction(servingProxyInfo);
+//
+//      mService.simulateUpdateProxyInfo(mEthernetAgent.getNetwork(), servingEthProxy);
+//      ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+//      assertEquals(servingEthProxy, mService.getProxyForNetwork(null));
+//      assertEquals(servingEthProxy, mService.getProxyForNetwork(
+//              mEthernetAgent.getNetwork()));
+//      assertEquals(initialProxyInfo,
+//              mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+//      assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//      b2.expectBroadcast();
+//
+//      // Ethernet disconnects, back to WiFi
+//      mEthernetAgent.disconnect();
+//      ethernetCallback.expect(CallbackEntry.LOST, mEthernetAgent);
+//      wifiCallback.assertNoCallback();
+//
+//      assertEquals(initialProxyInfo, mService.getProxyForNetwork(null));
+//      assertEquals(initialProxyInfo,
+//              mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+//      assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+//      // After a while the PAC file for wifi is resolved again
+//      mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+//      wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+//      assertEquals(servingProxyInfo, mService.getProxyForNetwork(null));
+//      assertEquals(servingProxyInfo,
+//              mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+//      assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//      b3.expectBroadcast();
+//
+//      // Expect a broadcast after wifi disconnected. The proxy might be default proxy or an
+//      // empty proxy built by buildDirectProxy. See {@link ProxyTracker.sendProxyBroadcast}.
+//      // Thus here expects a broadcast will be received but does not verify the content of the
+//      // proxy.
+//      final ExpectedBroadcast b4 = expectProxyChangeAction();
+//      mWiFiAgent.disconnect();
+//      b4.expectBroadcast();
+//      wifiCallback.expect(CallbackEntry.LOST, mWiFiAgent);
+//      assertNull(mService.getProxyForNetwork(null));
+        assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+        assertNull(mService.getGlobalProxy());
+    }
+
     @Test
     public void testGetProxyForVPN() throws Exception {
         final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
@@ -11352,7 +11614,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 +12603,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 +16496,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 +16514,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 +16545,7 @@
                 mCellAgent.getNetwork().netId,
                 allowAllUidRangesParcel,
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellAllAllowedConfig});
         } else {
@@ -16302,7 +16564,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 +16598,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 +16610,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 +16622,7 @@
         // disconnects.
         enterpriseAgent.disconnect();
         waitForIdle();
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellAllAllowedConfig});
         } else {
@@ -16385,7 +16647,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 +16675,7 @@
                 mCellAgent.getNetwork().netId,
                 excludeAppRangesParcel,
                 0 /* subPriority */);
-        if (SdkLevel.isAtLeastU()) {
+        if (mDeps.isAtLeastU()) {
             inOrder.verify(mMockNetd).setNetworkAllowlist(
                     new NativeUidRangeConfig[]{cellExcludeAppConfig});
         } else {
@@ -16437,7 +16699,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 +16710,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 +16719,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 +16775,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 +16883,7 @@
                 agent.getNetwork().getNetId(),
                 intToUidRangeStableParcels(uids),
                 preferenceOrder);
-        if (SdkLevel.isAtLeastT()) {
+        if (mDeps.isAtLeastT()) {
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids200Parcel);
         }
 
@@ -16629,7 +16891,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 +16902,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 +16919,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 +16928,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 +16936,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 +16989,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 +17042,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 +17052,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 +17974,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 +18235,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 52f2712..a54a521 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -20,6 +20,7 @@
 
 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;
@@ -97,8 +98,10 @@
         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();
+        doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
+        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
+                sharedLog) {
                     @Override
                     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
                             @Nullable Network network) {