Merge "Update the QueryTaskConfig constructor" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 83619d6..39009cb 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,15 @@
+[Builtin Hooks]
+bpfmt = true
+clang_format = true
+ktfmt = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
+ktfmt = --kotlinlang-style
+
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
 
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index ae0161d..0f5a014 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -198,6 +198,10 @@
 
     /**
      * VIRTUAL tethering type.
+     *
+     * This tethering type is for providing external network to virtual machines
+     * running on top of Android devices, which are created and managed by
+     * AVF(Android Virtualization Framework).
      * @hide
      */
     @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5e41dd9..a6a967b 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -4489,6 +4489,7 @@
     public static final int CALLBACK_BLK_CHANGED                = 11;
     /** @hide */
     public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+    // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
 
 
     /** @hide */
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 64624ae..419ec3a 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -93,6 +93,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
@@ -768,7 +769,7 @@
             private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
                 final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
                 for (String subtype : subtypes) {
-                    subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+                    subtypeMap.put(DnsUtils.toDnsUpperCase(subtype), subtype);
                 }
                 return new ArraySet<>(subtypeMap.values());
             }
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 54943c7..f55db93 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -24,6 +24,7 @@
 import android.util.Pair;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -193,7 +194,7 @@
                         // Ignore any PTR records that don't match the current query.
                         if (!CollectionUtils.any(questions,
                                 q -> q instanceof MdnsPointerRecord
-                                        && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+                                        && DnsUtils.equalsDnsLabelIgnoreDnsCase(
                                                 q.getName(), ptrRecord.getName()))) {
                             continue;
                         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index b870477..9c52eca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -41,6 +41,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -272,7 +273,7 @@
             return true;
         }
         // Check if it conflicts with the default hostname.
-        return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
+        return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
     }
 
     private void updateRegistrationUntilNoConflict(
@@ -436,8 +437,8 @@
                     continue;
                 }
                 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
-                if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
-                        && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+                if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+                        && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
                         other.getServiceType())) {
                     return mPendingRegistrations.keyAt(i);
                 }
@@ -463,13 +464,13 @@
                 final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
                 final int otherServiceId = mPendingRegistrations.keyAt(i);
                 if (clientUid != otherRegistration.mClientUid
-                        && MdnsUtils.equalsIgnoreDnsCase(
+                        && DnsUtils.equalsIgnoreDnsCase(
                                 info.getHostname(), otherInfo.getHostname())) {
                     return otherServiceId;
                 }
                 if (!info.getHostAddresses().isEmpty()
                         && !otherInfo.getHostAddresses().isEmpty()
-                        && MdnsUtils.equalsIgnoreDnsCase(
+                        && DnsUtils.equalsIgnoreDnsCase(
                                 info.getHostname(), otherInfo.getHostname())) {
                     return otherServiceId;
                 }
@@ -849,7 +850,7 @@
                 sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
                 continue;
             }
-            priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+            priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority);
         }
         return priorities;
     }
@@ -995,7 +996,7 @@
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
         final Integer mapPriority = mServiceTypeToOffloadPriority.get(
-                MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+                DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType()));
         // Higher values of priority are less prioritized
         final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
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 0ab7a76..8123d27 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -30,6 +30,7 @@
 import androidx.annotation.GuardedBy;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -66,8 +67,8 @@
 
         public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
                 @NonNull MdnsServiceTypeClient client) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             clients.put(perSocketServiceType, client);
         }
@@ -75,18 +76,18 @@
         @Nullable
         public MdnsServiceTypeClient get(
                 @NonNull String serviceType, @NonNull SocketKey socketKey) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             return clients.getOrDefault(perSocketServiceType, null);
         }
 
         public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
                 final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
-                if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
+                if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
                     list.add(clients.valueAt(i));
                 }
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 1239180..a5b8803 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,7 +20,7 @@
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -153,7 +153,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(super.hashCode(),
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(mNextDomain)),
                 Arrays.hashCode(mTypes));
     }
 
@@ -167,7 +167,7 @@
         }
 
         return super.equals(other)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
                 ((MdnsNsecRecord) other).mNextDomain)
                 && Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 6879a64..cf788be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,8 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -180,7 +180,7 @@
             int existingOffset = entry.getKey();
             String[] existingLabels = entry.getValue();
 
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
                 writePointer(existingOffset);
                 return;
             } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index e5c90a4..39bf653 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -68,7 +68,7 @@
     }
 
     public boolean hasSubtype() {
-        return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+        return (name != null) && (name.length > 2) && DnsUtils.equalsIgnoreDnsCase(name[1],
                 MdnsConstants.SUBTYPE_LABEL);
     }
 
@@ -83,7 +83,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
+        return (super.hashCode() * 31) + Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(pointer));
     }
 
     @Override
@@ -95,7 +95,7 @@
             return false;
         }
 
-        return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+        return super.equals(other) && DnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
                 ((MdnsPointerRecord) other).pointer);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index e88947a..4a44fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -23,8 +23,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -114,7 +114,7 @@
         private static boolean containsName(@NonNull List<MdnsRecord> records,
                 @NonNull String[] name) {
             return CollectionUtils.any(records,
-                    r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
+                    r -> DnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
         }
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index b865319..d464ca7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -25,7 +25,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -139,7 +139,7 @@
         }
 
         for (int i = 0; i < list1.length; ++i) {
-            if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
+            if (!DnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
                 return false;
             }
         }
@@ -284,13 +284,13 @@
 
         MdnsRecord otherRecord = (MdnsRecord) other;
 
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+        return DnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
                 == otherRecord.type);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
+        return Objects.hash(Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(name)), type);
     }
 
     /**
@@ -311,7 +311,7 @@
 
         public Key(int recordType, String[] recordName) {
             this.recordType = recordType;
-            this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
+            this.recordName = DnsUtils.toDnsLabelsUpperCase(recordName);
         }
 
         @Override
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 36f3982..0e84764 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -38,6 +38,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HexDump;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -494,8 +495,8 @@
         }
         for (int i = 0; i < mServices.size(); i++) {
             final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
-            if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
-                    && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
+            if (DnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+                    && DnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
                 return mServices.keyAt(i);
             }
         }
@@ -821,7 +822,7 @@
              must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
              "CNAME" (5), and the record rrclass must match the question qclass unless the
              qclass is "ANY" (255) */
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
                 continue;
             }
             hasFullyOwnedNameMatch |= !info.isSharedName;
@@ -1232,7 +1233,7 @@
             return RecordConflictType.NO_CONFLICT;
         }
 
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
             return RecordConflictType.NO_CONFLICT;
         }
 
@@ -1270,7 +1271,7 @@
         }
 
         // Different names. There won't be a conflict.
-        if (!MdnsUtils.equalsIgnoreDnsCase(
+        if (!DnsUtils.equalsIgnoreDnsCase(
                 record.getName()[0], registration.serviceInfo.getHostname())) {
             return RecordConflictType.NO_CONFLICT;
         }
@@ -1351,7 +1352,7 @@
             int id = mServices.keyAt(i);
             ServiceRegistration service = mServices.valueAt(i);
             if (service.exiting) continue;
-            if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+            if (DnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
                 consumer.accept(id, service);
             }
         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 05ad1be..3636644 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -21,7 +21,7 @@
 import android.net.Network;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -111,7 +111,7 @@
      * pointer record is already present in the response with the same TTL.
      */
     public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
             throw new IllegalArgumentException(
                     "Pointer records for different service names cannot be added");
         }
@@ -305,13 +305,13 @@
         boolean dropAddressRecords = false;
 
         for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
         }
         for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index b812bb4..52e76ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -22,6 +22,7 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.EOFException;
@@ -49,7 +50,7 @@
             List<MdnsResponse> responses, String[] pointer) {
         if (responses != null) {
             for (MdnsResponse response : responses) {
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
                     return response;
                 }
             }
@@ -65,7 +66,7 @@
                 if (serviceRecord == null) {
                     continue;
                 }
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
                         hostName)) {
                     return response;
                 }
@@ -318,7 +319,7 @@
             if (serviceRecord == null) {
                 continue;
             }
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
                 if (result == null) {
                     result = new ArrayList<>(/* initialCapacity= */ responses.size());
                 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index e9a41d1..7eea93a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,10 +16,10 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.toDnsUpperCase;
 import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
 
 import static java.lang.Math.min;
 
@@ -49,16 +49,16 @@
  */
 public class MdnsServiceCache {
     static class CacheKey {
-        @NonNull final String mLowercaseServiceType;
+        @NonNull final String mUpperCaseServiceType;
         @NonNull final SocketKey mSocketKey;
 
         CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
-            mLowercaseServiceType = toDnsLowerCase(serviceType);
+            mUpperCaseServiceType = toDnsUpperCase(serviceType);
             mSocketKey = socketKey;
         }
 
         @Override public int hashCode() {
-            return Objects.hash(mLowercaseServiceType, mSocketKey);
+            return Objects.hash(mUpperCaseServiceType, mSocketKey);
         }
 
         @Override public boolean equals(Object other) {
@@ -68,7 +68,7 @@
             if (!(other instanceof CacheKey)) {
                 return false;
             }
-            return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
+            return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
                     && Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 0d6a9ec..fd716d2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -151,7 +151,7 @@
     public int hashCode() {
         return (super.hashCode() * 31)
                 + Objects.hash(servicePriority, serviceWeight,
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(serviceHost)),
                 servicePort);
     }
 
@@ -168,7 +168,7 @@
         return super.equals(other)
                 && (servicePriority == otherRecord.servicePriority)
                 && (serviceWeight == otherRecord.serviceWeight)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
                 && (servicePort == otherRecord.servicePort);
     }
 }
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 5f9a9e7..8959c1b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -33,6 +33,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -120,11 +121,11 @@
          * @return true if the service name was not discovered before.
          */
         boolean setServiceDiscovered(@NonNull String serviceName) {
-            return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+            return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName));
         }
 
         void unsetServiceDiscovered(@NonNull String serviceName) {
-            discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+            discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName));
         }
     }
 
@@ -461,7 +462,7 @@
             @NonNull MdnsSearchOptions options) {
         final boolean matchesInstanceName = options.getResolveInstanceName() == null
                 // DNS is case-insensitive, so ignore case in the comparison
-                || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+                || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
                 response.getServiceInstanceName());
 
         // If discovery is requiring some subtypes, the response must have one that matches a
@@ -471,7 +472,7 @@
         final boolean matchesSubtype = options.getSubtypes().size() == 0
                 || CollectionUtils.any(options.getSubtypes(), requiredSub ->
                 CollectionUtils.any(responseSubtypes, actualSub ->
-                        MdnsUtils.equalsIgnoreDnsCase(
+                        DnsUtils.equalsIgnoreDnsCase(
                                 MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
 
         return matchesInstanceName && matchesSubtype;
@@ -662,7 +663,7 @@
                 continue;
             }
             if (CollectionUtils.any(resolveResponses,
-                    r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+                    r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
                 continue;
             }
             MdnsResponse knownResponse =
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 226867f..8745941 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns.util;
 
+import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
 import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
 
 import android.annotation.NonNull;
@@ -56,18 +58,17 @@
     private MdnsUtils() { }
 
     /**
-     * Convert the string to DNS case-insensitive lowercase
+     * Compare labels a equals b or a is suffix of b.
      *
-     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
-     * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they
-     * transform into a-z. Otherwise, they are kept as-is.
+     * @param a the type or subtype.
+     * @param b the base type
      */
-    public static String toDnsLowerCase(@NonNull String string) {
-        final char[] outChars = new char[string.length()];
-        for (int i = 0; i < string.length(); i++) {
-            outChars[i] = toDnsLowerCase(string.charAt(i));
-        }
-        return new String(outChars);
+    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+            @NonNull String[] b) {
+        return equalsDnsLabelIgnoreDnsCase(a, b)
+                || ((b.length == (a.length + 2))
+                && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+                && MdnsRecord.labelsAreSuffix(a, b));
     }
 
     /**
@@ -81,70 +82,6 @@
         }
     }
 
-    /**
-     * Convert the array of labels to DNS case-insensitive lowercase.
-     */
-    public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
-        final String[] outStrings = new String[labels.length];
-        for (int i = 0; i < labels.length; ++i) {
-            outStrings[i] = toDnsLowerCase(labels[i]);
-        }
-        return outStrings;
-    }
-
-    /**
-     * Compare two strings by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
-        if (a == null || b == null) {
-            return a == null && b == null;
-        }
-        if (a.length() != b.length()) return false;
-        for (int i = 0; i < a.length(); i++) {
-            if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare two set of DNS labels by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
-        if (a == b) {
-            return true;
-        }
-        int length = a.length;
-        if (b.length != length) {
-            return false;
-        }
-        for (int i = 0; i < length; i++) {
-            if (!equalsIgnoreDnsCase(a[i], b[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare labels a equals b or a is suffix of b.
-     *
-     * @param a the type or subtype.
-     * @param b the base type
-     */
-    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
-            @NonNull String[] b) {
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
-                || ((b.length == (a.length + 2))
-                && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
-                && MdnsRecord.labelsAreSuffix(a, b));
-    }
-
-    private static char toDnsLowerCase(char a) {
-        return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
-    }
-
     /*** Ensure that current running thread is same as given handler thread */
     public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
         if (!isRunningOnHandlerThread(handler)) {
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
new file mode 100644
index 0000000..060a984
--- /dev/null
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+
+import com.android.net.module.util.GrowingIntArray;
+
+/**
+ * A utility class to add/remove {@link NetworkCallback}s from a queue.
+ *
+ * <p>This is intended to be used as a temporary builder to create/modify callbacks stored in an int
+ * array for memory efficiency.
+ *
+ * <p>Intended usage:
+ * <pre>
+ *     final CallbackQueue queue = new CallbackQueue(storedCallbacks);
+ *     queue.forEach(netId, callbackId -> { [...] });
+ *     queue.addCallback(netId, callbackId);
+ *     [...]
+ *     queue.shrinkToLength();
+ *     storedCallbacks = queue.getBackingArray();
+ * </pre>
+ *
+ * <p>This class is not thread-safe.
+ */
+public class CallbackQueue extends GrowingIntArray {
+    public CallbackQueue(int[] initialCallbacks) {
+        super(initialCallbacks);
+    }
+
+    /**
+     * Get a callback int from netId and callbackId.
+     *
+     * <p>The first 16 bits of each int is the netId; the last 16 bits are the callback index.
+     */
+    private static int getCallbackInt(int netId, int callbackId) {
+        return (netId << 16) | (callbackId & 0xffff);
+    }
+
+    private static int getNetId(int callbackInt) {
+        return callbackInt >>> 16;
+    }
+
+    private static int getCallbackId(int callbackInt) {
+        return callbackInt & 0xffff;
+    }
+
+    /**
+     * A consumer interface for {@link #forEach(CallbackConsumer)}.
+     *
+     * <p>This is similar to a BiConsumer&lt;Integer, Integer&gt;, but avoids the boxing cost.
+     */
+    public interface CallbackConsumer {
+        /**
+         * Method called on each callback in the queue.
+         */
+        void accept(int netId, int callbackId);
+    }
+
+    /**
+     * Iterate over all callbacks in the queue.
+     */
+    public void forEach(@NonNull CallbackConsumer consumer) {
+        forEach(value -> {
+            final int netId = getNetId(value);
+            final int callbackId = getCallbackId(value);
+            consumer.accept(netId, callbackId);
+        });
+    }
+
+    /**
+     * Indicates whether the queue contains a callback for the given (netId, callbackId).
+     */
+    public boolean hasCallback(int netId, int callbackId) {
+        return contains(getCallbackInt(netId, callbackId));
+    }
+
+    /**
+     * Remove all callbacks for the given netId.
+     *
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacksForNetId(int netId) {
+        return removeValues(cb -> getNetId(cb) == netId);
+    }
+
+    /**
+     * Remove all callbacks for the given netId and callbackId.
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacks(int netId, int callbackId) {
+        final int cbInt = getCallbackInt(netId, callbackId);
+        return removeValues(cb -> cb == cbInt);
+    }
+
+    /**
+     * Add a callback at the end of the queue.
+     */
+    public void addCallback(int netId, int callbackId) {
+        add(getCallbackInt(netId, callbackId));
+    }
+
+    @Override
+    protected String valueToString(int item) {
+        final int callbackId = getCallbackId(item);
+        final int netId = getNetId(item);
+        return ConnectivityManager.getCallbackName(callbackId) + "(" + netId + ")";
+    }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f015742..953fd76 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,8 +38,8 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CALLBACK_AVAILABLE;
 import static android.net.ConnectivityManager.CALLBACK_BLK_CHANGED;
 import static android.net.ConnectivityManager.CALLBACK_CAP_CHANGED;
@@ -56,6 +56,7 @@
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -76,7 +77,6 @@
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
-import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
@@ -145,8 +145,9 @@
 import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
 import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
 import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
-import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 
 import android.Manifest;
 import android.annotation.CheckResult;
@@ -329,9 +330,9 @@
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
 import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 import com.android.net.module.util.LocationPermissionChecker;
-import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.PerUidCounter;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.TcUtils;
 import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
@@ -510,6 +511,9 @@
 
     private final boolean mUseDeclaredMethodsForCallbacksEnabled;
 
+    // Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks.
+    private final boolean mQueueCallbacksForFrozenApps;
+
     /**
      * Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
      * Key is uid based on mAsUid of registered networkRequestInfo
@@ -1873,6 +1877,9 @@
                 context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
         mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
                 ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+        // registerUidFrozenStateChangedCallback is only available on U+
+        mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
                 this::handleUidCarrierPrivilegesLost, mHandler);
@@ -2028,7 +2035,7 @@
         mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS);
         mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
                 mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
-        if (mDestroyFrozenSockets) {
+        if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
                 @Override
@@ -3464,6 +3471,14 @@
 
     private void handleFrozenUids(int[] uids, int[] frozenStates) {
         ensureRunningOnConnectivityServiceThread();
+        handleDestroyFrozenSockets(uids, frozenStates);
+        // TODO: handle freezing NetworkCallbacks
+    }
+
+    private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
+        if (!mDestroyFrozenSockets) {
+            return;
+        }
         for (int i = 0; i < uids.length; i++) {
             final int uid = uids[i];
             final boolean addReason = frozenStates[i] == UID_FROZEN_STATE_FROZEN;
@@ -7764,6 +7779,11 @@
             }
         }
 
+        boolean isCallbackOverridden(int callbackId) {
+            return !mUseDeclaredMethodsForCallbacksEnabled
+                    || (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
+        }
+
         boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
             // Compare two preference orders.
             return mPreferenceOrder < target.mPreferenceOrder;
@@ -10233,6 +10253,18 @@
         return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
     }
 
+    private Bundle makeCommonBundleForCallback(@NonNull final NetworkRequestInfo nri,
+            @Nullable Network network) {
+        final Bundle bundle = new Bundle();
+        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+        // TODO: check if defensive copies of data is needed.
+        putParcelable(bundle, nri.getNetworkRequestForCallback());
+        if (network != null) {
+            putParcelable(bundle, network);
+        }
+        return bundle;
+    }
+
     // networkAgent is only allowed to be null if notificationType is
     // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
     // available, while all other cases are about some particular network.
@@ -10245,22 +10277,17 @@
             // are Type.LISTEN, but should not have NetworkCallbacks invoked.
             return;
         }
-        if (mUseDeclaredMethodsForCallbacksEnabled
-                && (nri.mDeclaredMethodsFlags & (1 << notificationType)) == 0) {
+        if (!nri.isCallbackOverridden(notificationType)) {
             // No need to send the notification as the recipient method is not overridden
             return;
         }
-        final Bundle bundle = new Bundle();
-        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
-        // TODO: check if defensive copies of data is needed.
-        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
-        putParcelable(bundle, nrForCallback);
-        Message msg = Message.obtain();
-        if (notificationType != CALLBACK_UNAVAIL) {
-            putParcelable(bundle, networkAgent.network);
-        }
+        final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
+                ? null
+                : networkAgent.network;
+        final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
         final boolean includeLocationSensitiveInfo =
                 (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
         switch (notificationType) {
             case CALLBACK_AVAILABLE: {
                 final NetworkCapabilities nc =
@@ -10277,12 +10304,6 @@
                 // method here.
                 bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
                         localNetworkInfoForNai(networkAgent));
-                // For this notification, arg1 contains the blocked status.
-                msg.arg1 = arg1;
-                break;
-            }
-            case CALLBACK_LOSING: {
-                msg.arg1 = arg1;
                 break;
             }
             case CALLBACK_CAP_CHANGED: {
@@ -10305,7 +10326,6 @@
             }
             case CALLBACK_BLK_CHANGED: {
                 maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
-                msg.arg1 = arg1;
                 break;
             }
             case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
@@ -10317,17 +10337,26 @@
                 break;
             }
         }
+        callCallbackForRequest(nri, notificationType, bundle, arg1);
+    }
+
+    private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, int notificationType,
+            Bundle bundle, int arg1) {
+        Message msg = Message.obtain();
+        msg.arg1 = arg1;
         msg.what = notificationType;
         msg.setData(bundle);
         try {
             if (VDBG) {
                 String notification = ConnectivityManager.getCallbackName(notificationType);
-                log("sending notification " + notification + " for " + nrForCallback);
+                log("sending notification " + notification + " for "
+                        + nri.getNetworkRequestForCallback());
             }
             nri.mMessenger.send(msg);
         } catch (RemoteException e) {
             // may occur naturally in the race of binder death.
-            loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+            loge("RemoteException caught trying to send a callback msg for "
+                    + nri.getNetworkRequestForCallback());
         }
     }
 
@@ -11416,11 +11445,7 @@
             return;
         }
 
-        final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
-        final boolean metered = nai.networkCapabilities.isMetered();
-        final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
-        callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE,
-                getBlockedState(nri.mAsUid, blockedReasons, metered, vpnBlocked));
+        callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE, getBlockedState(nai, nri.mAsUid));
     }
 
     // Notify the requests on this NAI that the network is now lingered.
@@ -11450,6 +11475,13 @@
                 : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
     }
 
+    private int getBlockedState(@NonNull NetworkAgentInfo nai, int uid) {
+        final boolean metered = nai.networkCapabilities.isMetered();
+        final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+        final int blockedReasons = mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE);
+        return getBlockedState(uid, blockedReasons, metered, vpnBlocked);
+    }
+
     private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
         if (blockedReasons == BLOCKED_REASON_NONE) {
             mUidBlockedReasons.delete(uid);
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 1ee1ed7..df87316 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -49,6 +49,9 @@
     public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
             "use_declared_methods_for_callbacks";
 
+    public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS =
+            "queue_callbacks_for_frozen_apps";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 14bd5df..c9c8be9 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -443,6 +443,7 @@
         "device/com/android/net/module/util/SharedLog.java",
         "framework/com/android/net/module/util/ByteUtils.java",
         "framework/com/android/net/module/util/CollectionUtils.java",
+        "framework/com/android/net/module/util/DnsUtils.java",
         "framework/com/android/net/module/util/HexDump.java",
         "framework/com/android/net/module/util/LinkPropertiesUtils.java",
     ],
diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
new file mode 100644
index 0000000..4a81c10
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.StringJoiner;
+import java.util.function.IntConsumer;
+import java.util.function.IntPredicate;
+
+/**
+ * A growing array of primitive ints.
+ *
+ * <p>This is similar to ArrayList&lt;Integer&gt;, but avoids the cost of boxing (each Integer costs
+ * 16 bytes) and creation / garbage collection of individual Integer objects.
+ *
+ * <p>This class does not use any heuristic for growing capacity, so every call to
+ * {@link #add(int)} may reallocate the backing array. Callers should use
+ * {@link #ensureHasCapacity(int)} to minimize this behavior when they plan to add several values.
+ */
+public class GrowingIntArray {
+    private int[] mValues;
+    private int mLength;
+
+    /**
+     * Create an empty GrowingIntArray with the given capacity.
+     */
+    public GrowingIntArray(int initialCapacity) {
+        mValues = new int[initialCapacity];
+        mLength = 0;
+    }
+
+    /**
+     * Create a GrowingIntArray with an initial array of values.
+     *
+     * <p>The array will be used as-is and may be modified, so callers must stop using it after
+     * calling this constructor.
+     */
+    protected GrowingIntArray(int[] initialValues) {
+        mValues = initialValues;
+        mLength = initialValues.length;
+    }
+
+    /**
+     * Add a value to the array.
+     */
+    public void add(int value) {
+        ensureHasCapacity(1);
+        mValues[mLength] = value;
+        mLength++;
+    }
+
+    /**
+     * Get the current number of values in the array.
+     */
+    public int length() {
+        return mLength;
+    }
+
+    /**
+     * Get the value at a given index.
+     *
+     * @throws ArrayIndexOutOfBoundsException if the index is out of bounds.
+     */
+    public int get(int index) {
+        if (index < 0 || index >= mLength) {
+            throw new ArrayIndexOutOfBoundsException(index);
+        }
+        return mValues[index];
+    }
+
+    /**
+     * Iterate over all values in the array.
+     */
+    public void forEach(@NonNull IntConsumer consumer) {
+        for (int i = 0; i < mLength; i++) {
+            consumer.accept(mValues[i]);
+        }
+    }
+
+    /**
+     * Remove all values matching a predicate.
+     *
+     * @return true if at least one value was removed.
+     */
+    public boolean removeValues(@NonNull IntPredicate predicate) {
+        int newQueueLength = 0;
+        for (int i = 0; i < mLength; i++) {
+            final int cb = mValues[i];
+            if (!predicate.test(cb)) {
+                mValues[newQueueLength] = cb;
+                newQueueLength++;
+            }
+        }
+        if (mLength != newQueueLength) {
+            mLength = newQueueLength;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether the array contains the given value.
+     */
+    public boolean contains(int value) {
+        for (int i = 0; i < mLength; i++) {
+            if (mValues[i] == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove all values from the array.
+     */
+    public void clear() {
+        mLength = 0;
+    }
+
+    /**
+     * Ensure at least the given number of values can be added to the array without reallocating.
+     *
+     * @param capacity The minimum number of additional values the array must be able to hold.
+     */
+    public void ensureHasCapacity(int capacity) {
+        if (mValues.length >= mLength + capacity) {
+            return;
+        }
+        mValues = Arrays.copyOf(mValues, mLength + capacity);
+    }
+
+    @VisibleForTesting
+    int getBackingArrayLength() {
+        return mValues.length;
+    }
+
+    /**
+     * Shrink the array backing this class to the minimum required length.
+     */
+    public void shrinkToLength() {
+        if (mValues.length != mLength) {
+            mValues = Arrays.copyOf(mValues, mLength);
+        }
+    }
+
+    /**
+     * Get values as array by shrinking the internal array to length and returning it.
+     *
+     * <p>This avoids reallocations if the array is already the correct length, but callers should
+     * stop using this instance of {@link GrowingIntArray} if they use the array returned by this
+     * method.
+     */
+    public int[] getShrinkedBackingArray() {
+        shrinkToLength();
+        return mValues;
+    }
+
+    /**
+     * Get the String representation of an item in the array, for use by {@link #toString()}.
+     */
+    protected String valueToString(int item) {
+        return String.valueOf(item);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        final StringJoiner joiner = new StringJoiner(",", "[", "]");
+        forEach(item -> joiner.add(valueToString(item)));
+        return joiner.toString();
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsUtils.java b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
new file mode 100644
index 0000000..19ffd72
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Dns common utility functions.
+ *
+ * @hide
+ */
+public class DnsUtils {
+
+    private DnsUtils() { }
+
+    /**
+     * Convert the string to DNS case-insensitive uppercase.
+     *
+     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
+     * their unaccented counterparts. So the "DNS uppercase" should be if character is a-z then they
+     * transform into A-Z. Otherwise, they are kept as-is.
+     */
+    public static String toDnsUpperCase(@NonNull String string) {
+        final char[] outChars = new char[string.length()];
+        for (int i = 0; i < string.length(); i++) {
+            outChars[i] = toDnsUpperCase(string.charAt(i));
+        }
+        return new String(outChars);
+    }
+
+    /**
+     * Convert the array of labels to DNS case-insensitive uppercase.
+     */
+    public static String[] toDnsLabelsUpperCase(@NonNull String[] labels) {
+        final String[] outStrings = new String[labels.length];
+        for (int i = 0; i < labels.length; ++i) {
+            outStrings[i] = toDnsUpperCase(labels[i]);
+        }
+        return outStrings;
+    }
+
+    /**
+     * Compare two strings by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+        if (a == null || b == null) {
+            return a == null && b == null;
+        }
+        if (a.length() != b.length()) return false;
+        for (int i = 0; i < a.length(); i++) {
+            if (toDnsUpperCase(a.charAt(i)) != toDnsUpperCase(b.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compare two set of DNS labels by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+        if (a == b) {
+            return true;
+        }
+        int length = a.length;
+        if (b.length != length) {
+            return false;
+        }
+        for (int i = 0; i < length; i++) {
+            if (!equalsIgnoreDnsCase(a[i], b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static char toDnsUpperCase(char a) {
+        return a >= 'a' && a <= 'z' ? (char) (a - ('a' - 'A')) : a;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index a8c50d8..f1ff2e4 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -260,6 +260,18 @@
     public static final short DNS_OVER_TLS_PORT = 853;
 
     /**
+     * Dns query type constants.
+     *
+     * See also:
+     *    - https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
+     */
+    public static final int TYPE_A = 1;
+    public static final int TYPE_PTR = 12;
+    public static final int TYPE_TXT = 16;
+    public static final int TYPE_AAAA = 28;
+    public static final int TYPE_SRV = 33;
+
+    /**
      * IEEE802.11 standard constants.
      *
      * See also:
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
new file mode 100644
index 0000000..7b1f08a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.toDnsLabelsUpperCase
+import com.android.net.module.util.DnsUtils.toDnsUpperCase
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+class DnsUtilsTest {
+    @Test
+    fun testToDnsUpperCase() {
+        assertEquals("TEST", toDnsUpperCase("TEST"))
+        assertEquals("TEST", toDnsUpperCase("TeSt"))
+        assertEquals("TEST", toDnsUpperCase("test"))
+        assertEquals("TÉST", toDnsUpperCase("TÉST"))
+        assertEquals("ลฃéST", toDnsUpperCase("ลฃést"))
+        // Unicode characters 0x10000 (๐€€), 0x10001 (๐€), 0x10041 (๐)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertEquals(
+            "TEST: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                toDnsUpperCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+        )
+        // Also test some characters where the first surrogate is not \ud800
+        assertEquals(
+            "TEST: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                toDnsUpperCase(
+                    "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+                )
+        )
+    }
+
+    @Test
+    fun testToDnsLabelsUpperCase() {
+        assertArrayEquals(
+            arrayOf("TEST", "TÉST", "ลฃéST"),
+            toDnsLabelsUpperCase(arrayOf("TeSt", "TÉST", "ลฃést"))
+        )
+    }
+
+    @Test
+    fun testEqualsIgnoreDnsCase() {
+        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
+        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
+        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
+        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
+        assertFalse(equalsIgnoreDnsCase("ลขÉST", "ลฃést"))
+        // Unicode characters 0x10000 (๐€€), 0x10001 (๐€), 0x10041 (๐)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertTrue(equalsIgnoreDnsCase(
+                "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+        ))
+        // Also test some characters where the first surrogate is not \ud800
+        assertTrue(equalsIgnoreDnsCase(
+                "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+        ))
+    }
+
+    @Test
+    fun testEqualsLabelIgnoreDnsCase() {
+        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
new file mode 100644
index 0000000..bdcb8c0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GrowingIntArrayTest {
+    @Test
+    fun testAddAndGet() {
+        val array = GrowingIntArray(1)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        assertEquals(-1, array.get(0))
+        assertEquals(0, array.get(1))
+        assertEquals(2, array.get(2))
+        assertEquals(3, array.length())
+    }
+
+    @Test
+    fun testForEach() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        val actual = mutableListOf<Int>()
+        array.forEach { actual.add(it) }
+
+        val expected = listOf(-1, 0, 2)
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun testForEach_EmptyArray() {
+        val array = GrowingIntArray(10)
+        array.forEach {
+            fail("This should not be called")
+        }
+    }
+
+    @Test
+    fun testRemoveValues() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        array.removeValues { it <= 0 }
+        assertEquals(1, array.length())
+        assertEquals(2, array.get(0))
+    }
+
+    @Test
+    fun testContains() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertTrue(array.contains(-1))
+        assertTrue(array.contains(2))
+
+        assertFalse(array.contains(0))
+        assertFalse(array.contains(3))
+    }
+
+    @Test
+    fun testClear() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+        array.clear()
+
+        assertEquals(0, array.length())
+    }
+
+    @Test
+    fun testEnsureHasCapacity() {
+        val array = GrowingIntArray(0)
+        array.add(42)
+        array.ensureHasCapacity(2)
+
+        assertEquals(3, array.backingArrayLength)
+    }
+
+    @Test
+    fun testGetShrinkedBackingArray() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertContentEquals(intArrayOf(-1, 2), array.shrinkedBackingArray)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", GrowingIntArray(10).toString())
+        assertEquals("[1,2,3]", GrowingIntArray(3).apply {
+            add(1)
+            add(2)
+            add(3)
+        }.toString())
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
new file mode 100644
index 0000000..7e508fb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DefaultNetworkRestoreMonitorTest {
+    private val restoreDefaultNetworkDesc =
+            Description.createSuiteDescription("RestoreDefaultNetwork")
+    private val testDesc = Description.createTestDescription("testClass", "testMethod")
+    private val wifiCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_WIFI)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cellCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cm = mock(ConnectivityManager::class.java)
+    private val pm = mock(PackageManager::class.java).also {
+        doReturn(true).`when`(it).hasSystemFeature(anyString())
+    }
+    private val ctx = mock(Context::class.java).also {
+        doReturn(cm).`when`(it).getSystemService(ConnectivityManager::class.java)
+        doReturn(pm).`when`(it).getPackageManager()
+    }
+    private val notifier = mock(RunNotifier::class.java)
+    private val defaultNetworkMonitor = DefaultNetworkRestoreMonitor(
+        ctx,
+        notifier,
+        timeoutMs = 0
+    )
+
+    private fun getRunListener(): RunListener {
+        val captor = ArgumentCaptor.forClass(RunListener::class.java)
+        verify(notifier).addListener(captor.capture())
+        return captor.value
+    }
+
+    private fun mockDefaultNetworkCapabilities(cap: NetworkCapabilities?) {
+        if (cap == null) {
+            doNothing().`when`(cm).registerDefaultNetworkCallback(any())
+            return
+        }
+        doAnswer {
+            val callback = it.getArgument(0) as NetworkCallback
+            callback.onCapabilitiesChanged(Network(100), cap)
+        }.`when`(cm).registerDefaultNetworkCallback(any())
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_defaultNetworkRestored() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier, never()).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testStartWithoutDefaultNetwork() {
+        // There is no default network when the tests start
+        mockDefaultNetworkCapabilities(null)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        mockDefaultNetworkCapabilities(wifiCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called
+        inOrder.verify(notifier).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testEndWithoutDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // There is no default network after the test
+        mockDefaultNetworkCapabilities(null)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testChangeDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // The default network transport types change after the test
+        mockDefaultNetworkCapabilities(cellCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
new file mode 100644
index 0000000..1b709b2
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.internal.annotations.VisibleForTesting
+import com.android.net.module.util.BitUtils
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.runner.Description
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+class DefaultNetworkRestoreMonitor(
+        ctx: Context,
+        private val notifier: RunNotifier,
+        private val timeoutMs: Long = 3000
+) {
+    var firstFailure: Exception? = null
+    var initialTransports = 0L
+    val cm = ctx.getSystemService(ConnectivityManager::class.java)!!
+    val pm = ctx.packageManager
+    val listener = object : RunListener() {
+        override fun testFinished(desc: Description) {
+            // Only the first method that does not restore the default network should be blamed.
+            if (firstFailure != null) {
+                return
+            }
+            val cb = TestableNetworkCallback()
+            cm.registerDefaultNetworkCallback(cb)
+            try {
+                cb.eventuallyExpect<RecorderCallback.CallbackEntry.CapabilitiesChanged>(
+                    timeoutMs = timeoutMs
+                ) {
+                    BitUtils.packBits(it.caps.transportTypes) == initialTransports &&
+                            it.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                }
+            } catch (e: AssertionError) {
+                firstFailure = IllegalStateException(desc.methodName +
+                        " does not restore the default network")
+            } finally {
+                cm.unregisterNetworkCallback(cb)
+            }
+        }
+    }
+
+    fun init(connectUtil: ConnectUtil) {
+        // Ensure Wi-Fi and cellular connection before running test to avoid starting test
+        // with unexpected default network.
+        // ConnectivityTestTargetPreparer does the same thing, but it's possible that previous tests
+        // don't enable DefaultNetworkRestoreMonitor and the default network is not restored.
+        // This can be removed if all tests enable DefaultNetworkRestoreMonitor
+        if (pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            connectUtil.ensureWifiValidated()
+        }
+        if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            connectUtil.ensureCellularValidated()
+        }
+
+        val capFuture = CompletableFuture<NetworkCapabilities>()
+        val cb = object : ConnectivityManager.NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                    network: Network,
+                    cap: NetworkCapabilities
+            ) {
+                capFuture.complete(cap)
+            }
+        }
+        cm.registerDefaultNetworkCallback(cb)
+        try {
+            val cap = capFuture.get(100, TimeUnit.MILLISECONDS)
+            initialTransports = BitUtils.packBits(cap.transportTypes)
+        } catch (e: Exception) {
+            firstFailure = IllegalStateException(
+                    "Failed to get default network status before starting tests", e
+            )
+        } finally {
+            cm.unregisterNetworkCallback(cb)
+        }
+        notifier.addListener(listener)
+    }
+
+    fun reportResultAndCleanUp(desc: Description) {
+        notifier.fireTestStarted(desc)
+        if (firstFailure != null) {
+            notifier.fireTestFailure(
+                    Failure(desc, firstFailure)
+            )
+        }
+        notifier.fireTestFinished(desc)
+        notifier.removeListener(listener)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 8687ac7..a014834 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -16,6 +16,8 @@
 
 package com.android.testutils
 
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -57,6 +59,10 @@
 class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
     private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
     private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+    private val restoreDefaultNetworkDesc =
+            Description.createTestDescription(klass, "RestoreDefaultNetwork")
+    private val restoreDefaultNetwork = klass.isAnnotationPresent(RestoreDefaultNetwork::class.java)
+    val ctx = ApplicationProvider.getApplicationContext<Context>()
 
     // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
     // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
@@ -71,6 +77,10 @@
     // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
     annotation class MonitorThreadLeak
 
+    // Annotation for test classes to indicate the test runner should verify the default network is
+    // restored after each test.
+    annotation class RestoreDefaultNetwork
+
     private val baseRunner: RunnerWrapper<*>? = klass.let {
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -125,6 +135,14 @@
             )
             return
         }
+
+        val networkRestoreMonitor = if (restoreDefaultNetwork) {
+            DefaultNetworkRestoreMonitor(ctx, notifier).apply{
+                init(ConnectUtil(ctx))
+            }
+        } else {
+            null
+        }
         val threadCountsBeforeTest = if (shouldThreadLeakFailTest) {
             // Dump threads as a baseline to monitor thread leaks.
             getAllThreadNameCounts()
@@ -137,6 +155,7 @@
         if (threadCountsBeforeTest != null) {
             checkThreadLeak(notifier, threadCountsBeforeTest)
         }
+        networkRestoreMonitor?.reportResultAndCleanUp(restoreDefaultNetworkDesc)
         // Clears up internal state of all inline mocks.
         // TODO: Call clearInlineMocks() at the end of each test.
         Mockito.framework().clearInlineMocks()
@@ -163,6 +182,9 @@
             if (shouldThreadLeakFailTest) {
                 it.addChild(leakMonitorDesc)
             }
+            if (restoreDefaultNetwork) {
+                it.addChild(restoreDefaultNetworkDesc)
+            }
         }
     }
 
@@ -173,7 +195,14 @@
         // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
         if (baseRunner == null) return 1
 
-        return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
+        var testCount = baseRunner.testCount()
+        if (shouldThreadLeakFailTest) {
+            testCount += 1
+        }
+        if (restoreDefaultNetwork) {
+            testCount += 1
+        }
+        return testCount
     }
 
     @Throws(NoTestsRemainException::class)
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
new file mode 100644
index 0000000..8b4ffa5
--- /dev/null
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -0,0 +1,50 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+def assume_wifi_p2p_test_preconditions(
+    server_device: android_device, client_device: android_device
+) -> None:
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Assert pre-conditions
+  asserts.skip_if(not server.hasWifiFeature(), "Server requires Wifi feature")
+  asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+  asserts.skip_if(
+      not server.isP2pSupported(), "Server requires Wi-fi P2P feature"
+  )
+  asserts.skip_if(
+      not client.isP2pSupported(), "Client requires Wi-fi P2P feature"
+  )
+
+
+def setup_wifi_p2p_server_and_client(
+    server_device: android_device, client_device: android_device
+) -> None:
+  """Set up the Wi-Fi P2P server and client."""
+  # Start Wi-Fi P2P on both server and client.
+  server_device.connectivity_multi_devices_snippet.startWifiP2p()
+  client_device.connectivity_multi_devices_snippet.startWifiP2p()
+
+
+def cleanup_wifi_p2p(
+    server_device: android_device, client_device: android_device
+) -> None:
+  # Stop Wi-Fi P2P
+  server_device.connectivity_multi_devices_snippet.stopWifiP2p()
+  client_device.connectivity_multi_devices_snippet.stopWifiP2p()
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index b0b32c2..c94087e 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -26,6 +26,7 @@
     srcs: [
         "ConnectivityMultiDevicesSnippet.kt",
         "MdnsMultiDevicesSnippet.kt",
+        "Wifip2pMultiDevicesSnippet.kt",
     ],
     manifest: "AndroidManifest.xml",
     static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 967e581..4637497 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -21,6 +21,8 @@
   <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
   <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
   <uses-permission android:name="android.permission.INTERNET" />
+  <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+                   android:usesPermissionFlags="neverForLocation" />
   <application>
     <!-- Add any classes that implement the Snippet interface as meta-data, whose
          value is a comma-separated string, each section being the package path
@@ -28,7 +30,8 @@
     <meta-data
         android:name="mobly-snippets"
         android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
-                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet" />
+                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet,
+                       com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet" />
   </application>
   <!-- Add an instrumentation tag so that the app can be launched through an
        instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
new file mode 100644
index 0000000..e0929bb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.snippet.connectivity
+
+import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pManager
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.fail
+
+private const val TIMEOUT_MS = 60000L
+
+class Wifip2pMultiDevicesSnippet : Snippet {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
+    private val wifiManager by lazy {
+        context.getSystemService(WifiManager::class.java)
+                ?: fail("Could not get WifiManager service")
+    }
+    private val wifip2pManager by lazy {
+        context.getSystemService(WifiP2pManager::class.java)
+                ?: fail("Could not get WifiP2pManager service")
+    }
+    private lateinit var wifip2pChannel: WifiP2pManager.Channel
+
+    @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
+    fun isP2pSupported() = wifiManager.isP2pSupported()
+
+    @Rpc(description = "Start Wi-Fi P2P")
+    fun startWifiP2p() {
+        // Initialize Wi-Fi P2P
+        wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null)
+
+        // Ensure the Wi-Fi P2P channel is available
+        val p2pStateEnabledFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestP2pState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
+                p2pStateEnabledFuture.complete(true)
+            }
+        }
+        p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    @Rpc(description = "Stop Wi-Fi P2P")
+    fun stopWifiP2p() {
+        if (this::wifip2pChannel.isInitialized) {
+            wifip2pManager.cancelConnect(wifip2pChannel, null)
+            wifip2pManager.removeGroup(wifip2pChannel, null)
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 21eb90f..9458460 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -195,7 +195,6 @@
 
 import androidx.test.filters.RequiresDevice;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.DynamicConfigDeviceSide;
 import com.android.internal.util.ArrayUtils;
@@ -211,6 +210,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
@@ -274,7 +274,8 @@
 import fi.iki.elonen.NanoHTTPD.Response.IStatus;
 import fi.iki.elonen.NanoHTTPD.Response.Status;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
 public class ConnectivityManagerTest {
     @Rule(order = 1)
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 752891f..fa44ae9 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -852,13 +852,14 @@
     }
 
     public void doTestContinuousQueries(Executor executor) throws InterruptedException {
-        final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN;
         for (Network network : getTestableNetworks()) {
             for (int i = 0; i < QUERY_TIMES ; ++i) {
-                final VerifyCancelInetAddressCallback callback =
-                        new VerifyCancelInetAddressCallback(msg, null);
                 // query v6/v4 in turn
                 boolean queryV6 = (i % 2 == 0);
+                final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN
+                        + " on " + network + ", queryV6=" + queryV6;
+                final VerifyCancelInetAddressCallback callback =
+                        new VerifyCancelInetAddressCallback(msg, null);
                 mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
                         FLAG_NO_CACHE_LOOKUP, executor, null, callback);
 
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 06a827b..2c7d5c6 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -40,10 +40,10 @@
 import android.system.OsConstants;
 import android.util.ArraySet;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 
 import org.junit.Before;
@@ -53,7 +53,8 @@
 
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
 public class MultinetworkApiTest {
     @Rule(order = 1)
     public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index beb9274..60081d4 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -194,15 +194,16 @@
 // TODO : enable this in a Mainline update or in V.
 private const val SHOULD_CREATE_NETWORKS_IMMEDIATELY = false
 
-@RunWith(DevSdkIgnoreRunner::class)
-// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
-// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
-@IgnoreUpTo(Build.VERSION_CODES.R)
+@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
 // NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
 // for modules other than Connectivity does not provide much value. Only run them in connectivity
 // module MTS, so the tests only need to cover the case of an updated NetworkAgent.
 @ConnectivityModuleTest
-@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
+// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
 class NetworkAgentTest {
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index c118d0a..9d158fd 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -40,6 +40,6 @@
     srcs: [
         "bpf_existence_test.cpp",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
     min_sdk_version: "30", // Ensure test runs on R and above.
 }
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index c5088c6..ab2f28c 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -32,7 +32,7 @@
         "libmodules-utils-build",
         "libutils",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
 }
 
 filegroup {
diff --git a/tests/unit/java/com/android/server/CallbackQueueTest.kt b/tests/unit/java/com/android/server/CallbackQueueTest.kt
new file mode 100644
index 0000000..a6dd5c3
--- /dev/null
+++ b/tests/unit/java/com/android/server/CallbackQueueTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_NETID_1 = 123
+
+// Maximum 16 bits unsigned value
+private const val TEST_NETID_2 = 0xffff
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CallbackQueueTest {
+    @Test
+    fun testAddCallback() {
+        val cbs = listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED
+        )
+        val queue = CallbackQueue(intArrayOf()).apply {
+            cbs.forEach { addCallback(it.first, it.second) }
+        }
+
+        assertQueueEquals(cbs, queue)
+    }
+
+    @Test
+    fun testHasCallback() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+        }
+
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_2, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED))
+
+        assertFalse(queue.hasCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED))
+        assertFalse(queue.hasCallback(1234, CALLBACK_AVAILABLE))
+        assertFalse(queue.hasCallback(TEST_NETID_1, 5678))
+        assertFalse(queue.hasCallback(1234, 5678))
+    }
+
+    @Test
+    fun testRemoveCallbacks() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            assertTrue(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue)
+    }
+
+    @Test
+    fun testRemoveCallbacksForNetId() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacksForNetId(TEST_NETID_2))
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            assertFalse(removeCallbacksForNetId(TEST_NETID_1))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_IP_CHANGED)
+            assertTrue(removeCallbacksForNetId(TEST_NETID_2))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+        ), queue)
+    }
+
+    @Test
+    fun testConstructorFromExistingArray() {
+        val queue1 = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+        }
+        val queue2 = CallbackQueue(queue1.shrinkedBackingArray)
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue2)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", CallbackQueue(intArrayOf()).toString())
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            }.toString()
+        )
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1),CALLBACK_CAP_CHANGED($TEST_NETID_2)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+                addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            }.toString()
+        )
+    }
+
+    @Test
+    fun testMaxNetId() {
+        // CallbackQueue assumes netIds are at most 16 bits
+        assertTrue(NetIdManager.MAX_NET_ID <= 0xffff)
+    }
+
+    @Test
+    fun testMaxCallbackId() {
+        // CallbackQueue assumes callback IDs are at most 16 bits.
+        val constants = ConnectivityManager::class.java.declaredFields.filter {
+            Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+                    it.name.startsWith("CALLBACK_")
+        }
+        constants.forEach {
+            it.isAccessible = true
+            assertTrue(it.get(null) as Int <= 0xffff)
+        }
+    }
+}
+
+private fun assertQueueEquals(expected: List<Pair<Int, Int>>, actual: CallbackQueue) {
+    assertEquals(
+        expected.size,
+        actual.length(),
+        "Size mismatch between expected: $expected and actual: $actual"
+    )
+
+    var nextIndex = 0
+    actual.forEach { netId, cbId ->
+        val (expNetId, expCbId) = expected[nextIndex]
+        val msg = "$actual does not match $expected at index $nextIndex"
+        assertEquals(expNetId, netId, msg)
+        assertEquals(expCbId, cbId, msg)
+        nextIndex++
+    }
+    // Ensure forEach iterations and size are consistent
+    assertEquals(expected.size, nextIndex)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index be7f2a3..999d17d 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2180,6 +2180,7 @@
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
                 case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+                case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index cf88d05..5c3ad22 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -27,16 +27,11 @@
 import com.android.server.connectivity.mdns.MdnsPointerRecord
 import com.android.server.connectivity.mdns.MdnsRecord
 import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
 import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import java.net.DatagramPacket
 import kotlin.test.assertContentEquals
-import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -46,59 +41,6 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsUtilsTest {
-    @Test
-    fun testToDnsLowerCase() {
-        assertEquals("test", toDnsLowerCase("TEST"))
-        assertEquals("test", toDnsLowerCase("TeSt"))
-        assertEquals("test", toDnsLowerCase("test"))
-        assertEquals("tÉst", toDnsLowerCase("TÉST"))
-        assertEquals("ลฃést", toDnsLowerCase("ลฃést"))
-        // Unicode characters 0x10000 (๐€€), 0x10001 (๐€), 0x10041 (๐)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertEquals(
-            "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
-        )
-        // Also test some characters where the first surrogate is not \ud800
-        assertEquals(
-            "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                toDnsLowerCase(
-                    "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
-                )
-        )
-    }
-
-    @Test
-    fun testToDnsLabelsLowerCase() {
-        assertArrayEquals(
-            arrayOf("test", "tÉst", "ลฃést"),
-            toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ลฃést"))
-        )
-    }
-
-    @Test
-    fun testEqualsIgnoreDnsCase() {
-        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
-        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
-        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
-        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
-        assertFalse(equalsIgnoreDnsCase("ลขÉST", "ลฃést"))
-        // Unicode characters 0x10000 (๐€€), 0x10001 (๐€), 0x10041 (๐)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertTrue(equalsIgnoreDnsCase(
-            "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
-        ))
-        // Also test some characters where the first surrogate is not \ud800
-        assertTrue(equalsIgnoreDnsCase(
-            "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
-        ))
-    }
 
     @Test
     fun testTruncateServiceName() {
@@ -107,14 +49,6 @@
     }
 
     @Test
-    fun testEqualsLabelIgnoreDnsCase() {
-        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
-    }
-
-    @Test
     fun testTypeEqualsOrIsSubtype() {
         assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
             arrayOf("_type", "_tcp", "local"),
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index ed72fd2..de56ae5 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -165,6 +165,7 @@
         it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
         it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
         it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
+        it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
     }
     fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)