Move mDNS code to service-connectivity-t

Instead of using a separate service-mdns library, move the code to
service-connectivity-t.

service-connectivity-t is chosen because it has access to hidden API of
classes that were made updatable in T, such as NsdServiceInfo and
NsdManager. mdns code can be there as it is only loaded on T+.

Bug: 241738458
Test: atest
Change-Id: I7eb6c9ab8bf0e0a614ea2994c6ed80a1a780241f
diff --git a/service-t/Android.bp b/service-t/Android.bp
index d876166..5bf2973 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -52,6 +52,7 @@
         "framework-connectivity-t-pre-jarjar",
         // TODO: use framework-tethering-pre-jarjar when it is separated from framework-tethering
         "framework-tethering.impl",
+        "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
         "ServiceConnectivityResources",
diff --git a/service-t/src/com/android/server/mdns/ConnectivityMonitor.java b/service-t/src/com/android/server/mdns/ConnectivityMonitor.java
new file mode 100644
index 0000000..1623669
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/ConnectivityMonitor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.net.Network;
+
+/** Interface for monitoring connectivity changes. */
+public interface ConnectivityMonitor {
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    void startWatchingConnectivityChanges();
+
+    /** Stops monitoring changes of connectivity. */
+    void stopWatchingConnectivityChanges();
+
+    void notifyConnectivityChange();
+
+    /** Get available network which is received from connectivity change. */
+    Network getAvailableNetwork();
+
+    /** Listener interface for receiving connectivity changes. */
+    interface Listener {
+        void onConnectivityChanged();
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/ConnectivityMonitorWithConnectivityManager.java b/service-t/src/com/android/server/mdns/ConnectivityMonitorWithConnectivityManager.java
new file mode 100644
index 0000000..551e3db
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
+public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
+    private static final String TAG = "ConnMntrWConnMgr";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+
+    private final Listener listener;
+    private final ConnectivityManager.NetworkCallback networkCallback;
+    private final ConnectivityManager connectivityManager;
+    // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
+    // the receiver is unregistered twice yet.
+    private boolean isCallbackRegistered = false;
+    private Network lastAvailableNetwork = null;
+
+    @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+        this.listener = listener;
+
+        connectivityManager =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        networkCallback =
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        LOGGER.log("network available.");
+                        lastAvailableNetwork = network;
+                        notifyConnectivityChange();
+                    }
+
+                    @Override
+                    public void onLost(Network network) {
+                        LOGGER.log("network lost.");
+                        notifyConnectivityChange();
+                    }
+
+                    @Override
+                    public void onUnavailable() {
+                        LOGGER.log("network unavailable.");
+                        notifyConnectivityChange();
+                    }
+                };
+    }
+
+    @Override
+    public void notifyConnectivityChange() {
+        listener.onConnectivityChanged();
+    }
+
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public void startWatchingConnectivityChanges() {
+        LOGGER.log("Start watching connectivity changes");
+        if (isCallbackRegistered) {
+            return;
+        }
+
+        connectivityManager.registerNetworkCallback(
+                new NetworkRequest.Builder().addTransportType(
+                        NetworkCapabilities.TRANSPORT_WIFI).build(),
+                networkCallback);
+        isCallbackRegistered = true;
+    }
+
+    /** Stops monitoring changes of connectivity. */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public void stopWatchingConnectivityChanges() {
+        LOGGER.log("Stop watching connectivity changes");
+        if (!isCallbackRegistered) {
+            return;
+        }
+
+        connectivityManager.unregisterNetworkCallback(networkCallback);
+        isCallbackRegistered = false;
+    }
+
+    @Override
+    @Nullable
+    public Network getAvailableNetwork() {
+        return lastAvailableNetwork;
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/mdns/EnqueueMdnsQueryCallable.java
new file mode 100644
index 0000000..fdd1478
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/EnqueueMdnsQueryCallable.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
+ * query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
+ * and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
+ * it can not be enqueued, then call to {@link #call()} returns {@code null}.
+ */
+public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
+
+    private static final String TAG = "MdnsQueryCallable";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final List<Integer> castShellEmulatorMdnsPorts;
+
+    static {
+        castShellEmulatorMdnsPorts = new ArrayList<>();
+        String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
+
+        for (String port : stringPorts) {
+            try {
+                castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
+            } catch (NumberFormatException e) {
+                // Ignore.
+            }
+        }
+    }
+
+    private final WeakReference<MdnsSocketClientBase> weakRequestSender;
+    private final MdnsPacketWriter packetWriter;
+    private final String[] serviceTypeLabels;
+    private final List<String> subtypes;
+    private final boolean expectUnicastResponse;
+    private final int transactionId;
+    private final Network network;
+
+    EnqueueMdnsQueryCallable(
+            @NonNull MdnsSocketClientBase requestSender,
+            @NonNull MdnsPacketWriter packetWriter,
+            @NonNull String serviceType,
+            @NonNull Collection<String> subtypes,
+            boolean expectUnicastResponse,
+            int transactionId,
+            @Nullable Network network) {
+        weakRequestSender = new WeakReference<>(requestSender);
+        this.packetWriter = packetWriter;
+        serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+        this.subtypes = new ArrayList<>(subtypes);
+        this.expectUnicastResponse = expectUnicastResponse;
+        this.transactionId = transactionId;
+        this.network = network;
+    }
+
+    // Incompatible return type for override of Callable#call().
+    @SuppressWarnings("nullness:override.return.invalid")
+    @Override
+    @Nullable
+    public Pair<Integer, List<String>> call() {
+        try {
+            MdnsSocketClientBase requestSender = weakRequestSender.get();
+            if (requestSender == null) {
+                return null;
+            }
+
+            int numQuestions = 1;
+            if (!subtypes.isEmpty()) {
+                numQuestions += subtypes.size();
+            }
+
+            // Header.
+            packetWriter.writeUInt16(transactionId); // transaction ID
+            packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
+            packetWriter.writeUInt16(numQuestions); // number of questions
+            packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
+            packetWriter.writeUInt16(0); // number of authority entries
+            packetWriter.writeUInt16(0); // number of additional records
+
+            // Question(s). There will be one question for each (fqdn+subtype, recordType)
+          // combination,
+            // as well as one for each (fqdn, recordType) combination.
+
+            for (String subtype : subtypes) {
+                String[] labels = new String[serviceTypeLabels.length + 2];
+                labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+                labels[1] = MdnsConstants.SUBTYPE_LABEL;
+                System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+                packetWriter.writeLabels(labels);
+                packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+                packetWriter.writeUInt16(
+                        MdnsConstants.QCLASS_INTERNET
+                                | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+            }
+
+            packetWriter.writeLabels(serviceTypeLabels);
+            packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+            packetWriter.writeUInt16(
+                    MdnsConstants.QCLASS_INTERNET
+                            | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+
+            if (requestSender instanceof MdnsMultinetworkSocketClient) {
+                sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, network);
+                for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+                    sendPacketToIpv4AndIpv6(requestSender, emulatorPort, network);
+                }
+            } else if (requestSender instanceof MdnsSocketClient) {
+                final MdnsSocketClient client = (MdnsSocketClient) requestSender;
+                InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
+                if (client.isOnIPv6OnlyNetwork()) {
+                    mdnsAddress = MdnsConstants.getMdnsIPv6Address();
+                }
+
+                sendPacketTo(client, new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
+                for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+                    sendPacketTo(client, new InetSocketAddress(mdnsAddress, emulatorPort));
+                }
+            } else {
+                throw new IOException("Unknown socket client type: " + requestSender.getClass());
+            }
+            return Pair.create(transactionId, subtypes);
+        } catch (IOException e) {
+            LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+                    TextUtils.join(",", subtypes)), e);
+            return null;
+        }
+    }
+
+    private void sendPacketTo(MdnsSocketClientBase requestSender, InetSocketAddress address)
+            throws IOException {
+        DatagramPacket packet = packetWriter.getPacket(address);
+        if (expectUnicastResponse) {
+            requestSender.sendUnicastPacket(packet);
+        } else {
+            requestSender.sendMulticastPacket(packet);
+        }
+    }
+
+    private void sendPacketFromNetwork(MdnsSocketClientBase requestSender,
+            InetSocketAddress address, Network network)
+            throws IOException {
+        DatagramPacket packet = packetWriter.getPacket(address);
+        if (expectUnicastResponse) {
+            requestSender.sendUnicastPacket(packet, network);
+        } else {
+            requestSender.sendMulticastPacket(packet, network);
+        }
+    }
+
+    private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port,
+            Network network) {
+        try {
+            sendPacketFromNetwork(requestSender,
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), network);
+        } catch (IOException e) {
+            Log.i(TAG, "Can't send packet to IPv4", e);
+        }
+        try {
+            sendPacketFromNetwork(requestSender,
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), network);
+        } catch (IOException e) {
+            Log.i(TAG, "Can't send packet to IPv6", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/ExecutorProvider.java b/service-t/src/com/android/server/mdns/ExecutorProvider.java
new file mode 100644
index 0000000..72b65e0
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/ExecutorProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
+ * instances, and provides method to shutdown all the created executors.
+ */
+public class ExecutorProvider {
+
+    private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
+            new ArraySet<>();
+
+    /** Returns a new {@link ScheduledExecutorService} instance. */
+    public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
+        // TODO: actually use a pool ?
+        ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
+        serviceTypeClientSchedulerExecutors.add(executor);
+        return executor;
+    }
+
+    /** Shuts down all the created {@link ScheduledExecutorService} instances. */
+    public void shutdownAll() {
+        for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+            executor.shutdownNow();
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/mdns/MdnsAdvertiser.java
new file mode 100644
index 0000000..4e40efe
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsAdvertiser.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * MdnsAdvertiser manages advertising services per {@link com.android.server.NsdService} requests.
+ *
+ * All methods except the constructor must be called on the looper thread.
+ */
+public class MdnsAdvertiser {
+    private static final String TAG = MdnsAdvertiser.class.getSimpleName();
+    static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Looper mLooper;
+    private final AdvertiserCallback mCb;
+
+    // Max-sized buffers to be used as temporary buffer to read/build packets. May be used by
+    // multiple components, but only for self-contained operations in the looper thread, so not
+    // concurrently.
+    // TODO: set according to MTU. 1300 should fit for ethernet MTU 1500 with some overhead.
+    private final byte[] mPacketCreationBuffer = new byte[1300];
+
+    private final MdnsSocketProvider mSocketProvider;
+    private final ArrayMap<Network, InterfaceAdvertiserRequest> mAdvertiserRequests =
+            new ArrayMap<>();
+    private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAllAdvertisers =
+            new ArrayMap<>();
+    private final SparseArray<Registration> mRegistrations = new SparseArray<>();
+    private final Dependencies mDeps;
+
+    /**
+     * Dependencies for {@link MdnsAdvertiser}, useful for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see MdnsInterfaceAdvertiser
+         */
+        public MdnsInterfaceAdvertiser makeAdvertiser(@NonNull MdnsInterfaceSocket socket,
+                @NonNull List<LinkAddress> initialAddresses,
+                @NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
+                @NonNull MdnsInterfaceAdvertiser.Callback cb) {
+            // Note NetworkInterface is final and not mockable
+            final String logTag = socket.getInterface().getName();
+            return new MdnsInterfaceAdvertiser(logTag, socket, initialAddresses, looper,
+                    packetCreationBuffer, cb);
+        }
+    }
+
+    private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
+            new MdnsInterfaceAdvertiser.Callback() {
+        @Override
+        public void onRegisterServiceSucceeded(
+                @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
+            // Wait for all current interfaces to be done probing before notifying of success.
+            if (anyAdvertiser(a -> a.isProbing(serviceId))) return;
+            // The service may still be unregistered/renamed if a conflict is found on a later added
+            // interface, or if a conflicting announcement/reply is detected (RFC6762 9.)
+
+            final Registration registration = mRegistrations.get(serviceId);
+            if (registration == null) {
+                Log.wtf(TAG, "Register succeeded for unknown registration");
+                return;
+            }
+            if (!registration.mNotifiedRegistrationSuccess) {
+                mCb.onRegisterServiceSucceeded(serviceId, registration.getServiceInfo());
+                registration.mNotifiedRegistrationSuccess = true;
+            }
+        }
+
+        @Override
+        public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
+            // TODO: handle conflicts found after registration (during or after probing)
+        }
+
+        @Override
+        public void onDestroyed(@NonNull MdnsInterfaceSocket socket) {
+            for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
+                if (mAdvertiserRequests.valueAt(i).onAdvertiserDestroyed(socket)) {
+                    mAdvertiserRequests.removeAt(i);
+                }
+            }
+            mAllAdvertisers.remove(socket);
+        }
+    };
+
+    /**
+     * A request for a {@link MdnsInterfaceAdvertiser}.
+     *
+     * This class tracks services to be advertised on all sockets provided via a registered
+     * {@link MdnsSocketProvider.SocketCallback}.
+     */
+    private class InterfaceAdvertiserRequest implements MdnsSocketProvider.SocketCallback {
+        /** Registrations to add to newer MdnsInterfaceAdvertisers when sockets are created. */
+        @NonNull
+        private final SparseArray<Registration> mPendingRegistrations = new SparseArray<>();
+        @NonNull
+        private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAdvertisers =
+                new ArrayMap<>();
+
+        InterfaceAdvertiserRequest(@Nullable Network requestedNetwork) {
+            mSocketProvider.requestSocket(requestedNetwork, this);
+        }
+
+        /**
+         * Called when an advertiser was destroyed, after all services were unregistered and it sent
+         * exit announcements, or the interface is gone.
+         *
+         * @return true if this {@link InterfaceAdvertiserRequest} should now be deleted.
+         */
+        boolean onAdvertiserDestroyed(@NonNull MdnsInterfaceSocket socket) {
+            mAdvertisers.remove(socket);
+            if (mAdvertisers.size() == 0 && mPendingRegistrations.size() == 0) {
+                // No advertiser is using sockets from this request anymore (in particular for exit
+                // announcements), and there is no registration so newer sockets will not be
+                // necessary, so the request can be unregistered.
+                mSocketProvider.unrequestSocket(this);
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Get the ID of a conflicting service, or -1 if none.
+         */
+        int getConflictingService(@NonNull NsdServiceInfo info) {
+            for (int i = 0; i < mPendingRegistrations.size(); i++) {
+                final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
+                if (info.getServiceName().equals(other.getServiceName())
+                        && info.getServiceType().equals(other.getServiceType())) {
+                    return mPendingRegistrations.keyAt(i);
+                }
+            }
+            return -1;
+        }
+
+        void addService(int id, Registration registration)
+                throws NameConflictException {
+            final int conflicting = getConflictingService(registration.getServiceInfo());
+            if (conflicting >= 0) {
+                throw new NameConflictException(conflicting);
+            }
+
+            mPendingRegistrations.put(id, registration);
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+            }
+        }
+
+        void removeService(int id) {
+            mPendingRegistrations.remove(id);
+            for (int i = 0; i < mAdvertisers.size(); i++) {
+                mAdvertisers.valueAt(i).removeService(id);
+            }
+        }
+
+        @Override
+        public void onSocketCreated(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket,
+                @NonNull List<LinkAddress> addresses) {
+            MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.get(socket);
+            if (advertiser == null) {
+                advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
+                        mInterfaceAdvertiserCb);
+                mAllAdvertisers.put(socket, advertiser);
+                advertiser.start();
+            }
+            mAdvertisers.put(socket, advertiser);
+            for (int i = 0; i < mPendingRegistrations.size(); i++) {
+                try {
+                    advertiser.addService(mPendingRegistrations.keyAt(i),
+                            mPendingRegistrations.valueAt(i).getServiceInfo());
+                } catch (NameConflictException e) {
+                    Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
+                }
+            }
+        }
+
+        @Override
+        public void onInterfaceDestroyed(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket) {
+            final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
+            if (advertiser != null) advertiser.destroyNow();
+        }
+
+        @Override
+        public void onAddressesChanged(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
+            final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
+            if (advertiser != null) advertiser.updateAddresses(addresses);
+        }
+    }
+
+    private static class Registration {
+        @NonNull
+        final String mOriginalName;
+        boolean mNotifiedRegistrationSuccess;
+        private int mConflictCount;
+        @NonNull
+        private NsdServiceInfo mServiceInfo;
+
+        private Registration(@NonNull NsdServiceInfo serviceInfo) {
+            this.mOriginalName = serviceInfo.getServiceName();
+            this.mServiceInfo = serviceInfo;
+        }
+
+        /**
+         * Update the registration to use a different service name, after a conflict was found.
+         *
+         * If a name conflict was found during probing or because different advertising requests
+         * used the same name, the registration is attempted again with a new name (here using
+         * a number suffix, (1), (2) etc). Registration success is notified once probing succeeds
+         * with a new name. This matches legacy behavior based on mdnsresponder, and appendix D of
+         * RFC6763.
+         * @return The new service info with the updated name.
+         */
+        @NonNull
+        private NsdServiceInfo updateForConflict() {
+            mConflictCount++;
+            // In case of conflict choose a different service name. After the first conflict use
+            // "Name (2)", then "Name (3)" etc.
+            // TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
+            final NsdServiceInfo newInfo = new NsdServiceInfo();
+            newInfo.setServiceName(mOriginalName + " (" + (mConflictCount + 1) + ")");
+            newInfo.setServiceType(mServiceInfo.getServiceType());
+            for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
+                newInfo.setAttribute(attr.getKey(), attr.getValue());
+            }
+            newInfo.setHost(mServiceInfo.getHost());
+            newInfo.setPort(mServiceInfo.getPort());
+            newInfo.setNetwork(mServiceInfo.getNetwork());
+            // interfaceIndex is not set when registering
+
+            mServiceInfo = newInfo;
+            return mServiceInfo;
+        }
+
+        @NonNull
+        public NsdServiceInfo getServiceInfo() {
+            return mServiceInfo;
+        }
+    }
+
+    /**
+     * Callbacks for advertising services.
+     *
+     * Every method is called on the MdnsAdvertiser looper thread.
+     */
+    public interface AdvertiserCallback {
+        /**
+         * Called when a service was successfully registered, after probing.
+         *
+         * @param serviceId ID of the service provided when registering.
+         * @param registeredInfo Registered info, which may be different from the requested info,
+         *                       after probing and possibly choosing alternative service names.
+         */
+        void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo);
+
+        /**
+         * Called when service registration failed.
+         *
+         * @param serviceId ID of the service provided when registering.
+         * @param errorCode One of {@code NsdManager.FAILURE_*}
+         */
+        void onRegisterServiceFailed(int serviceId, int errorCode);
+
+        // Unregistration is notified immediately as success in NsdService so no callback is needed
+        // here.
+    }
+
+    public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
+            @NonNull AdvertiserCallback cb) {
+        this(looper, socketProvider, cb, new Dependencies());
+    }
+
+    @VisibleForTesting
+    MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
+            @NonNull AdvertiserCallback cb, @NonNull Dependencies deps) {
+        mLooper = looper;
+        mCb = cb;
+        mSocketProvider = socketProvider;
+        mDeps = deps;
+    }
+
+    private void checkThread() {
+        if (Thread.currentThread() != mLooper.getThread()) {
+            throw new IllegalStateException("This must be called on the looper thread");
+        }
+    }
+
+    /**
+     * Add a service to advertise.
+     * @param id A unique ID for the service.
+     * @param service The service info to advertise.
+     */
+    public void addService(int id, NsdServiceInfo service) {
+        checkThread();
+        if (mRegistrations.get(id) != null) {
+            Log.e(TAG, "Adding duplicate registration for " + service);
+            // TODO (b/264986328): add a more specific error code
+            mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+            return;
+        }
+
+        if (DBG) {
+            Log.i(TAG, "Adding service " + service + " with ID " + id);
+        }
+
+        try {
+            final Registration registration = new Registration(service);
+            while (!tryAddRegistration(id, registration)) {
+                registration.updateForConflict();
+            }
+
+            mRegistrations.put(id, registration);
+        } catch (IOException e) {
+            Log.e(TAG, "Error adding service " + service, e);
+            removeService(id);
+            // TODO (b/264986328): add a more specific error code
+            mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
+        }
+    }
+
+    private boolean tryAddRegistration(int id, @NonNull Registration registration)
+            throws IOException {
+        final NsdServiceInfo serviceInfo = registration.getServiceInfo();
+        final Network network = serviceInfo.getNetwork();
+        try {
+            InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network);
+            if (advertiser == null) {
+                advertiser = new InterfaceAdvertiserRequest(network);
+                mAdvertiserRequests.put(network, advertiser);
+            }
+            advertiser.addService(id, registration);
+        } catch (NameConflictException e) {
+            if (DBG) {
+                Log.i(TAG, "Service name conflicts: " + serviceInfo.getServiceName());
+            }
+            removeService(id);
+            return false;
+        }
+
+        // When adding a service to a specific network, check that it does not conflict with other
+        // registrations advertising on all networks
+        final InterfaceAdvertiserRequest allNetworksAdvertiser = mAdvertiserRequests.get(null);
+        if (network != null && allNetworksAdvertiser != null
+                && allNetworksAdvertiser.getConflictingService(serviceInfo) >= 0) {
+            if (DBG) {
+                Log.i(TAG, "Service conflicts with advertisement on all networks: "
+                        + serviceInfo.getServiceName());
+            }
+            removeService(id);
+            return false;
+        }
+
+        mRegistrations.put(id, registration);
+        return true;
+    }
+
+    /**
+     * Remove a previously added service.
+     * @param id ID used when registering.
+     */
+    public void removeService(int id) {
+        checkThread();
+        if (!mRegistrations.contains(id)) return;
+        if (DBG) {
+            Log.i(TAG, "Removing service with ID " + id);
+        }
+        for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
+            final InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.valueAt(i);
+            advertiser.removeService(id);
+        }
+        mRegistrations.remove(id);
+    }
+
+    private boolean anyAdvertiser(@NonNull Predicate<MdnsInterfaceAdvertiser> predicate) {
+        for (int i = 0; i < mAllAdvertisers.size(); i++) {
+            if (predicate.test(mAllAdvertisers.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/mdns/MdnsAnnouncer.java
new file mode 100644
index 0000000..7c84323
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsAnnouncer.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Sends mDns announcements when a service registration changes and at regular intervals.
+ *
+ * This allows maintaining other hosts' caches up-to-date. See RFC6762 8.3.
+ */
+public class MdnsAnnouncer extends MdnsPacketRepeater<MdnsAnnouncer.AnnouncementInfo> {
+    private static final long ANNOUNCEMENT_INITIAL_DELAY_MS = 1000L;
+    @VisibleForTesting
+    static final int ANNOUNCEMENT_COUNT = 8;
+
+    @NonNull
+    private final String mLogTag;
+
+    /** Announcement request to send with {@link MdnsAnnouncer}. */
+    public static class AnnouncementInfo implements MdnsPacketRepeater.Request {
+        @NonNull
+        private final MdnsPacket mPacket;
+
+        AnnouncementInfo(List<MdnsRecord> announcedRecords, List<MdnsRecord> additionalRecords) {
+            // Records to announce (as answers)
+            // Records to place in the "Additional records", with NSEC negative responses
+            // to mark records that have been verified unique
+            final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
+            mPacket = new MdnsPacket(flags,
+                    Collections.emptyList() /* questions */,
+                    announcedRecords,
+                    Collections.emptyList() /* authorityRecords */,
+                    additionalRecords);
+        }
+
+        @Override
+        public MdnsPacket getPacket(int index) {
+            return mPacket;
+        }
+
+        @Override
+        public long getDelayMs(int nextIndex) {
+            // Delay is doubled for each announcement
+            return ANNOUNCEMENT_INITIAL_DELAY_MS << (nextIndex - 1);
+        }
+
+        @Override
+        public int getNumSends() {
+            return ANNOUNCEMENT_COUNT;
+        }
+    }
+
+    public MdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
+            @NonNull MdnsReplySender replySender,
+            @Nullable PacketRepeaterCallback<AnnouncementInfo> cb) {
+        super(looper, replySender, cb);
+        mLogTag = MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag;
+    }
+
+    @Override
+    protected String getTag() {
+        return mLogTag;
+    }
+
+    // TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
+    // so it can update the last advertised timestamp of the associated records.
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsAnyRecord.java b/service-t/src/com/android/server/mdns/MdnsAnyRecord.java
new file mode 100644
index 0000000..fcfe9f7
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsAnyRecord.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.net.DnsResolver;
+
+import java.io.IOException;
+
+/**
+ * A mDNS "ANY" record, used in mDNS questions to query for any record type.
+ */
+public class MdnsAnyRecord extends MdnsRecord {
+
+    protected MdnsAnyRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        super(name, TYPE_ANY, reader, true /* isQuestion */);
+    }
+
+    protected MdnsAnyRecord(String[] name, boolean unicast) {
+        super(name, TYPE_ANY, DnsResolver.CLASS_IN /* cls */,
+                0L /* receiptTimeMillis */, unicast /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        // No data to read
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        // No data to write
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsConfigs.java b/service-t/src/com/android/server/mdns/MdnsConfigs.java
new file mode 100644
index 0000000..75c7e6c
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsConfigs.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * mDNS configuration values.
+ *
+ * TODO: consider making some of these adjustable via flags.
+ */
+public class MdnsConfigs {
+    public static String[] castShellEmulatorMdnsPorts() {
+        return new String[0];
+    }
+
+    public static long initialTimeBetweenBurstsMs() {
+        return 5000L;
+    }
+
+    public static long timeBetweenBurstsMs() {
+        return 20_000L;
+    }
+
+    public static int queriesPerBurst() {
+        return 3;
+    }
+
+    public static long timeBetweenQueriesInBurstMs() {
+        return 1000L;
+    }
+
+    public static int queriesPerBurstPassive() {
+        return 1;
+    }
+
+    public static boolean alwaysAskForUnicastResponseInEachBurst() {
+        return false;
+    }
+
+    public static boolean useSessionIdToScheduleMdnsTask() {
+        return false;
+    }
+
+    public static boolean shouldCancelScanTaskWhenFutureIsNull() {
+        return false;
+    }
+
+    public static long sleepTimeForSocketThreadMs() {
+        return 20_000L;
+    }
+
+    public static boolean checkMulticastResponse() {
+        return false;
+    }
+
+    public static boolean useSeparateSocketToSendUnicastQuery() {
+        return false;
+    }
+
+    public static long checkMulticastResponseIntervalMs() {
+        return 10_000L;
+    }
+
+    public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
+        return true;
+    }
+
+    public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
+        return false;
+    }
+
+    public static int mdnsPacketQueueMaxSize() {
+        return Integer.MAX_VALUE;
+    }
+
+    public static boolean preferIpv6() {
+        return false;
+    }
+
+    public static boolean removeServiceAfterTtlExpires() {
+        return false;
+    }
+
+    public static boolean allowSearchOptionsToRemoveExpiredService() {
+        return false;
+    }
+
+    public static boolean allowNetworkInterfaceIndexPropagation() {
+        return true;
+    }
+
+    public static boolean allowMultipleSrvRecordsPerHost() {
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsConstants.java b/service-t/src/com/android/server/mdns/MdnsConstants.java
new file mode 100644
index 0000000..396be5f
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsConstants.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+
+/** mDNS-related constants. */
+@VisibleForTesting
+public final class MdnsConstants {
+    public static final int MDNS_PORT = 5353;
+    // Flags word format is:
+    // 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+    // QR [ Opcode  ] AA TC RD RA  Z AD CD [  Rcode  ]
+    // See http://www.networksorcery.com/enp/protocol/dns.htm
+    // For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
+    // should be 0.
+    public static final int FLAGS_QUERY = 0x0000;
+    public static final int FLAGS_RESPONSE_MASK = 0xF80F;
+    public static final int FLAGS_RESPONSE = 0x8000;
+    public static final int QCLASS_INTERNET = 0x0001;
+    public static final int QCLASS_UNICAST = 0x8000;
+    public static final String SUBTYPE_LABEL = "_sub";
+    public static final String SUBTYPE_PREFIX = "_";
+    private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
+    private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+    private static InetAddress mdnsAddress;
+    private MdnsConstants() {
+    }
+
+    public static InetAddress getMdnsIPv4Address() {
+        synchronized (MdnsConstants.class) {
+            InetAddress addr = null;
+            try {
+                addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
+            } catch (UnknownHostException e) {
+                /* won't happen */
+            }
+            mdnsAddress = addr;
+            return mdnsAddress;
+        }
+    }
+
+    public static InetAddress getMdnsIPv6Address() {
+        synchronized (MdnsConstants.class) {
+            InetAddress addr = null;
+            try {
+                addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
+            } catch (UnknownHostException e) {
+                /* won't happen */
+            }
+            mdnsAddress = addr;
+            return mdnsAddress;
+        }
+    }
+
+    public static Charset getUtf8Charset() {
+        return UTF_8;
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/mdns/MdnsDiscoveryManager.java
new file mode 100644
index 0000000..cc6b98b
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsDiscoveryManager.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
+ * notify them when a mDNS service instance is found, updated, or removed?
+ */
+public class MdnsDiscoveryManager implements MdnsSocketClientBase.Callback {
+    private static final String TAG = MdnsDiscoveryManager.class.getSimpleName();
+    public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
+
+    private final ExecutorProvider executorProvider;
+    private final MdnsSocketClientBase socketClient;
+
+    private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
+
+    public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
+            @NonNull MdnsSocketClientBase socketClient) {
+        this.executorProvider = executorProvider;
+        this.socketClient = socketClient;
+    }
+
+    /**
+     * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
+     * {@code listener} for receiving mDNS service discovery responses.
+     *
+     * @param serviceType   The type of the service to discover.
+     * @param listener      The {@link MdnsServiceBrowserListener} listener.
+     * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
+     *                      serviceType}.
+     */
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public synchronized void registerListener(
+            @NonNull String serviceType,
+            @NonNull MdnsServiceBrowserListener listener,
+            @NonNull MdnsSearchOptions searchOptions) {
+        LOGGER.log(
+                "Registering listener for subtypes: %s",
+                TextUtils.join(",", searchOptions.getSubtypes()));
+        if (serviceTypeClients.isEmpty()) {
+            // First listener. Starts the socket client.
+            try {
+                socketClient.startDiscovery();
+            } catch (IOException e) {
+                LOGGER.e("Failed to start discover.", e);
+                return;
+            }
+        }
+        // Request the network for discovery.
+        socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork());
+
+        // All listeners of the same service types shares the same MdnsServiceTypeClient.
+        MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+        if (serviceTypeClient == null) {
+            serviceTypeClient = createServiceTypeClient(serviceType);
+            serviceTypeClients.put(serviceType, serviceTypeClient);
+        }
+        // TODO(b/264634275): Wait for a socket to be created before sending packets.
+        serviceTypeClient.startSendAndReceive(listener, searchOptions);
+    }
+
+    /**
+     * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
+     * registered for the given service type, stops discovery for the service type.
+     *
+     * @param serviceType The type of the service to discover.
+     * @param listener    The {@link MdnsServiceBrowserListener} listener.
+     */
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    public synchronized void unregisterListener(
+            @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+        LOGGER.log("Unregistering listener for service type: %s", serviceType);
+        if (DBG) Log.d(TAG, "Unregistering listener for serviceType:" + serviceType);
+        MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+        if (serviceTypeClient == null) {
+            return;
+        }
+        if (serviceTypeClient.stopSendAndReceive(listener)) {
+            // No listener is registered for the service type anymore, remove it from the list of
+            // the service type clients.
+            serviceTypeClients.remove(serviceType);
+            if (serviceTypeClients.isEmpty()) {
+                // No discovery request. Stops the socket client.
+                socketClient.stopDiscovery();
+            }
+        }
+        // Unrequested the network.
+        socketClient.notifyNetworkUnrequested(listener);
+    }
+
+    @Override
+    public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
+        String[] name =
+                response.getPointerRecords().isEmpty()
+                        ? null
+                        : response.getPointerRecords().get(0).getName();
+        if (name != null) {
+            for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+                String[] serviceType = serviceTypeClient.getServiceTypeLabels();
+                if ((Arrays.equals(name, serviceType)
+                        || ((name.length == (serviceType.length + 2))
+                        && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+                        && MdnsRecord.labelsAreSuffix(serviceType, name)))) {
+                    serviceTypeClient.processResponse(response);
+                    return;
+                }
+            }
+        }
+    }
+
+    @Override
+    public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+        for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+            serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+        }
+    }
+
+    @VisibleForTesting
+    MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+        return new MdnsServiceTypeClient(
+                serviceType, socketClient,
+                executorProvider.newServiceTypeClientSchedulerExecutor());
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsInetAddressRecord.java b/service-t/src/com/android/server/mdns/MdnsInetAddressRecord.java
new file mode 100644
index 0000000..dd8a526
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsInetAddressRecord.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
+@VisibleForTesting
+public class MdnsInetAddressRecord extends MdnsRecord {
+    @Nullable private Inet6Address inet6Address;
+    @Nullable private Inet4Address inet4Address;
+
+    /**
+     * Constructs the {@link MdnsRecord}
+     *
+     * @param name   the service host name
+     * @param type   the type of record (either Type 'AAAA' or Type 'A')
+     * @param reader the reader to read the record from.
+     */
+    public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
+            throws IOException {
+        this(name, type, reader, false);
+    }
+
+    /**
+     * Constructs the {@link MdnsRecord}
+     *
+     * @param name       the service host name
+     * @param type       the type of record (either Type 'AAAA' or Type 'A')
+     * @param reader     the reader to read the record from.
+     * @param isQuestion whether the record is in the question section
+     */
+    public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader,
+            boolean isQuestion)
+            throws IOException {
+        super(name, type, reader, isQuestion);
+    }
+
+    public MdnsInetAddressRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
+                    long ttlMillis, InetAddress address) {
+        super(name, address instanceof Inet4Address ? TYPE_A : TYPE_AAAA,
+                MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush, ttlMillis);
+        if (address instanceof Inet4Address) {
+            inet4Address = (Inet4Address) address;
+        } else {
+            inet6Address = (Inet6Address) address;
+        }
+    }
+
+    /** Returns the IPv6 address. */
+    @Nullable
+    public Inet6Address getInet6Address() {
+        return inet6Address;
+    }
+
+    /** Returns the IPv4 address. */
+    @Nullable
+    public Inet4Address getInet4Address() {
+        return inet4Address;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        int size = 4;
+        if (super.getType() == MdnsRecord.TYPE_AAAA) {
+            size = 16;
+        }
+        byte[] buf = new byte[size];
+        reader.readBytes(buf);
+        try {
+            InetAddress address = InetAddress.getByAddress(buf);
+            if (address instanceof Inet4Address) {
+                inet4Address = (Inet4Address) address;
+                inet6Address = null;
+            } else if (address instanceof Inet6Address) {
+                inet4Address = null;
+                inet6Address = (Inet6Address) address;
+            } else {
+                inet4Address = null;
+                inet6Address = null;
+            }
+        } catch (UnknownHostException e) {
+            // Ignore exception
+        }
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        byte[] buf = null;
+        if (inet4Address != null) {
+            buf = inet4Address.getAddress();
+        } else if (inet6Address != null) {
+            buf = inet6Address.getAddress();
+        }
+        if (buf != null) {
+            writer.writeBytes(buf);
+        }
+    }
+
+    @Override
+    public String toString() {
+        String type = "AAAA";
+        if (super.getType() == MdnsRecord.TYPE_A) {
+            type = "A";
+        }
+        return String.format(
+                Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
+                inet6Address);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31)
+                + Objects.hashCode(inet4Address)
+                + Objects.hashCode(inet6Address);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsInetAddressRecord)) {
+            return false;
+        }
+
+        return super.equals(other)
+                && Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
+                && Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
new file mode 100644
index 0000000..997dcbb
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
+ */
+public class MdnsInterfaceAdvertiser {
+    private static final boolean DBG = MdnsAdvertiser.DBG;
+    @VisibleForTesting
+    public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L;
+    @NonNull
+    private final String mTag;
+    @NonNull
+    private final ProbingCallback mProbingCallback = new ProbingCallback();
+    @NonNull
+    private final AnnouncingCallback mAnnouncingCallback = new AnnouncingCallback();
+    @NonNull
+    private final MdnsRecordRepository mRecordRepository;
+    @NonNull
+    private final Callback mCb;
+    // Callbacks are on the same looper thread, but posted to the next handler loop
+    @NonNull
+    private final Handler mCbHandler;
+    @NonNull
+    private final MdnsInterfaceSocket mSocket;
+    @NonNull
+    private final MdnsAnnouncer mAnnouncer;
+    @NonNull
+    private final MdnsProber mProber;
+    @NonNull
+    private final MdnsReplySender mReplySender;
+
+    /**
+     * Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
+     */
+    interface Callback {
+        /**
+         * Called by the advertiser after it successfully registered a service, after probing.
+         */
+        void onRegisterServiceSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+
+        /**
+         * Called by the advertiser when a conflict was found, during or after probing.
+         *
+         * If a conflict is found during probing, the {@link #renameServiceForConflict} must be
+         * called to restart probing and attempt registration with a different name.
+         */
+        void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+
+        /**
+         * Called by the advertiser when it destroyed itself.
+         *
+         * This can happen after a call to {@link #destroyNow()}, or after all services were
+         * unregistered and the advertiser finished sending exit announcements.
+         */
+        void onDestroyed(@NonNull MdnsInterfaceSocket socket);
+    }
+
+    /**
+     * Callbacks from {@link MdnsProber}.
+     */
+    private class ProbingCallback implements
+            PacketRepeaterCallback<MdnsProber.ProbingInfo> {
+        @Override
+        public void onFinished(MdnsProber.ProbingInfo info) {
+            final MdnsAnnouncer.AnnouncementInfo announcementInfo;
+            if (DBG) {
+                Log.v(mTag, "Probing finished for service " + info.getServiceId());
+            }
+            mCbHandler.post(() -> mCb.onRegisterServiceSucceeded(
+                    MdnsInterfaceAdvertiser.this, info.getServiceId()));
+            try {
+                announcementInfo = mRecordRepository.onProbingSucceeded(info);
+            } catch (IOException e) {
+                Log.e(mTag, "Error building announcements", e);
+                return;
+            }
+
+            mAnnouncer.startSending(info.getServiceId(), announcementInfo,
+                    0L /* initialDelayMs */);
+        }
+    }
+
+    /**
+     * Callbacks from {@link MdnsAnnouncer}.
+     */
+    private class AnnouncingCallback
+            implements PacketRepeaterCallback<MdnsAnnouncer.AnnouncementInfo> {
+        // TODO: implement
+    }
+
+    /**
+     * Dependencies for {@link MdnsInterfaceAdvertiser}, useful for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** @see MdnsRecordRepository */
+        @NonNull
+        public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper) {
+            return new MdnsRecordRepository(looper);
+        }
+
+        /** @see MdnsReplySender */
+        @NonNull
+        public MdnsReplySender makeReplySender(@NonNull Looper looper,
+                @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer) {
+            return new MdnsReplySender(looper, socket, packetCreationBuffer);
+        }
+
+        /** @see MdnsAnnouncer */
+        public MdnsAnnouncer makeMdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
+                @NonNull MdnsReplySender replySender,
+                @Nullable PacketRepeaterCallback<MdnsAnnouncer.AnnouncementInfo> cb) {
+            return new MdnsAnnouncer(interfaceTag, looper, replySender, cb);
+        }
+
+        /** @see MdnsProber */
+        public MdnsProber makeMdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
+                @NonNull MdnsReplySender replySender,
+                @NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb) {
+            return new MdnsProber(interfaceTag, looper, replySender, cb);
+        }
+    }
+
+    public MdnsInterfaceAdvertiser(@NonNull String logTag,
+            @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses,
+            @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb) {
+        this(logTag, socket, initialAddresses, looper, packetCreationBuffer, cb,
+                new Dependencies());
+    }
+
+    public MdnsInterfaceAdvertiser(@NonNull String logTag,
+            @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses,
+            @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
+            @NonNull Dependencies deps) {
+        mTag = MdnsInterfaceAdvertiser.class.getSimpleName() + "/" + logTag;
+        mRecordRepository = deps.makeRecordRepository(looper);
+        mRecordRepository.updateAddresses(initialAddresses);
+        mSocket = socket;
+        mCb = cb;
+        mCbHandler = new Handler(looper);
+        mReplySender = deps.makeReplySender(looper, socket, packetCreationBuffer);
+        mAnnouncer = deps.makeMdnsAnnouncer(logTag, looper, mReplySender,
+                mAnnouncingCallback);
+        mProber = deps.makeMdnsProber(logTag, looper, mReplySender, mProbingCallback);
+    }
+
+    /**
+     * Start the advertiser.
+     *
+     * The advertiser will stop itself when all services are removed and exit announcements sent,
+     * notifying via {@link Callback#onDestroyed}. This can also be triggered manually via
+     * {@link #destroyNow()}.
+     */
+    public void start() {
+        // TODO: start receiving packets
+    }
+
+    /**
+     * Start advertising a service.
+     *
+     * @throws NameConflictException There is already a service being advertised with that name.
+     */
+    public void addService(int id, NsdServiceInfo service) throws NameConflictException {
+        final int replacedExitingService = mRecordRepository.addService(id, service);
+        // Cancel announcements for the existing service. This only happens for exiting services
+        // (so cancelling exiting announcements), as per RecordRepository.addService.
+        if (replacedExitingService >= 0) {
+            if (DBG) {
+                Log.d(mTag, "Service " + replacedExitingService
+                        + " getting re-added, cancelling exit announcements");
+            }
+            mAnnouncer.stop(replacedExitingService);
+        }
+        mProber.startProbing(mRecordRepository.setServiceProbing(id));
+    }
+
+    /**
+     * Stop advertising a service.
+     *
+     * This will trigger exit announcements for the service.
+     */
+    public void removeService(int id) {
+        mProber.stop(id);
+        mAnnouncer.stop(id);
+        final MdnsAnnouncer.AnnouncementInfo exitInfo = mRecordRepository.exitService(id);
+        if (exitInfo != null) {
+            // This effectively schedules destroyNow(), as it is to be called when the exit
+            // announcement finishes if there is no service left.
+            // A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is
+            // also useful to ensure that when a host receives the exit announcement, the service
+            // has been unregistered on all interfaces; so an announcement sent from interface A
+            // that was already in-flight while unregistering won't be received after the exit on
+            // interface B.
+            mAnnouncer.startSending(id, exitInfo, EXIT_ANNOUNCEMENT_DELAY_MS);
+        } else {
+            // No exit announcement necessary: remove the service immediately.
+            mRecordRepository.removeService(id);
+            if (mRecordRepository.getServicesCount() == 0) {
+                destroyNow();
+            }
+        }
+    }
+
+    /**
+     * Update interface addresses used to advertise.
+     *
+     * This causes new address records to be announced.
+     */
+    public void updateAddresses(@NonNull List<LinkAddress> newAddresses) {
+        mRecordRepository.updateAddresses(newAddresses);
+        // TODO: restart advertising, but figure out what exit messages need to be sent for the
+        // previous addresses
+    }
+
+    /**
+     * Destroy the advertiser immediately, not sending any exit announcement.
+     *
+     * <p>Useful when the underlying network went away. This will trigger an onDestroyed callback.
+     */
+    public void destroyNow() {
+        for (int serviceId : mRecordRepository.clearServices()) {
+            mProber.stop(serviceId);
+            mAnnouncer.stop(serviceId);
+        }
+
+        // TODO: stop receiving packets
+        mCbHandler.post(() -> mCb.onDestroyed(mSocket));
+    }
+
+    /**
+     * Reset a service to the probing state due to a conflict found on the network.
+     */
+    public void restartProbingForConflict(int serviceId) {
+        // TODO: implement
+    }
+
+    /**
+     * Rename a service following a conflict found on the network, and restart probing.
+     */
+    public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
+        // TODO: implement
+    }
+
+    /**
+     * Indicates whether probing is in progress for the given service on this interface.
+     *
+     * Also returns false if the specified service is not registered.
+     */
+    public boolean isProbing(int serviceId) {
+        return mRecordRepository.isProbing(serviceId);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsInterfaceSocket.java b/service-t/src/com/android/server/mdns/MdnsInterfaceSocket.java
new file mode 100644
index 0000000..d1290b6
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsInterfaceSocket.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV4_ADDRESS;
+import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV6_ADDRESS;
+
+import android.annotation.NonNull;
+import android.net.LinkAddress;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.util.List;
+
+/**
+ * {@link MdnsInterfaceSocket} provides a similar interface to {@link MulticastSocket} and binds to
+ * an available multicast network interfaces.
+ *
+ * <p>This isn't thread safe and should be always called on the same thread unless specified
+ * otherwise.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ * @see MulticastSocket for javadoc of each public method.
+ */
+public class MdnsInterfaceSocket {
+    private static final String TAG = MdnsInterfaceSocket.class.getSimpleName();
+    @NonNull private final MulticastSocket mMulticastSocket;
+    @NonNull private final NetworkInterface mNetworkInterface;
+    @NonNull private final MulticastPacketReader mPacketReader;
+    @NonNull private final ParcelFileDescriptor mFileDescriptor;
+    private boolean mJoinedIpv4 = false;
+    private boolean mJoinedIpv6 = false;
+
+    public MdnsInterfaceSocket(@NonNull NetworkInterface networkInterface, int port,
+            @NonNull Looper looper, @NonNull byte[] packetReadBuffer)
+            throws IOException {
+        mNetworkInterface = networkInterface;
+        mMulticastSocket = new MulticastSocket(port);
+        // RFC Spec: https://tools.ietf.org/html/rfc6762. Time to live is set 255
+        mMulticastSocket.setTimeToLive(255);
+        mMulticastSocket.setNetworkInterface(networkInterface);
+
+        // Bind socket to the interface for receiving from that interface only.
+        mFileDescriptor = ParcelFileDescriptor.fromDatagramSocket(mMulticastSocket);
+        try {
+            final FileDescriptor fd = mFileDescriptor.getFileDescriptor();
+            final int flags = Os.fcntlInt(fd, OsConstants.F_GETFL, 0);
+            Os.fcntlInt(fd, OsConstants.F_SETFL, flags | OsConstants.SOCK_NONBLOCK);
+            SocketUtils.bindSocketToInterface(fd, mNetworkInterface.getName());
+        } catch (ErrnoException e) {
+            throw new IOException("Error setting socket options", e);
+        }
+
+        mPacketReader = new MulticastPacketReader(networkInterface.getName(), mFileDescriptor,
+                new Handler(looper), packetReadBuffer);
+        mPacketReader.start();
+    }
+
+    /**
+     * Sends a datagram packet from this socket.
+     *
+     * <p>This method could be used on any thread.
+     */
+    public void send(@NonNull DatagramPacket packet) throws IOException {
+        mMulticastSocket.send(packet);
+    }
+
+    private static boolean hasIpv4Address(@NonNull List<LinkAddress> addresses) {
+        for (LinkAddress address : addresses) {
+            if (address.isIpv4()) return true;
+        }
+        return false;
+    }
+
+    private static boolean hasIpv6Address(@NonNull List<LinkAddress> addresses) {
+        for (LinkAddress address : addresses) {
+            if (address.isIpv6()) return true;
+        }
+        return false;
+    }
+
+    /*** Joins both IPv4 and IPv6 multicast groups. */
+    public void joinGroup(@NonNull List<LinkAddress> addresses) {
+        maybeJoinIpv4(addresses);
+        maybeJoinIpv6(addresses);
+    }
+
+    private boolean joinGroup(@NonNull InetSocketAddress multicastAddress) {
+        try {
+            mMulticastSocket.joinGroup(multicastAddress, mNetworkInterface);
+            return true;
+        } catch (IOException e) {
+            // The address may have just been removed
+            Log.e(TAG, "Error joining multicast group for " + mNetworkInterface, e);
+            return false;
+        }
+    }
+
+    private void maybeJoinIpv4(@NonNull List<LinkAddress> addresses) {
+        final boolean hasAddr = hasIpv4Address(addresses);
+        if (!mJoinedIpv4 && hasAddr) {
+            mJoinedIpv4 = joinGroup(MULTICAST_IPV4_ADDRESS);
+        } else if (!hasAddr) {
+            // Lost IPv4 address
+            mJoinedIpv4 = false;
+        }
+    }
+
+    private void maybeJoinIpv6(@NonNull List<LinkAddress> addresses) {
+        final boolean hasAddr = hasIpv6Address(addresses);
+        if (!mJoinedIpv6 && hasAddr) {
+            mJoinedIpv6 = joinGroup(MULTICAST_IPV6_ADDRESS);
+        } else if (!hasAddr) {
+            // Lost IPv6 address
+            mJoinedIpv6 = false;
+        }
+    }
+
+    /*** Destroy the socket */
+    public void destroy() {
+        mPacketReader.stop();
+        try {
+            mFileDescriptor.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Close file descriptor failed.");
+        }
+        mMulticastSocket.close();
+    }
+
+    /**
+     * Add a handler to receive callbacks when reads the packet from socket. If the handler is
+     * already set, this is a no-op.
+     */
+    public void addPacketHandler(@NonNull MulticastPacketReader.PacketHandler handler) {
+        mPacketReader.addPacketHandler(handler);
+    }
+
+    /**
+     * Returns the network interface that this socket is bound to.
+     *
+     * <p>This method could be used on any thread.
+     */
+    public NetworkInterface getInterface() {
+        return mNetworkInterface;
+    }
+
+    /*** Returns whether this socket has joined IPv4 group */
+    public boolean hasJoinedIpv4() {
+        return mJoinedIpv4;
+    }
+
+    /*** Returns whether this socket has joined IPv6 group */
+    public boolean hasJoinedIpv6() {
+        return mJoinedIpv6;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/mdns/MdnsMultinetworkSocketClient.java
new file mode 100644
index 0000000..d959065
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsMultinetworkSocketClient.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsSocketProvider.ensureRunningOnHandlerThread;
+import static com.android.server.connectivity.mdns.MdnsSocketProvider.isNetworkMatched;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
+ *
+ *  * <p>This class is not thread safe.
+ */
+public class MdnsMultinetworkSocketClient implements MdnsSocketClientBase {
+    private static final String TAG = MdnsMultinetworkSocketClient.class.getSimpleName();
+    private static final boolean DBG = MdnsDiscoveryManager.DBG;
+
+    @NonNull private final Handler mHandler;
+    @NonNull private final MdnsSocketProvider mSocketProvider;
+    @NonNull private final MdnsResponseDecoder mResponseDecoder;
+
+    private final Map<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
+            new ArrayMap<>();
+    private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets = new ArrayMap<>();
+    private final ArrayMap<MdnsInterfaceSocket, ReadPacketHandler> mSocketPacketHandlers =
+            new ArrayMap<>();
+    private MdnsSocketClientBase.Callback mCallback = null;
+    private int mReceivedPacketNumber = 0;
+
+    public MdnsMultinetworkSocketClient(@NonNull Looper looper,
+            @NonNull MdnsSocketProvider provider) {
+        mHandler = new Handler(looper);
+        mSocketProvider = provider;
+        mResponseDecoder = new MdnsResponseDecoder(
+                new MdnsResponseDecoder.Clock(), null /* serviceType */);
+    }
+
+    private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
+        @Override
+        public void onSocketCreated(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
+            // The socket may be already created by other request before, try to get the stored
+            // ReadPacketHandler.
+            ReadPacketHandler handler = mSocketPacketHandlers.get(socket);
+            if (handler == null) {
+                // First request to create this socket. Initial a ReadPacketHandler for this socket.
+                handler = new ReadPacketHandler(network, socket.getInterface().getIndex());
+                mSocketPacketHandlers.put(socket, handler);
+            }
+            socket.addPacketHandler(handler);
+            mActiveNetworkSockets.put(socket, network);
+        }
+
+        @Override
+        public void onInterfaceDestroyed(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket) {
+            mSocketPacketHandlers.remove(socket);
+            mActiveNetworkSockets.remove(socket);
+        }
+    }
+
+    private class ReadPacketHandler implements MulticastPacketReader.PacketHandler {
+        private final Network mNetwork;
+        private final int mInterfaceIndex;
+
+        ReadPacketHandler(@NonNull Network network, int interfaceIndex) {
+            mNetwork = network;
+            mInterfaceIndex = interfaceIndex;
+        }
+
+        @Override
+        public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) {
+            processResponsePacket(recvbuf, length, mInterfaceIndex, mNetwork);
+        }
+    }
+
+    /*** Set callback for receiving mDns response */
+    @Override
+    public void setCallback(@Nullable MdnsSocketClientBase.Callback callback) {
+        ensureRunningOnHandlerThread(mHandler);
+        mCallback = callback;
+    }
+
+    /***
+     * Notify that the given network is requested for mdns discovery / resolution
+     *
+     * @param listener the listener for discovery.
+     * @param network the target network for discovery. Null means discovery on all possible
+     *                interfaces.
+     */
+    @Override
+    public void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
+            @Nullable Network network) {
+        ensureRunningOnHandlerThread(mHandler);
+        InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
+        if (callback != null) {
+            throw new IllegalArgumentException("Can not register duplicated listener");
+        }
+
+        if (DBG) Log.d(TAG, "notifyNetworkRequested: network=" + network);
+        callback = new InterfaceSocketCallback();
+        mRequestedNetworks.put(listener, callback);
+        mSocketProvider.requestSocket(network, callback);
+    }
+
+    /*** Notify that the network is unrequested */
+    @Override
+    public void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) {
+        ensureRunningOnHandlerThread(mHandler);
+        final InterfaceSocketCallback callback = mRequestedNetworks.remove(listener);
+        if (callback == null) {
+            Log.e(TAG, "Can not be unrequested with unknown listener=" + listener);
+            return;
+        }
+        mSocketProvider.unrequestSocket(callback);
+    }
+
+    private void sendMdnsPacket(@NonNull DatagramPacket packet, @Nullable Network targetNetwork) {
+        final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
+                instanceof Inet6Address;
+        final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
+                instanceof Inet4Address;
+        for (int i = 0; i < mActiveNetworkSockets.size(); i++) {
+            final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
+            final Network network = mActiveNetworkSockets.valueAt(i);
+            // Check ip capability and network before sending packet
+            if (((isIpv6 && socket.hasJoinedIpv6()) || (isIpv4 && socket.hasJoinedIpv4()))
+                    && isNetworkMatched(targetNetwork, network)) {
+                try {
+                    socket.send(packet);
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to send a mDNS packet.", e);
+                }
+            }
+        }
+    }
+
+    private void processResponsePacket(byte[] recvbuf, int length, int interfaceIndex,
+            @NonNull Network network) {
+        int packetNumber = ++mReceivedPacketNumber;
+
+        final List<MdnsResponse> responses = new ArrayList<>();
+        final int errorCode = mResponseDecoder.decode(
+                recvbuf, length, responses, interfaceIndex, network);
+        if (errorCode == MdnsResponseDecoder.SUCCESS) {
+            for (MdnsResponse response : responses) {
+                if (mCallback != null) {
+                    mCallback.onResponseReceived(response);
+                }
+            }
+        } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+            if (mCallback != null) {
+                mCallback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+            }
+        }
+    }
+
+    /** Sends a mDNS request packet that asks for multicast response. */
+    @Override
+    public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+        sendMulticastPacket(packet, null /* network */);
+    }
+
+    /**
+     * Sends a mDNS request packet via given network that asks for multicast response. Null network
+     * means sending packet via all networks.
+     */
+    @Override
+    public void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        mHandler.post(() -> sendMdnsPacket(packet, network));
+    }
+
+    /** Sends a mDNS request packet that asks for unicast response. */
+    @Override
+    public void sendUnicastPacket(@NonNull DatagramPacket packet) {
+        sendUnicastPacket(packet, null /* network */);
+    }
+
+    /**
+     * Sends a mDNS request packet via given network that asks for unicast response. Null network
+     * means sending packet via all networks.
+     */
+    @Override
+    public void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        // TODO: Separate unicast packet.
+        mHandler.post(() -> sendMdnsPacket(packet, network));
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/mdns/MdnsNsecRecord.java
new file mode 100644
index 0000000..06fdd5e
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsNsecRecord.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.net.DnsResolver;
+
+import com.android.net.module.util.CollectionUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * A mDNS "NSEC" record, used in particular for negative responses (RFC6762 6.1).
+ */
+public class MdnsNsecRecord extends MdnsRecord {
+    private String[] mNextDomain;
+    private int[] mTypes;
+
+    public MdnsNsecRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        this(name, reader, false);
+    }
+
+    public MdnsNsecRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)
+            throws IOException {
+        super(name, TYPE_NSEC, reader, isQuestion);
+    }
+
+    public MdnsNsecRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis,
+            String[] nextDomain, int[] types) {
+        super(name, TYPE_NSEC, DnsResolver.CLASS_IN, receiptTimeMillis, cacheFlush, ttlMillis);
+        mNextDomain = nextDomain;
+        final int[] sortedTypes = Arrays.copyOf(types, types.length);
+        Arrays.sort(sortedTypes);
+        mTypes = sortedTypes;
+    }
+
+    public String[] getNextDomain() {
+        return mNextDomain;
+    }
+
+    public int[] getTypes() {
+        return mTypes;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        mNextDomain = reader.readLabels();
+        mTypes = readTypes(reader);
+    }
+
+    private int[] readTypes(MdnsPacketReader reader) throws IOException {
+        // See RFC3845 #2.1.2
+        final ArrayList<Integer> types = new ArrayList<>();
+        int prevBlockNumber = -1;
+        while (reader.getRemaining() > 0) {
+            final int blockNumber = reader.readUInt8();
+            if (blockNumber <= prevBlockNumber) {
+                throw new IOException(
+                        "Unordered block number: " + blockNumber + " after " + prevBlockNumber);
+            }
+            prevBlockNumber = blockNumber;
+            final int bitmapLength = reader.readUInt8();
+            if (bitmapLength > 32 || bitmapLength <= 0) {
+                throw new IOException("Invalid bitmap length: " + bitmapLength);
+            }
+            final byte[] bitmap = new byte[bitmapLength];
+            reader.readBytes(bitmap);
+
+            for (int bitmapIndex = 0; bitmapIndex < bitmap.length; bitmapIndex++) {
+                final byte bitmapByte = bitmap[bitmapIndex];
+                for (int bit = 0; bit < 8; bit++) {
+                    if ((bitmapByte & (1 << (7 - bit))) != 0) {
+                        types.add(blockNumber * 256 + bitmapIndex * 8 + bit);
+                    }
+                }
+            }
+        }
+
+        return CollectionUtils.toIntArray(types);
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        // Standard NSEC records should use no compression for the Next Domain Name field as per
+        // RFC3845 2.1.1, but for mDNS RFC6762 18.14 specifies that compression should be used.
+        writer.writeLabels(mNextDomain);
+
+        // type bitmaps: RFC3845 2.1.2
+        int typesBlockStart = 0;
+        int pendingBlockNumber = -1;
+        int blockLength = 0;
+        // Loop on types (which are sorted in increasing order) to find each block and determine
+        // their length; use writeTypeBlock once the length of each block has been found.
+        for (int i = 0; i < mTypes.length; i++) {
+            final int blockNumber = mTypes[i] / 256;
+            final int typeLowOrder = mTypes[i] % 256;
+            // If the low-order 8 bits are e.g. 0x10, bit number 16 (=0x10) will be set in the
+            // bitmap; this is the first bit of byte 2 (byte 0 is 0-7, 1 is 8-15, etc.)
+            final int byteIndex = typeLowOrder / 8;
+
+            if (pendingBlockNumber >= 0 && blockNumber != pendingBlockNumber) {
+                // Just reached a new block; write the previous one
+                writeTypeBlock(writer, typesBlockStart, i - 1, blockLength);
+                typesBlockStart = i;
+                blockLength = 0;
+            }
+            blockLength = Math.max(blockLength, byteIndex + 1);
+            pendingBlockNumber = blockNumber;
+        }
+
+        if (pendingBlockNumber >= 0) {
+            writeTypeBlock(writer, typesBlockStart, mTypes.length - 1, blockLength);
+        }
+    }
+
+    private void writeTypeBlock(MdnsPacketWriter writer,
+            int typesStart, int typesEnd, int bytesInBlock) throws IOException {
+        final int blockNumber = mTypes[typesStart] / 256;
+        final byte[] bytes = new byte[bytesInBlock];
+        for (int i = typesStart; i <= typesEnd; i++) {
+            final int typeLowOrder = mTypes[i] % 256;
+            bytes[typeLowOrder / 8] |= 1 << (7 - (typeLowOrder % 8));
+        }
+        writer.writeUInt8(blockNumber);
+        writer.writeUInt8(bytesInBlock);
+        writer.writeBytes(bytes);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsPacket.java b/service-t/src/com/android/server/mdns/MdnsPacket.java
new file mode 100644
index 0000000..eae084a
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsPacket.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class holding data that can be included in a mDNS packet.
+ */
+public class MdnsPacket {
+    public final int flags;
+    public final List<MdnsRecord> questions;
+    public final List<MdnsRecord> answers;
+    public final List<MdnsRecord> authorityRecords;
+    public final List<MdnsRecord> additionalRecords;
+
+    MdnsPacket(int flags,
+            List<MdnsRecord> questions,
+            List<MdnsRecord> answers,
+            List<MdnsRecord> authorityRecords,
+            List<MdnsRecord> additionalRecords) {
+        this.flags = flags;
+        this.questions = Collections.unmodifiableList(questions);
+        this.answers = Collections.unmodifiableList(answers);
+        this.authorityRecords = Collections.unmodifiableList(authorityRecords);
+        this.additionalRecords = Collections.unmodifiableList(additionalRecords);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsPacketReader.java b/service-t/src/com/android/server/mdns/MdnsPacketReader.java
new file mode 100644
index 0000000..aa38844
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsPacketReader.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Simple decoder for mDNS packets. */
+public class MdnsPacketReader {
+    private final byte[] buf;
+    private final int count;
+    private final SparseArray<LabelEntry> labelDictionary;
+    private int pos;
+    private int limit;
+
+    /** Constructs a reader for the given packet. */
+    public MdnsPacketReader(DatagramPacket packet) {
+        this(packet.getData(), packet.getLength());
+    }
+
+    /** Constructs a reader for the given packet. */
+    public MdnsPacketReader(byte[] buffer, int length) {
+        buf = buffer;
+        count = length;
+        pos = 0;
+        limit = -1;
+        labelDictionary = new SparseArray<>(16);
+    }
+
+    /**
+     * Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
+     * read past this limit will result in an EOFException.
+     *
+     * @param limit The new limit.
+     * @throws IOException If there is insufficient data for the new limit.
+     */
+    public void setLimit(int limit) throws IOException {
+        if (limit >= 0) {
+            if (pos + limit <= count) {
+                this.limit = pos + limit;
+            } else {
+                throw new IOException(
+                        String.format(
+                                Locale.ROOT,
+                                "attempt to set limit beyond available data: %d exceeds %d",
+                                pos + limit,
+                                count));
+            }
+        }
+    }
+
+    /** Clears the limit set by {@link #setLimit}. */
+    public void clearLimit() {
+        limit = -1;
+    }
+
+    /**
+     * Returns the number of bytes left to read, between the current read position and either the
+     * limit (if set) or the end of the packet.
+     */
+    public int getRemaining() {
+        return (limit >= 0 ? limit : count) - pos;
+    }
+
+    /**
+     * Reads an unsigned 8-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public int readUInt8() throws EOFException {
+        checkRemaining(1);
+        byte val = buf[pos++];
+        return val & 0xFF;
+    }
+
+    /**
+     * Reads an unsigned 16-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public int readUInt16() throws EOFException {
+        checkRemaining(2);
+        int val = (buf[pos++] & 0xFF) << 8;
+        val |= (buf[pos++]) & 0xFF;
+        return val;
+    }
+
+    /**
+     * Reads an unsigned 32-bit integer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public long readUInt32() throws EOFException {
+        checkRemaining(4);
+        long val = (long) (buf[pos++] & 0xFF) << 24;
+        val |= (long) (buf[pos++] & 0xFF) << 16;
+        val |= (long) (buf[pos++] & 0xFF) << 8;
+        val |= buf[pos++] & 0xFF;
+        return val;
+    }
+
+    /**
+     * Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
+     * either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
+     * pointer, or a pointer.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     * @throws IOException  If invalid data is read.
+     */
+    public String[] readLabels() throws IOException {
+        List<String> result = new ArrayList<>(5);
+        LabelEntry previousEntry = null;
+
+        while (getRemaining() > 0) {
+            byte nextByte = peekByte();
+
+            if (nextByte == 0) {
+                // A NUL byte terminates a sequence of labels.
+                skip(1);
+                break;
+            }
+
+            int currentOffset = pos;
+
+            boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
+            if (isLabelPointer) {
+                // A pointer terminates a sequence of labels. Store the pointer value in the
+                // previous label entry.
+                int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
+                if (previousEntry != null) {
+                    previousEntry.nextOffset = labelOffset;
+                }
+
+                // Follow the chain of labels starting at this pointer, adding all of them onto the
+                // result.
+                while (labelOffset != 0) {
+                    LabelEntry entry = labelDictionary.get(labelOffset);
+                    if (entry == null) {
+                        throw new IOException(
+                                String.format(Locale.ROOT, "Invalid label pointer: %04X",
+                                        labelOffset));
+                    }
+                    result.add(entry.label);
+                    labelOffset = entry.nextOffset;
+                }
+                break;
+            } else {
+                // It's an ordinary label. Chain it onto the previous label entry (if any), and add
+                // it onto the result.
+                String val = readString();
+                LabelEntry newEntry = new LabelEntry(val);
+                labelDictionary.put(currentOffset, newEntry);
+
+                if (previousEntry != null) {
+                    previousEntry.nextOffset = currentOffset;
+                }
+                previousEntry = newEntry;
+                result.add(val);
+            }
+        }
+
+        return result.toArray(new String[result.size()]);
+    }
+
+    /**
+     * Reads a length-prefixed string.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public String readString() throws EOFException {
+        int len = readUInt8();
+        checkRemaining(len);
+        String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
+        pos += len;
+        return val;
+    }
+
+    @Nullable
+    public TextEntry readTextEntry() throws EOFException {
+        int len = readUInt8();
+        checkRemaining(len);
+        byte[] bytes = new byte[len];
+        System.arraycopy(buf, pos, bytes, 0, bytes.length);
+        pos += len;
+        return TextEntry.fromBytes(bytes);
+    }
+
+    /**
+     * Reads a specific number of bytes.
+     *
+     * @param bytes The array to fill.
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public void readBytes(byte[] bytes) throws EOFException {
+        checkRemaining(bytes.length);
+        System.arraycopy(buf, pos, bytes, 0, bytes.length);
+        pos += bytes.length;
+    }
+
+    /**
+     * Skips over the given number of bytes.
+     *
+     * @param count The number of bytes to read and discard.
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public void skip(int count) throws EOFException {
+        checkRemaining(count);
+        pos += count;
+    }
+
+    /**
+     * Peeks at and returns the next byte in the packet, without advancing the read position.
+     *
+     * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+     *                      read.
+     */
+    public byte peekByte() throws EOFException {
+        checkRemaining(1);
+        return buf[pos];
+    }
+
+    /** Returns the current byte position of the reader for the data packet. */
+    public int getPosition() {
+        return pos;
+    }
+
+    // Checks if the number of remaining bytes to be read in the packet is at least |count|.
+    private void checkRemaining(int count) throws EOFException {
+        if (getRemaining() < count) {
+            throw new EOFException();
+        }
+    }
+
+    private static class LabelEntry {
+        public final String label;
+        public int nextOffset = 0;
+
+        public LabelEntry(String label) {
+            this.label = label;
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/mdns/MdnsPacketRepeater.java
new file mode 100644
index 0000000..ae54e70
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsPacketRepeater.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+/**
+ * A class used to send several packets at given time intervals.
+ * @param <T> The type of the request providing packet repeating parameters.
+ */
+public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
+    private static final boolean DBG = MdnsAdvertiser.DBG;
+    private static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
+            MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    private static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
+            MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+    private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
+            IPV4_ADDR, IPV6_ADDR
+    };
+
+    @NonNull
+    private final MdnsReplySender mReplySender;
+    @NonNull
+    protected final Handler mHandler;
+    @Nullable
+    private final PacketRepeaterCallback<T> mCb;
+
+    /**
+     * Status callback from {@link MdnsPacketRepeater}.
+     *
+     * Callbacks are called on the {@link MdnsPacketRepeater} handler thread.
+     * @param <T> The type of the request providing packet repeating parameters.
+     */
+    public interface PacketRepeaterCallback<T extends MdnsPacketRepeater.Request> {
+        /**
+         * Called when a packet was sent.
+         */
+        default void onSent(int index, @NonNull T info) {}
+
+        /**
+         * Called when the {@link MdnsPacketRepeater} is done sending packets.
+         */
+        default void onFinished(@NonNull T info) {}
+    }
+
+    /**
+     * A request to repeat packets.
+     *
+     * All methods are called in the looper thread.
+     */
+    public interface Request {
+        /**
+         * Get a packet to send for one iteration.
+         */
+        @NonNull
+        MdnsPacket getPacket(int index);
+
+        /**
+         * Get the delay in milliseconds until the next packet transmission.
+         */
+        long getDelayMs(int nextIndex);
+
+        /**
+         * Get the number of packets that should be sent.
+         */
+        int getNumSends();
+    }
+
+    /**
+     * Get the logging tag to use.
+     */
+    @NonNull
+    protected abstract String getTag();
+
+    private final class ProbeHandler extends Handler {
+        ProbeHandler(@NonNull Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            final int index = msg.arg1;
+            final T request = (T) msg.obj;
+
+            if (index >= request.getNumSends()) {
+                if (mCb != null) {
+                    mCb.onFinished(request);
+                }
+                return;
+            }
+
+            final MdnsPacket packet = request.getPacket(index);
+            if (DBG) {
+                Log.v(getTag(), "Sending packets for iteration " + index + " out of "
+                        + request.getNumSends());
+            }
+            // Send to both v4 and v6 addresses; the reply sender will take care of ignoring the
+            // send when the socket has not joined the relevant group.
+            for (InetSocketAddress destination : ALL_ADDRS) {
+                try {
+                    mReplySender.sendNow(packet, destination);
+                } catch (IOException e) {
+                    Log.e(getTag(), "Error sending packet to " + destination, e);
+                }
+            }
+
+            int nextIndex = index + 1;
+            // No need to go through the last handler loop if there's no callback to call
+            if (nextIndex < request.getNumSends() || mCb != null) {
+                // TODO: consider using AlarmManager / WakeupMessage to avoid missing sending during
+                // deep sleep; but this would affect battery life, and discovered services are
+                // likely not to be available since the device is in deep sleep anyway.
+                final long delay = request.getDelayMs(nextIndex);
+                sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
+                if (DBG) Log.v(getTag(), "Scheduled next packet in " + delay + "ms");
+            }
+
+            // Call onSent after scheduling the next run, to allow the callback to cancel it
+            if (mCb != null) {
+                mCb.onSent(index, request);
+            }
+        }
+    }
+
+    protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
+            @Nullable PacketRepeaterCallback<T> cb) {
+        mHandler = new ProbeHandler(looper);
+        mReplySender = replySender;
+        mCb = cb;
+    }
+
+    protected void startSending(int id, @NonNull T request, long initialDelayMs) {
+        if (DBG) {
+            Log.v(getTag(), "Starting send with id " + id + ", request "
+                    + request.getClass().getSimpleName() + ", delay " + initialDelayMs);
+        }
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(id, 0, 0, request), initialDelayMs);
+    }
+
+    /**
+     * Stop sending the packets for the specified ID
+     * @return true if probing was in progress, false if this was a no-op
+     */
+    public boolean stop(int id) {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException("stop can only be called from the looper thread");
+        }
+        // Since this is run on the looper thread, messages cannot be currently processing and are
+        // all in the handler queue; unless this method is called from a message, but the current
+        // message cannot be cancelled.
+        if (mHandler.hasMessages(id)) {
+            if (DBG) {
+                Log.v(getTag(), "Stopping send on id " + id);
+            }
+            mHandler.removeMessages(id);
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/mdns/MdnsPacketWriter.java
new file mode 100644
index 0000000..c0f9b8b
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsPacketWriter.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Simple encoder for mDNS packets. */
+public class MdnsPacketWriter {
+    private static final int MDNS_POINTER_MASK = 0xC000;
+    private final byte[] data;
+    private final Map<Integer, String[]> labelDictionary = new HashMap<>();
+    private int pos = 0;
+    private int savedWritePos = -1;
+
+    /**
+     * Constructs a writer for a new packet.
+     *
+     * @param maxSize The maximum size of a packet.
+     */
+    public MdnsPacketWriter(int maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("invalid size");
+        }
+
+        data = new byte[maxSize];
+    }
+
+    /**
+     * Constructs a writer for a new packet.
+     *
+     * @param buffer The buffer to write to.
+     */
+    public MdnsPacketWriter(byte[] buffer) {
+        data = buffer;
+    }
+
+    /** Returns the current write position. */
+    public int getWritePosition() {
+        return pos;
+    }
+
+    /**
+     * Saves the current write position and then rewinds the write position by the given number of
+     * bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
+     * nested.
+     *
+     * @param position The position to rewind to.
+     * @throws IOException If the count would go beyond the beginning of the packet, or if there is
+     *                     already a rewind in effect.
+     */
+    public void rewind(int position) throws IOException {
+        if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
+            throw new IOException("invalid rewind");
+        }
+
+        savedWritePos = pos;
+        pos = position;
+    }
+
+    /**
+     * Sets the current write position to what it was prior to the last rewind.
+     *
+     * @throws IOException If there was no rewind in effect.
+     */
+    public void unrewind() throws IOException {
+        if (savedWritePos == -1) {
+            throw new IOException("no rewind is in effect");
+        }
+        pos = savedWritePos;
+        savedWritePos = -1;
+    }
+
+    /** Clears any rewind state. */
+    public void clearRewind() {
+        savedWritePos = -1;
+    }
+
+    /**
+     * Writes an unsigned 8-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt8(int value) throws IOException {
+        checkRemaining(1);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes an unsigned 16-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt16(int value) throws IOException {
+        checkRemaining(2);
+        data[pos++] = (byte) ((value >>> 8) & 0xFF);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes an unsigned 32-bit integer.
+     *
+     * @param value The value to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeUInt32(long value) throws IOException {
+        checkRemaining(4);
+        data[pos++] = (byte) ((value >>> 24) & 0xFF);
+        data[pos++] = (byte) ((value >>> 16) & 0xFF);
+        data[pos++] = (byte) ((value >>> 8) & 0xFF);
+        data[pos++] = (byte) (value & 0xFF);
+    }
+
+    /**
+     * Writes a specific number of bytes.
+     *
+     * @param data The array to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeBytes(byte[] data) throws IOException {
+        checkRemaining(data.length);
+        System.arraycopy(data, 0, this.data, pos, data.length);
+        pos += data.length;
+    }
+
+    /**
+     * Writes a string.
+     *
+     * @param value The string to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeString(String value) throws IOException {
+        byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
+        writeUInt8(utf8.length);
+        writeBytes(utf8);
+    }
+
+    public void writeTextEntry(TextEntry textEntry) throws IOException {
+        byte[] bytes = textEntry.toBytes();
+        writeUInt8(bytes.length);
+        writeBytes(bytes);
+    }
+
+    /**
+     * Writes a series of labels. Uses name compression.
+     *
+     * @param labels The labels to write.
+     * @throws IOException If there is not enough space remaining in the packet.
+     */
+    public void writeLabels(String[] labels) throws IOException {
+        // See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
+        // of the name compression method used here.
+
+        int suffixLength = 0;
+        int suffixPointer = 0;
+
+        for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
+            int existingOffset = entry.getKey();
+            String[] existingLabels = entry.getValue();
+
+            if (Arrays.equals(existingLabels, labels)) {
+                writePointer(existingOffset);
+                return;
+            } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
+                // Keep track of the longest matching suffix so far.
+                if (existingLabels.length > suffixLength) {
+                    suffixLength = existingLabels.length;
+                    suffixPointer = existingOffset;
+                }
+            }
+        }
+
+        final int[] offsets;
+        if (suffixLength > 0) {
+            offsets = writePartialLabelsNoCompression(labels, labels.length - suffixLength);
+            writePointer(suffixPointer);
+        } else {
+            offsets = writeLabelsNoCompression(labels);
+        }
+
+        // Add entries to the label dictionary for each suffix of the label list, including
+        // the whole list itself.
+        // Do not replace the last suffixLength suffixes that already have dictionary entries.
+        for (int i = 0, len = labels.length; i < labels.length - suffixLength; ++i, --len) {
+            String[] value = new String[len];
+            System.arraycopy(labels, i, value, 0, len);
+            labelDictionary.put(offsets[i], value);
+        }
+    }
+
+    private int[] writePartialLabelsNoCompression(String[] labels, int count) throws IOException {
+        int[] offsets = new int[count];
+        for (int i = 0; i < count; ++i) {
+            offsets[i] = getWritePosition();
+            writeString(labels[i]);
+        }
+        return offsets;
+    }
+
+    /**
+     * Write a series a labels, without using name compression.
+     *
+     * @return The offsets where each label was written to.
+     */
+    public int[] writeLabelsNoCompression(String[] labels) throws IOException {
+        final int[] offsets = writePartialLabelsNoCompression(labels, labels.length);
+        writeUInt8(0); // NUL terminator
+        return offsets;
+    }
+
+    /** Returns the number of bytes that can still be written. */
+    public int getRemaining() {
+        return data.length - pos;
+    }
+
+    // Writes a pointer to a label.
+    private void writePointer(int offset) throws IOException {
+        writeUInt16(MDNS_POINTER_MASK | offset);
+    }
+
+    // Checks if the remaining space in the packet is at least |count|.
+    private void checkRemaining(int count) throws IOException {
+        if (getRemaining() < count) {
+            throw new IOException();
+        }
+    }
+
+    /** Builds and returns the packet. */
+    public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
+        return new DatagramPacket(data, pos, destAddress);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/mdns/MdnsPointerRecord.java
new file mode 100644
index 0000000..2c7b26b
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsPointerRecord.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "PTR" record, which holds a name (the "pointer"). */
+@VisibleForTesting
+public class MdnsPointerRecord extends MdnsRecord {
+    private String[] pointer;
+
+    public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        this(name, reader, false);
+    }
+
+    public MdnsPointerRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)
+            throws IOException {
+        super(name, TYPE_PTR, reader, isQuestion);
+    }
+
+    public MdnsPointerRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
+                    long ttlMillis, String[] pointer) {
+        super(name, TYPE_PTR, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
+                ttlMillis);
+        this.pointer = pointer;
+    }
+
+    /** Returns the pointer as an array of labels. */
+    public String[] getPointer() {
+        return pointer;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        pointer = reader.readLabels();
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        writer.writeLabels(pointer);
+    }
+
+    public boolean hasSubtype() {
+        return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+    }
+
+    public String getSubtype() {
+        return hasSubtype() ? name[0] : null;
+    }
+
+    @Override
+    public String toString() {
+        return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsPointerRecord)) {
+            return false;
+        }
+
+        return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsProber.java b/service-t/src/com/android/server/mdns/MdnsProber.java
new file mode 100644
index 0000000..2cd9148
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsProber.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Sends mDns probe requests to verify service records are unique on the network.
+ *
+ * TODO: implement receiving replies and handling conflicts.
+ */
+public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
+    @NonNull
+    private final String mLogTag;
+
+    public MdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
+            @NonNull MdnsReplySender replySender,
+            @NonNull PacketRepeaterCallback<ProbingInfo> cb) {
+        // 3 packets as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
+        super(looper, replySender, cb);
+        mLogTag = MdnsProber.class.getSimpleName() + "/" + interfaceTag;
+    }
+
+    /** Probing request to send with {@link MdnsProber}. */
+    public static class ProbingInfo implements Request {
+
+        private final int mServiceId;
+        @NonNull
+        private final MdnsPacket mPacket;
+
+        /**
+         * Create a new ProbingInfo
+         * @param serviceId Service to probe for.
+         * @param probeRecords Records to be probed for uniqueness.
+         */
+        ProbingInfo(int serviceId, @NonNull List<MdnsRecord> probeRecords) {
+            mServiceId = serviceId;
+            mPacket = makePacket(probeRecords);
+        }
+
+        public int getServiceId() {
+            return mServiceId;
+        }
+
+        @NonNull
+        @Override
+        public MdnsPacket getPacket(int index) {
+            return mPacket;
+        }
+
+        @Override
+        public long getDelayMs(int nextIndex) {
+            // As per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
+            return 250L;
+        }
+
+        @Override
+        public int getNumSends() {
+            // 3 packets as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
+            return 3;
+        }
+
+        private static MdnsPacket makePacket(@NonNull List<MdnsRecord> records) {
+            final ArrayList<MdnsRecord> questions = new ArrayList<>(records.size());
+            for (final MdnsRecord record : records) {
+                if (containsName(questions, record.getName())) {
+                    // Already added this name
+                    continue;
+                }
+
+                // TODO: legacy Android mDNS used to send the first probe (only) as unicast, even
+                //  though https://datatracker.ietf.org/doc/html/rfc6762#section-8.1 says they
+                // SHOULD all be. rfc6762 15.1 says that if the port is shared with another
+                // responder unicast questions should not be used, and the legacy mdnsresponder may
+                // be running, so not using unicast at all may be better. Consider using legacy
+                // behavior if this causes problems.
+                questions.add(new MdnsAnyRecord(record.getName(), false /* unicast */));
+            }
+
+            return new MdnsPacket(
+                    MdnsConstants.FLAGS_QUERY,
+                    questions,
+                    Collections.emptyList() /* answers */,
+                    records /* authorityRecords */,
+                    Collections.emptyList() /* additionalRecords */);
+        }
+
+        /**
+         * Return whether the specified name is present in the list of records.
+         */
+        private static boolean containsName(@NonNull List<MdnsRecord> records,
+                @NonNull String[] name) {
+            return CollectionUtils.any(records, r -> Arrays.equals(name, r.getName()));
+        }
+    }
+
+    @NonNull
+    @Override
+    protected String getTag() {
+        return mLogTag;
+    }
+
+    @VisibleForTesting
+    protected long getInitialDelay() {
+        // First wait for a random time in 0-250ms
+        // as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
+        return (long) (Math.random() * 250);
+    }
+
+    /**
+     * Start sending packets for probing.
+     */
+    public void startProbing(@NonNull ProbingInfo info) {
+        startProbing(info, getInitialDelay());
+    }
+
+    private void startProbing(@NonNull ProbingInfo info, long delay) {
+        startSending(info.getServiceId(), info, delay);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsRecord.java b/service-t/src/com/android/server/mdns/MdnsRecord.java
new file mode 100644
index 0000000..00871ea
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsRecord.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Abstract base class for mDNS records. Stores the header fields and provides methods for reading
+ * the record from and writing it to a packet.
+ */
+public abstract class MdnsRecord {
+    public static final int TYPE_A = 0x0001;
+    public static final int TYPE_AAAA = 0x001C;
+    public static final int TYPE_PTR = 0x000C;
+    public static final int TYPE_SRV = 0x0021;
+    public static final int TYPE_TXT = 0x0010;
+    public static final int TYPE_NSEC = 0x002f;
+    public static final int TYPE_ANY = 0x00ff;
+
+    private static final int FLAG_CACHE_FLUSH = 0x8000;
+
+    public static final long RECEIPT_TIME_NOT_SENT = 0L;
+
+    /** Status indicating that the record is current. */
+    public static final int STATUS_OK = 0;
+    /** Status indicating that the record has expired (TTL reached 0). */
+    public static final int STATUS_EXPIRED = 1;
+    /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
+    public static final int STATUS_NEEDS_REFRESH = 2;
+
+    protected final String[] name;
+    private final int type;
+    private final int cls;
+    private final long receiptTimeMillis;
+    private final long ttlMillis;
+    private Object key;
+
+    /**
+     * Constructs a new record with the given name and type.
+     *
+     * @param reader The reader to read the record from.
+     * @param isQuestion Whether the record was included in the questions part of the message.
+     * @throws IOException If an error occurs while reading the packet.
+     */
+    protected MdnsRecord(String[] name, int type, MdnsPacketReader reader, boolean isQuestion)
+            throws IOException {
+        this.name = name;
+        this.type = type;
+        cls = reader.readUInt16();
+        receiptTimeMillis = SystemClock.elapsedRealtime();
+
+        if (isQuestion) {
+            // Questions do not have TTL or data
+            ttlMillis = 0L;
+        } else {
+            ttlMillis = SECONDS.toMillis(reader.readUInt32());
+            int dataLength = reader.readUInt16();
+
+            reader.setLimit(dataLength);
+            readData(reader);
+            reader.clearLimit();
+        }
+    }
+
+    /**
+     * Constructs a new record with the given name and type.
+     *
+     * @param reader The reader to read the record from.
+     * @throws IOException If an error occurs while reading the packet.
+     */
+    // call to readData(com.android.server.connectivity.mdns.MdnsPacketReader) not allowed on given
+    // receiver.
+    @SuppressWarnings("nullness:method.invocation.invalid")
+    protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
+        this(name, type, reader, false);
+    }
+
+    /**
+     * Constructs a new record with the given properties.
+     */
+    protected MdnsRecord(String[] name, int type, int cls, long receiptTimeMillis,
+            boolean cacheFlush, long ttlMillis) {
+        this.name = name;
+        this.type = type;
+        this.cls = cls | (cacheFlush ? FLAG_CACHE_FLUSH : 0);
+        this.receiptTimeMillis = receiptTimeMillis;
+        this.ttlMillis = ttlMillis;
+    }
+
+    /**
+     * Converts an array of labels into their dot-separated string representation. This method
+     * should
+     * be used for logging purposes only.
+     */
+    public static String labelsToString(String[] labels) {
+        if (labels == null) {
+            return null;
+        }
+        return TextUtils.join(".", labels);
+    }
+
+    /** Tests if |list1| is a suffix of |list2|. */
+    public static boolean labelsAreSuffix(String[] list1, String[] list2) {
+        int offset = list2.length - list1.length;
+
+        if (offset < 1) {
+            return false;
+        }
+
+        for (int i = 0; i < list1.length; ++i) {
+            if (!list1[i].equals(list2[i + offset])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /** Returns the record's receipt (creation) time. */
+    public final long getReceiptTime() {
+        return receiptTimeMillis;
+    }
+
+    /** Returns the record's name. */
+    public String[] getName() {
+        return name;
+    }
+
+    /** Returns the record's original TTL, in milliseconds. */
+    public final long getTtl() {
+        return ttlMillis;
+    }
+
+    /** Returns the record's type. */
+    public final int getType() {
+        return type;
+    }
+
+    /** Return the record's class. */
+    public final int getRecordClass() {
+        return cls & ~FLAG_CACHE_FLUSH;
+    }
+
+    /** Return whether the cache flush flag is set. */
+    public final boolean getCacheFlush() {
+        return (cls & FLAG_CACHE_FLUSH) != 0;
+    }
+
+    /**
+     * Returns the record's remaining TTL.
+     *
+     * If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the
+     * original TTL of the record.
+     * @param now The current system time.
+     * @return The remaning TTL, in milliseconds.
+     */
+    public long getRemainingTTL(final long now) {
+        if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) {
+            return ttlMillis;
+        }
+
+        long age = now - receiptTimeMillis;
+        if (age > ttlMillis) {
+            return 0;
+        }
+
+        return ttlMillis - age;
+    }
+
+    /**
+     * Reads the record's payload from a packet.
+     *
+     * @param reader The reader to use.
+     * @throws IOException If an I/O error occurs.
+     */
+    protected abstract void readData(MdnsPacketReader reader) throws IOException;
+
+    /**
+     * Write the first fields of the record, which are common fields for questions and answers.
+     *
+     * @param writer The writer to use.
+     */
+    public final void writeHeaderFields(MdnsPacketWriter writer) throws IOException {
+        writer.writeLabels(name);
+        writer.writeUInt16(type);
+        writer.writeUInt16(cls);
+    }
+
+    /**
+     * Writes the record to a packet.
+     *
+     * @param writer The writer to use.
+     * @param now    The current system time. This is used when writing the updated TTL.
+     */
+    @VisibleForTesting
+    public final void write(MdnsPacketWriter writer, long now) throws IOException {
+        writeHeaderFields(writer);
+
+        writer.writeUInt32(MILLISECONDS.toSeconds(getRemainingTTL(now)));
+
+        int dataLengthPos = writer.getWritePosition();
+        writer.writeUInt16(0); // data length
+        int dataPos = writer.getWritePosition();
+
+        writeData(writer);
+
+        // Calculate amount of data written, and overwrite the data field earlier in the packet.
+        int endPos = writer.getWritePosition();
+        int dataLength = endPos - dataPos;
+        writer.rewind(dataLengthPos);
+        writer.writeUInt16(dataLength);
+        writer.unrewind();
+    }
+
+    /**
+     * Writes the record's payload to a packet.
+     *
+     * @param writer The writer to use.
+     * @throws IOException If an I/O error occurs.
+     */
+    protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
+
+    /** Gets the status of the record. */
+    public int getStatus(final long now) {
+        if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) {
+            return STATUS_OK;
+        }
+        final long age = now - receiptTimeMillis;
+        if (age > ttlMillis) {
+            return STATUS_EXPIRED;
+        }
+        if (age > (ttlMillis / 2)) {
+            return STATUS_NEEDS_REFRESH;
+        }
+        return STATUS_OK;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof MdnsRecord)) {
+            return false;
+        }
+
+        MdnsRecord otherRecord = (MdnsRecord) other;
+
+        return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(name), type);
+    }
+
+    /**
+     * Returns an opaque object that uniquely identifies this record through a combination of its
+     * type
+     * and name. Suitable for use as a key in caches.
+     */
+    public final Object getKey() {
+        if (key == null) {
+            key = new Key(type, name);
+        }
+        return key;
+    }
+
+    private static final class Key {
+        private final int recordType;
+        private final String[] recordName;
+
+        public Key(int recordType, String[] recordName) {
+            this.recordType = recordType;
+            this.recordName = recordName;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof Key)) {
+                return false;
+            }
+
+            Key otherKey = (Key) other;
+
+            return (recordType == otherKey.recordType) && Arrays.equals(recordName,
+                    otherKey.recordName);
+        }
+
+        @Override
+        public int hashCode() {
+            return (recordType * 31) + Arrays.hashCode(recordName);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
new file mode 100644
index 0000000..bb9c751
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Looper;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A repository of records advertised through {@link MdnsInterfaceAdvertiser}.
+ *
+ * Must be used on a consistent looper thread.
+ */
+public class MdnsRecordRepository {
+    // TTLs as per RFC6762 10.
+    // TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
+    // host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
+    // record)
+    private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+    // TTL for other records
+    private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+
+    // Top-level domain for link-local queries, as per RFC6762 3.
+    private static final String LOCAL_TLD = "local";
+
+    // Service type for service enumeration (RFC6763 9.)
+    private static final String[] DNS_SD_SERVICE_TYPE =
+            new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
+
+    // Map of service unique ID -> records for service
+    @NonNull
+    private final SparseArray<ServiceRegistration> mServices = new SparseArray<>();
+    @NonNull
+    private final Looper mLooper;
+    @NonNull
+    private String[] mDeviceHostname;
+
+    public MdnsRecordRepository(@NonNull Looper looper) {
+        this(looper, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public MdnsRecordRepository(@NonNull Looper looper, @NonNull Dependencies deps) {
+        mDeviceHostname = deps.getHostname();
+        mLooper = looper;
+    }
+
+    /**
+     * Dependencies to use with {@link MdnsRecordRepository}, useful for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Get a unique hostname to be used by the device.
+         */
+        @NonNull
+        public String[] getHostname() {
+            // Generate a very-probably-unique hostname. This allows minimizing possible conflicts
+            // to the point that probing for it is no longer necessary (as per RFC6762 8.1 last
+            // paragraph), and does not leak more information than what could already be obtained by
+            // looking at the mDNS packets source address.
+            // This differs from historical behavior that just used "Android.local" for many
+            // devices, creating a lot of conflicts.
+            // Having a different hostname per interface is an acceptable option as per RFC6762 14.
+            // This hostname will change every time the interface is reconnected, so this does not
+            // allow tracking the device.
+            // TODO: consider deriving a hostname from other sources, such as the IPv6 addresses
+            // (reusing the same privacy-protecting mechanics).
+            return new String[] {
+                    "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD };
+        }
+
+        /**
+         * @see NetworkInterface#getInetAddresses().
+         */
+        @NonNull
+        public Enumeration<InetAddress> getInterfaceInetAddresses(@NonNull NetworkInterface iface) {
+            return iface.getInetAddresses();
+        }
+    }
+
+    private static class RecordInfo<T extends MdnsRecord> {
+        public final T record;
+        public final NsdServiceInfo serviceInfo;
+
+        /**
+         * Whether the name of this record is expected to be fully owned by the service or may be
+         * advertised by other hosts as well (shared).
+         */
+        public final boolean isSharedName;
+
+        /**
+         * Whether probing is still in progress for the record.
+         */
+        public boolean isProbing;
+
+        RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName,
+                 boolean probing) {
+            this.serviceInfo = serviceInfo;
+            this.record = record;
+            this.isSharedName = sharedName;
+            this.isProbing = probing;
+        }
+    }
+
+    private static class ServiceRegistration {
+        @NonNull
+        public final List<RecordInfo<?>> allRecords;
+        @NonNull
+        public final RecordInfo<MdnsPointerRecord> ptrRecord;
+        @NonNull
+        public final RecordInfo<MdnsServiceRecord> srvRecord;
+        @NonNull
+        public final RecordInfo<MdnsTextRecord> txtRecord;
+        @NonNull
+        public final NsdServiceInfo serviceInfo;
+
+        /**
+         * Whether the service is sending exit announcements and will be destroyed soon.
+         */
+        public boolean exiting = false;
+
+        /**
+         * Create a ServiceRegistration for dns-sd service registration (RFC6763).
+         *
+         * @param deviceHostname Hostname of the device (for the interface used)
+         * @param serviceInfo Service to advertise
+         */
+        ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo) {
+            this.serviceInfo = serviceInfo;
+
+            final String[] serviceType = splitServiceType(serviceInfo);
+            final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+
+            // Service PTR record
+            ptrRecord = new RecordInfo<>(
+                    serviceInfo,
+                    new MdnsPointerRecord(
+                            serviceType,
+                            0L /* receiptTimeMillis */,
+                            false /* cacheFlush */,
+                            NON_NAME_RECORDS_TTL_MILLIS,
+                            serviceName),
+                    true /* sharedName */, true /* probing */);
+
+            srvRecord = new RecordInfo<>(
+                    serviceInfo,
+                    new MdnsServiceRecord(serviceName,
+                            0L /* receiptTimeMillis */,
+                            true /* cacheFlush */,
+                            NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
+                            serviceInfo.getPort(),
+                            deviceHostname),
+                    false /* sharedName */, true /* probing */);
+
+            txtRecord = new RecordInfo<>(
+                    serviceInfo,
+                    new MdnsTextRecord(serviceName,
+                            0L /* receiptTimeMillis */,
+                            true /* cacheFlush */, // Service name is verified unique after probing
+                            NON_NAME_RECORDS_TTL_MILLIS,
+                            attrsToTextEntries(serviceInfo.getAttributes())),
+                    false /* sharedName */, true /* probing */);
+
+            final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(4);
+            allRecords.add(ptrRecord);
+            allRecords.add(srvRecord);
+            allRecords.add(txtRecord);
+            // Service type enumeration record (RFC6763 9.)
+            allRecords.add(new RecordInfo<>(
+                    serviceInfo,
+                    new MdnsPointerRecord(
+                            DNS_SD_SERVICE_TYPE,
+                            0L /* receiptTimeMillis */,
+                            false /* cacheFlush */,
+                            NON_NAME_RECORDS_TTL_MILLIS,
+                            serviceType),
+                    true /* sharedName */, true /* probing */));
+
+            this.allRecords = Collections.unmodifiableList(allRecords);
+        }
+
+        void setProbing(boolean probing) {
+            for (RecordInfo<?> info : allRecords) {
+                info.isProbing = probing;
+            }
+        }
+    }
+
+    /**
+     * Inform the repository of the latest interface addresses.
+     */
+    public void updateAddresses(@NonNull List<LinkAddress> newAddresses) {
+        // TODO: implement to update addresses in records
+    }
+
+    /**
+     * Add a service to the repository.
+     *
+     * This may remove/replace any existing service that used the name added but is exiting.
+     * @param serviceId A unique service ID.
+     * @param serviceInfo Service info to add.
+     * @return If the added service replaced another with a matching name (which was exiting), the
+     *         ID of the replaced service.
+     * @throws NameConflictException There is already a (non-exiting) service using the name.
+     */
+    public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+        if (mServices.contains(serviceId)) {
+            throw new IllegalArgumentException(
+                    "Service ID must not be reused across registrations: " + serviceId);
+        }
+
+        final int existing = getServiceByName(serviceInfo.getServiceName());
+        // It's OK to re-add a service that is exiting
+        if (existing >= 0 && !mServices.get(existing).exiting) {
+            throw new NameConflictException(existing);
+        }
+
+        final ServiceRegistration registration = new ServiceRegistration(
+                mDeviceHostname, serviceInfo);
+        mServices.put(serviceId, registration);
+
+        // Remove existing exiting service
+        mServices.remove(existing);
+        return existing;
+    }
+
+    /**
+     * @return The ID of the service identified by its name, or -1 if none.
+     */
+    private int getServiceByName(@NonNull String serviceName) {
+        for (int i = 0; i < mServices.size(); i++) {
+            final ServiceRegistration registration = mServices.valueAt(i);
+            if (serviceName.equals(registration.serviceInfo.getServiceName())) {
+                return mServices.keyAt(i);
+            }
+        }
+        return -1;
+    }
+
+    private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
+            @NonNull MdnsServiceRecord srvRecord) {
+        final List<MdnsRecord> probingRecords = new ArrayList<>();
+        // Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
+        // RFC6762 10.2
+        probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
+                0L /* receiptTimeMillis */,
+                false /* cacheFlush */,
+                srvRecord.getTtl(),
+                srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
+                srvRecord.getServicePort(),
+                srvRecord.getServiceHost()));
+
+        return new MdnsProber.ProbingInfo(serviceId, probingRecords);
+    }
+
+    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
+        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+        for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
+            out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
+        }
+        return out;
+    }
+
+    /**
+     * Mark a service in the repository as exiting.
+     * @param id ID of the service, used at registration time.
+     * @return The exit announcement to indicate the service was removed, or null if not necessary.
+     */
+    @Nullable
+    public MdnsAnnouncer.AnnouncementInfo exitService(int id) {
+        final ServiceRegistration registration = mServices.get(id);
+        if (registration == null) return null;
+        if (registration.exiting) return null;
+
+        registration.exiting = true;
+
+        // TODO: implement
+        return null;
+    }
+
+    /**
+     * Remove a service from the repository
+     */
+    public void removeService(int id) {
+        mServices.remove(id);
+    }
+
+    /**
+     * @return The number of services currently held in the repository, including exiting services.
+     */
+    public int getServicesCount() {
+        return mServices.size();
+    }
+
+    /**
+     * Remove all services from the repository
+     * @return IDs of the removed services
+     */
+    @NonNull
+    public int[] clearServices() {
+        final int[] ret = new int[mServices.size()];
+        for (int i = 0; i < mServices.size(); i++) {
+            ret[i] = mServices.keyAt(i);
+        }
+        mServices.clear();
+        return ret;
+    }
+
+    /**
+     * Called to indicate that probing succeeded for a service.
+     * @param probeSuccessInfo The successful probing info.
+     * @return The {@link MdnsAnnouncer.AnnouncementInfo} to send, now that probing has succeeded.
+     */
+    public MdnsAnnouncer.AnnouncementInfo onProbingSucceeded(
+            MdnsProber.ProbingInfo probeSuccessInfo) throws IOException {
+        // TODO: implement: set service as not probing anymore and generate announcements
+        throw new IOException("Announcements not implemented");
+    }
+
+    /**
+     * (Re)set a service to the probing state.
+     * @return The {@link MdnsProber.ProbingInfo} to send for probing.
+     */
+    @Nullable
+    public MdnsProber.ProbingInfo setServiceProbing(int serviceId) {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) return null;
+
+        registration.setProbing(true);
+        return makeProbingInfo(serviceId, registration.srvRecord.record);
+    }
+
+    /**
+     * Indicates whether a given service is in probing state.
+     */
+    public boolean isProbing(int serviceId) {
+        final ServiceRegistration registration = mServices.get(serviceId);
+        if (registration == null) return false;
+
+        return registration.srvRecord.isProbing;
+    }
+
+    private static String[] splitFullyQualifiedName(
+            @NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
+        final String[] split = new String[serviceType.length + 1];
+        split[0] = info.getServiceName();
+        System.arraycopy(serviceType, 0, split, 1, serviceType.length);
+
+        return split;
+    }
+
+    private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
+        // String.split(pattern, 0) removes trailing empty strings, which would appear when
+        // splitting "domain.name." (with a dot a the end), so this is what is needed here.
+        final String[] split = info.getServiceType().split("\\.", 0);
+        final String[] type = new String[split.length + 1];
+        System.arraycopy(split, 0, type, 0, split.length);
+        type[split.length] = LOCAL_TLD;
+
+        return type;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsReplySender.java b/service-t/src/com/android/server/mdns/MdnsReplySender.java
new file mode 100644
index 0000000..c6b8f47
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsReplySender.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Looper;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+
+/**
+ * A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them
+ * to be sent after some delay.
+ *
+ * TODO: implement sending after a delay, combining queued replies and duplicate answer suppression
+ */
+public class MdnsReplySender {
+    @NonNull
+    private final MdnsInterfaceSocket mSocket;
+    @NonNull
+    private final Looper mLooper;
+    @NonNull
+    private final byte[] mPacketCreationBuffer;
+
+    public MdnsReplySender(@NonNull Looper looper,
+            @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer) {
+        mLooper = looper;
+        mSocket = socket;
+        mPacketCreationBuffer = packetCreationBuffer;
+    }
+
+    /**
+     * Send a packet immediately.
+     *
+     * Must be called on the looper thread used by the {@link MdnsReplySender}.
+     */
+    public void sendNow(@NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)
+            throws IOException {
+        if (Thread.currentThread() != mLooper.getThread()) {
+            throw new IllegalStateException("sendNow must be called in the handler thread");
+        }
+        if (!((destination.getAddress() instanceof Inet6Address && mSocket.hasJoinedIpv6())
+                || (destination.getAddress() instanceof Inet4Address && mSocket.hasJoinedIpv4()))) {
+            // Skip sending if the socket has not joined the v4/v6 group (there was no address)
+            return;
+        }
+
+        // TODO: support packets over size (send in multiple packets with TC bit set)
+        final MdnsPacketWriter writer = new MdnsPacketWriter(mPacketCreationBuffer);
+
+        writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+        writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
+        writer.writeUInt16(packet.questions.size()); // questions count
+        writer.writeUInt16(packet.answers.size()); // answers count
+        writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
+        writer.writeUInt16(packet.additionalRecords.size()); // additional records count
+
+        for (MdnsRecord record : packet.questions) {
+            // Questions do not have TTL or data
+            record.writeHeaderFields(writer);
+        }
+        for (MdnsRecord record : packet.answers) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.authorityRecords) {
+            record.write(writer, 0L);
+        }
+        for (MdnsRecord record : packet.additionalRecords) {
+            record.write(writer, 0L);
+        }
+
+        final int len = writer.getWritePosition();
+        final byte[] outBuffer = new byte[len];
+        System.arraycopy(mPacketCreationBuffer, 0, outBuffer, 0, len);
+
+        mSocket.send(new DatagramPacket(outBuffer, 0, len, destination));
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsResponse.java b/service-t/src/com/android/server/mdns/MdnsResponse.java
new file mode 100644
index 0000000..3a41978
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsResponse.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+import android.net.Network;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/** An mDNS response. */
+public class MdnsResponse {
+    private final List<MdnsRecord> records;
+    private final List<MdnsPointerRecord> pointerRecords;
+    private MdnsServiceRecord serviceRecord;
+    private MdnsTextRecord textRecord;
+    private MdnsInetAddressRecord inet4AddressRecord;
+    private MdnsInetAddressRecord inet6AddressRecord;
+    private long lastUpdateTime;
+    private final int interfaceIndex;
+    @Nullable private final Network network;
+
+    /** Constructs a new, empty response. */
+    public MdnsResponse(long now, int interfaceIndex, @Nullable Network network) {
+        lastUpdateTime = now;
+        records = new LinkedList<>();
+        pointerRecords = new LinkedList<>();
+        this.interfaceIndex = interfaceIndex;
+        this.network = network;
+    }
+
+    // This generic typed helper compares records for equality.
+    // Returns True if records are the same.
+    private <T> boolean recordsAreSame(T a, T b) {
+        return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
+    }
+
+    /**
+     * Adds a pointer record.
+     *
+     * @return <code>true</code> if the record was added, or <code>false</code> if a matching
+     * pointer
+     * record is already present in the response.
+     */
+    public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
+        if (!pointerRecords.contains(pointerRecord)) {
+            pointerRecords.add(pointerRecord);
+            records.add(pointerRecord);
+            return true;
+        }
+
+        return false;
+    }
+
+    /** Gets the pointer records. */
+    public synchronized List<MdnsPointerRecord> getPointerRecords() {
+        // Returns a shallow copy.
+        return new LinkedList<>(pointerRecords);
+    }
+
+    public synchronized boolean hasPointerRecords() {
+        return !pointerRecords.isEmpty();
+    }
+
+    @VisibleForTesting
+    synchronized void clearPointerRecords() {
+        pointerRecords.clear();
+    }
+
+    public synchronized boolean hasSubtypes() {
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            if (pointerRecord.hasSubtype()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    public synchronized List<String> getSubtypes() {
+        List<String> subtypes = null;
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            String pointerRecordSubtype = pointerRecord.getSubtype();
+            if (pointerRecordSubtype != null) {
+                if (subtypes == null) {
+                    subtypes = new LinkedList<>();
+                }
+                subtypes.add(pointerRecordSubtype);
+            }
+        }
+
+        return subtypes;
+    }
+
+    @VisibleForTesting
+    public synchronized void removeSubtypes() {
+        Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
+        while (iter.hasNext()) {
+            MdnsPointerRecord pointerRecord = iter.next();
+            if (pointerRecord.hasSubtype()) {
+                iter.remove();
+            }
+        }
+    }
+
+    /** Sets the service record. */
+    public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
+        if (recordsAreSame(this.serviceRecord, serviceRecord)) {
+            return false;
+        }
+        if (this.serviceRecord != null) {
+            records.remove(this.serviceRecord);
+        }
+        this.serviceRecord = serviceRecord;
+        if (this.serviceRecord != null) {
+            records.add(this.serviceRecord);
+        }
+        return true;
+    }
+
+    /** Gets the service record. */
+    public synchronized MdnsServiceRecord getServiceRecord() {
+        return serviceRecord;
+    }
+
+    public synchronized boolean hasServiceRecord() {
+        return serviceRecord != null;
+    }
+
+    /** Sets the text record. */
+    public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
+        if (recordsAreSame(this.textRecord, textRecord)) {
+            return false;
+        }
+        if (this.textRecord != null) {
+            records.remove(this.textRecord);
+        }
+        this.textRecord = textRecord;
+        if (this.textRecord != null) {
+            records.add(this.textRecord);
+        }
+        return true;
+    }
+
+    /** Gets the text record. */
+    public synchronized MdnsTextRecord getTextRecord() {
+        return textRecord;
+    }
+
+    public synchronized boolean hasTextRecord() {
+        return textRecord != null;
+    }
+
+    /** Sets the IPv4 address record. */
+    public synchronized boolean setInet4AddressRecord(
+            @Nullable MdnsInetAddressRecord newInet4AddressRecord) {
+        if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
+            return false;
+        }
+        if (this.inet4AddressRecord != null) {
+            records.remove(this.inet4AddressRecord);
+        }
+        if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
+            this.inet4AddressRecord = newInet4AddressRecord;
+            records.add(this.inet4AddressRecord);
+        }
+        return true;
+    }
+
+    /** Gets the IPv4 address record. */
+    public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
+        return inet4AddressRecord;
+    }
+
+    public synchronized boolean hasInet4AddressRecord() {
+        return inet4AddressRecord != null;
+    }
+
+    /** Sets the IPv6 address record. */
+    public synchronized boolean setInet6AddressRecord(
+            @Nullable MdnsInetAddressRecord newInet6AddressRecord) {
+        if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
+            return false;
+        }
+        if (this.inet6AddressRecord != null) {
+            records.remove(this.inet6AddressRecord);
+        }
+        if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
+            this.inet6AddressRecord = newInet6AddressRecord;
+            records.add(this.inet6AddressRecord);
+        }
+        return true;
+    }
+
+    /**
+     * Returns the index of the network interface at which this response was received. Can be set to
+     * {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if unset.
+     */
+    public int getInterfaceIndex() {
+        return interfaceIndex;
+    }
+
+    /**
+     * Returns the network at which this response was received, or null if the network is unknown.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return network;
+    }
+
+    /** Gets the IPv6 address record. */
+    public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
+        return inet6AddressRecord;
+    }
+
+    public synchronized boolean hasInet6AddressRecord() {
+        return inet6AddressRecord != null;
+    }
+
+    /** Gets all of the records. */
+    public synchronized List<MdnsRecord> getRecords() {
+        return new LinkedList<>(records);
+    }
+
+    /**
+     * Merges any records that are present in another response into this one.
+     *
+     * @return <code>true</code> if any records were added or updated.
+     */
+    public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
+        lastUpdateTime = other.lastUpdateTime;
+
+        boolean updated = false;
+
+        List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
+        if (pointerRecords != null) {
+            for (MdnsPointerRecord pointerRecord : pointerRecords) {
+                if (addPointerRecord(pointerRecord)) {
+                    updated = true;
+                }
+            }
+        }
+
+        MdnsServiceRecord serviceRecord = other.getServiceRecord();
+        if (serviceRecord != null) {
+            if (setServiceRecord(serviceRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsTextRecord textRecord = other.getTextRecord();
+        if (textRecord != null) {
+            if (setTextRecord(textRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
+        if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
+            if (setInet4AddressRecord(otherInet4AddressRecord)) {
+                updated = true;
+            }
+        }
+
+        MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
+        if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
+            if (setInet6AddressRecord(otherInet6AddressRecord)) {
+                updated = true;
+            }
+        }
+
+        // If the hostname in the service record no longer matches the hostname in either of the
+        // address records, then drop the address records.
+        if (this.serviceRecord != null) {
+            boolean dropAddressRecords = false;
+
+            if (this.inet4AddressRecord != null) {
+                if (!Arrays.equals(
+                        this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
+                    dropAddressRecords = true;
+                }
+            }
+            if (this.inet6AddressRecord != null) {
+                if (!Arrays.equals(
+                        this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
+                    dropAddressRecords = true;
+                }
+            }
+
+            if (dropAddressRecords) {
+                setInet4AddressRecord(null);
+                setInet6AddressRecord(null);
+                updated = true;
+            }
+        }
+
+        return updated;
+    }
+
+    /**
+     * Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
+     * TXT, and A (for IPv4) or AAAA (for IPv6) records.
+     */
+    public synchronized boolean isComplete() {
+        return !pointerRecords.isEmpty()
+                && (serviceRecord != null)
+                && (textRecord != null)
+                && (inet4AddressRecord != null || inet6AddressRecord != null);
+    }
+
+    /**
+     * Returns the key for this response. The key uniquely identifies the response by its service
+     * name.
+     */
+    public synchronized String getServiceInstanceName() {
+        if (pointerRecords.isEmpty()) {
+            return null;
+        }
+        String[] pointers = pointerRecords.get(0).getPointer();
+        return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
+    }
+
+    /**
+     * Tests if this response is a goodbye message. This will be true if a service record is present
+     * and any of the records have a TTL of 0.
+     */
+    public synchronized boolean isGoodbye() {
+        if (getServiceInstanceName() != null) {
+            for (MdnsRecord record : records) {
+                // Expiring PTR records with subtypes just signal a change in known supported
+                // criteria, not the device itself going offline, so ignore those.
+                if ((record instanceof MdnsPointerRecord)
+                        && ((MdnsPointerRecord) record).hasSubtype()) {
+                    continue;
+                }
+
+                if (record.getTtl() == 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Writes the response to a packet.
+     *
+     * @param writer The writer to use.
+     * @param now    The current time. This is used to write updated TTLs that reflect the remaining
+     *               TTL
+     *               since the response was received.
+     * @return The number of records that were written.
+     * @throws IOException If an error occurred while writing (typically indicating overflow).
+     */
+    public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
+        int count = 0;
+        for (MdnsPointerRecord pointerRecord : pointerRecords) {
+            pointerRecord.write(writer, now);
+            ++count;
+        }
+
+        if (serviceRecord != null) {
+            serviceRecord.write(writer, now);
+            ++count;
+        }
+
+        if (textRecord != null) {
+            textRecord.write(writer, now);
+            ++count;
+        }
+
+        if (inet4AddressRecord != null) {
+            inet4AddressRecord.write(writer, now);
+            ++count;
+        }
+
+        if (inet6AddressRecord != null) {
+            inet6AddressRecord.write(writer, now);
+            ++count;
+        }
+
+        return count;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/mdns/MdnsResponseDecoder.java
new file mode 100644
index 0000000..50f2069
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsResponseDecoder.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.SystemClock;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/** A class that decodes mDNS responses from UDP packets. */
+public class MdnsResponseDecoder {
+
+    public static final int SUCCESS = 0;
+    private static final String TAG = "MdnsResponseDecoder";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private final boolean allowMultipleSrvRecordsPerHost =
+            MdnsConfigs.allowMultipleSrvRecordsPerHost();
+    @Nullable private final String[] serviceType;
+    private final Clock clock;
+
+    /** Constructs a new decoder that will extract responses for the given service type. */
+    public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+        this.clock = clock;
+        this.serviceType = serviceType;
+    }
+
+    private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
+        reader.skip(2 + 4); // skip the class and TTL
+        int dataLength = reader.readUInt16();
+        reader.skip(dataLength);
+    }
+
+    private static MdnsResponse findResponseWithPointer(
+            List<MdnsResponse> responses, String[] pointer) {
+        if (responses != null) {
+            for (MdnsResponse response : responses) {
+                List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
+                if (pointerRecords == null) {
+                    continue;
+                }
+                for (MdnsPointerRecord pointerRecord : pointerRecords) {
+                    if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
+                        return response;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private static MdnsResponse findResponseWithHostName(
+            List<MdnsResponse> responses, String[] hostName) {
+        if (responses != null) {
+            for (MdnsResponse response : responses) {
+                MdnsServiceRecord serviceRecord = response.getServiceRecord();
+                if (serviceRecord == null) {
+                    continue;
+                }
+                if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+                    return response;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Decodes all mDNS responses for the desired service type from a packet. The class does not
+     * check
+     * the responses for completeness; the caller should do that.
+     *
+     * @param packet The packet to read from.
+     * @param interfaceIndex the network interface index (or {@link
+     *     MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if not known) at which the packet was received
+     * @param network the network at which the packet was received, or null if it is unknown.
+     * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+     */
+    public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses,
+            int interfaceIndex, @Nullable Network network) {
+        return decode(packet.getData(), packet.getLength(), responses, interfaceIndex, network);
+    }
+
+    /**
+     * Decodes all mDNS responses for the desired service type from a packet. The class does not
+     * check
+     * the responses for completeness; the caller should do that.
+     *
+     * @param recvbuf The received data buffer to read from.
+     * @param length The length of received data buffer.
+     * @param interfaceIndex the network interface index (or {@link
+     *     MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if not known) at which the packet was received
+     * @param network the network at which the packet was received, or null if it is unknown.
+     * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+     */
+    public int decode(@NonNull byte[] recvbuf, int length, @NonNull List<MdnsResponse> responses,
+            int interfaceIndex, @Nullable Network network) {
+        MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length);
+
+        List<MdnsRecord> records;
+        try {
+            reader.readUInt16(); // transaction ID (not used)
+            int flags = reader.readUInt16();
+            if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
+                return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
+            }
+
+            int numQuestions = reader.readUInt16();
+            int numAnswers = reader.readUInt16();
+            int numAuthority = reader.readUInt16();
+            int numRecords = reader.readUInt16();
+
+            LOGGER.log(String.format(
+                    "num questions: %d, num answers: %d, num authority: %d, num records: %d",
+                    numQuestions, numAnswers, numAuthority, numRecords));
+
+            if (numAnswers < 1) {
+                return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
+            }
+
+            records = new LinkedList<>();
+
+            for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
+                String[] name;
+                try {
+                    name = reader.readLabels();
+                } catch (IOException e) {
+                    LOGGER.e("Failed to read labels from mDNS response.", e);
+                    return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
+                }
+                int type = reader.readUInt16();
+
+                switch (type) {
+                    case MdnsRecord.TYPE_A: {
+                        try {
+                            records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read A record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_AAAA: {
+                        try {
+                            // AAAA should only contain the IPv6 address.
+                            MdnsInetAddressRecord record =
+                                    new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+                            if (record.getInet6Address() != null) {
+                                records.add(record);
+                            }
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read AAAA record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_PTR: {
+                        try {
+                            records.add(new MdnsPointerRecord(name, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read PTR record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_SRV: {
+                        if (name.length == 4) {
+                            try {
+                                records.add(new MdnsServiceRecord(name, reader));
+                            } catch (IOException e) {
+                                LOGGER.e("Failed to read SRV record from mDNS response.", e);
+                                return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
+                            }
+                        } else {
+                            try {
+                                skipMdnsRecord(reader);
+                            } catch (IOException e) {
+                                LOGGER.e("Failed to skip SVR record from mDNS response.", e);
+                                return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
+                            }
+                        }
+                        break;
+                    }
+
+                    case MdnsRecord.TYPE_TXT: {
+                        try {
+                            records.add(new MdnsTextRecord(name, reader));
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to read TXT record from mDNS response.", e);
+                            return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
+                        }
+                        break;
+                    }
+
+                    default: {
+                        try {
+                            skipMdnsRecord(reader);
+                        } catch (IOException e) {
+                            LOGGER.e("Failed to skip mDNS record.", e);
+                            return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
+                        }
+                    }
+                }
+            }
+        } catch (EOFException e) {
+            LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
+            return MdnsResponseErrorCode.ERROR_END_OF_FILE;
+        }
+
+        // The response records are structured in a hierarchy, where some records reference
+        // others, as follows:
+        //
+        //        PTR
+        //        / \
+        //       /   \
+        //      TXT  SRV
+        //           / \
+        //          /   \
+        //         A   AAAA
+        //
+        // But the order in which these records appear in the response packet is completely
+        // arbitrary. This means that we need to rescan the record list to construct each level of
+        // this hierarchy.
+        //
+        // PTR: service type -> service instance name
+        //
+        // SRV: service instance name -> host name (priority, weight)
+        //
+        // TXT: service instance name -> machine readable txt entries.
+        //
+        // A: host name -> IP address
+
+        // Loop 1: find PTR records, which identify distinct service instances.
+        long now = SystemClock.elapsedRealtime();
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsPointerRecord) {
+                String[] name = record.getName();
+                if ((serviceType == null)
+                        || Arrays.equals(name, serviceType)
+                        || ((name.length == (serviceType.length + 2))
+                        && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+                        && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+                    MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
+                    // Group PTR records that refer to the same service instance name into a single
+                    // response.
+                    MdnsResponse response = findResponseWithPointer(responses,
+                            pointerRecord.getPointer());
+                    if (response == null) {
+                        response = new MdnsResponse(now, interfaceIndex, network);
+                        responses.add(response);
+                    }
+                    // Set interface index earlier because some responses have PTR record only.
+                    // Need to know every response is getting from which interface.
+                    response.addPointerRecord((MdnsPointerRecord) record);
+                }
+            }
+        }
+
+        // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsServiceRecord) {
+                MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
+                MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
+                if (response != null) {
+                    response.setServiceRecord(serviceRecord);
+                }
+            } else if (record instanceof MdnsTextRecord) {
+                MdnsTextRecord textRecord = (MdnsTextRecord) record;
+                MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
+                if (response != null) {
+                    response.setTextRecord(textRecord);
+                }
+            }
+        }
+
+        // Loop 3: find A and AAAA records, which reference the host name in the SRV record.
+        for (MdnsRecord record : records) {
+            if (record instanceof MdnsInetAddressRecord) {
+                MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
+                if (allowMultipleSrvRecordsPerHost) {
+                    List<MdnsResponse> matchingResponses =
+                            findResponsesWithHostName(responses, inetRecord.getName());
+                    for (MdnsResponse response : matchingResponses) {
+                        assignInetRecord(response, inetRecord);
+                    }
+                } else {
+                    MdnsResponse response =
+                            findResponseWithHostName(responses, inetRecord.getName());
+                    if (response != null) {
+                        assignInetRecord(response, inetRecord);
+                    }
+                }
+            }
+        }
+
+        return SUCCESS;
+    }
+
+    private static void assignInetRecord(MdnsResponse response, MdnsInetAddressRecord inetRecord) {
+        if (inetRecord.getInet4Address() != null) {
+            response.setInet4AddressRecord(inetRecord);
+        } else if (inetRecord.getInet6Address() != null) {
+            response.setInet6AddressRecord(inetRecord);
+        }
+    }
+
+    private static List<MdnsResponse> findResponsesWithHostName(
+            @Nullable List<MdnsResponse> responses, String[] hostName) {
+        if (responses == null || responses.isEmpty()) {
+            return List.of();
+        }
+
+        List<MdnsResponse> result = null;
+        for (MdnsResponse response : responses) {
+            MdnsServiceRecord serviceRecord = response.getServiceRecord();
+            if (serviceRecord == null) {
+                continue;
+            }
+            if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+                if (result == null) {
+                    result = new ArrayList<>(/* initialCapacity= */ responses.size());
+                }
+                result.add(response);
+            }
+        }
+        return result == null ? List.of() : result;
+    }
+
+    public static class Clock {
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsResponseErrorCode.java b/service-t/src/com/android/server/mdns/MdnsResponseErrorCode.java
new file mode 100644
index 0000000..73a7e3a
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsResponseErrorCode.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * The list of error code for parsing mDNS response.
+ *
+ * @hide
+ */
+public class MdnsResponseErrorCode {
+    public static final int SUCCESS = 0;
+    public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
+    public static final int ERROR_NO_ANSWERS = 2;
+    public static final int ERROR_READING_RECORD_NAME = 3;
+    public static final int ERROR_READING_A_RDATA = 4;
+    public static final int ERROR_READING_AAAA_RDATA = 5;
+    public static final int ERROR_READING_PTR_RDATA = 6;
+    public static final int ERROR_SKIPPING_PTR_RDATA = 7;
+    public static final int ERROR_READING_SRV_RDATA = 8;
+    public static final int ERROR_SKIPPING_SRV_RDATA = 9;
+    public static final int ERROR_READING_TXT_RDATA = 10;
+    public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
+    public static final int ERROR_END_OF_FILE = 12;
+    public static final int ERROR_READING_NSEC_RDATA = 13;
+    public static final int ERROR_READING_ANY_RDATA = 14;
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/mdns/MdnsSearchOptions.java
new file mode 100644
index 0000000..583c4a9
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsSearchOptions.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * API configuration parameters for searching the mDNS service.
+ *
+ * <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
+ *
+ * @hide
+ */
+public class MdnsSearchOptions implements Parcelable {
+
+    /** @hide */
+    public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
+            new Parcelable.Creator<MdnsSearchOptions>() {
+                @Override
+                public MdnsSearchOptions createFromParcel(Parcel source) {
+                    return new MdnsSearchOptions(source.createStringArrayList(),
+                            source.readBoolean(), source.readBoolean(),
+                            source.readParcelable(null));
+                }
+
+                @Override
+                public MdnsSearchOptions[] newArray(int size) {
+                    return new MdnsSearchOptions[size];
+                }
+            };
+    private static MdnsSearchOptions defaultOptions;
+    private final List<String> subtypes;
+
+    private final boolean isPassiveMode;
+    private final boolean removeExpiredService;
+    // The target network for searching. Null network means search on all possible interfaces.
+    @Nullable private final Network mNetwork;
+
+    /** Parcelable constructs for a {@link MdnsSearchOptions}. */
+    MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode, boolean removeExpiredService,
+            @Nullable Network network) {
+        this.subtypes = new ArrayList<>();
+        if (subtypes != null) {
+            this.subtypes.addAll(subtypes);
+        }
+        this.isPassiveMode = isPassiveMode;
+        this.removeExpiredService = removeExpiredService;
+        mNetwork = network;
+    }
+
+    /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** Returns a default search options. */
+    public static synchronized MdnsSearchOptions getDefaultOptions() {
+        if (defaultOptions == null) {
+            defaultOptions = newBuilder().build();
+        }
+        return defaultOptions;
+    }
+
+    /** @return the list of subtypes to search. */
+    public List<String> getSubtypes() {
+        return subtypes;
+    }
+
+    /**
+     * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
+     * order to conserve battery and produce less network traffic.
+     */
+    public boolean isPassiveMode() {
+        return isPassiveMode;
+    }
+
+    /** Returns {@code true} if service will be removed after its TTL expires. */
+    public boolean removeExpiredService() {
+        return removeExpiredService;
+    }
+
+    /**
+     * Returns the network which the mdns query should target on.
+     *
+     * @return the target network or null if search on all possible interfaces.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStringList(subtypes);
+        out.writeBoolean(isPassiveMode);
+        out.writeBoolean(removeExpiredService);
+        out.writeParcelable(mNetwork, 0);
+    }
+
+    /** A builder to create {@link MdnsSearchOptions}. */
+    public static final class Builder {
+        private final Set<String> subtypes;
+        private boolean isPassiveMode = true;
+        private boolean removeExpiredService;
+        private Network mNetwork;
+
+        private Builder() {
+            subtypes = new ArraySet<>();
+        }
+
+        /**
+         * Adds a subtype to search.
+         *
+         * @param subtype the subtype to add.
+         */
+        public Builder addSubtype(@NonNull String subtype) {
+            if (TextUtils.isEmpty(subtype)) {
+                throw new IllegalArgumentException("Empty subtype");
+            }
+            subtypes.add(subtype);
+            return this;
+        }
+
+        /**
+         * Adds a set of subtypes to search.
+         *
+         * @param subtypes The list of subtypes to add.
+         */
+        public Builder addSubtypes(@NonNull Collection<String> subtypes) {
+            this.subtypes.addAll(Objects.requireNonNull(subtypes));
+            return this;
+        }
+
+        /**
+         * Sets if the passive mode scan should be used. The passive mode scans less frequently in
+         * order to conserve battery and produce less network traffic.
+         *
+         * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
+         *                      false}, active mode will be used.
+         */
+        public Builder setIsPassiveMode(boolean isPassiveMode) {
+            this.isPassiveMode = isPassiveMode;
+            return this;
+        }
+
+        /**
+         * Sets if the service should be removed after TTL.
+         *
+         * @param removeExpiredService If set to {@code true}, the service will be removed after TTL
+         */
+        public Builder setRemoveExpiredService(boolean removeExpiredService) {
+            this.removeExpiredService = removeExpiredService;
+            return this;
+        }
+
+        /**
+         * Sets if the mdns query should target on specific network.
+         *
+         * @param network the mdns query will target on given network.
+         */
+        public Builder setNetwork(Network network) {
+            mNetwork = network;
+            return this;
+        }
+
+        /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
+        public MdnsSearchOptions build() {
+            return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode,
+                    removeExpiredService, mNetwork);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsServiceBrowserListener.java b/service-t/src/com/android/server/mdns/MdnsServiceBrowserListener.java
new file mode 100644
index 0000000..7c19359
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsServiceBrowserListener.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Listener interface for mDNS service instance discovery events.
+ *
+ * @hide
+ */
+public interface MdnsServiceBrowserListener {
+
+    /**
+     * Called when an mDNS service instance is found. This method would be called only if all
+     * service records (PTR, SRV, TXT, A or AAAA) are received .
+     *
+     * @param serviceInfo The found mDNS service instance.
+     */
+    void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when an mDNS service instance is updated. This method would be called only if all
+     * service records (PTR, SRV, TXT, A or AAAA) are received before.
+     *
+     * @param serviceInfo The updated mDNS service instance.
+     */
+    void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when a mDNS service instance is no longer valid and removed. This method would be
+     * called only if all service records (PTR, SRV, TXT, A or AAAA) are received before.
+     *
+     * @param serviceInfo The service instance of the removed mDNS service.
+     */
+    void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when searching for mDNS service has stopped because of an error.
+     *
+     * TODO (changed when importing code): define error constants
+     *
+     * @param error The error code of the stop reason.
+     */
+    void onSearchStoppedWithError(int error);
+
+    /** Called when it failed to start an mDNS service discovery process. */
+    void onSearchFailedToStart();
+
+    /**
+     * Called when a mDNS service discovery query has been sent.
+     *
+     * @param subtypes      The list of subtypes in the discovery query.
+     * @param transactionId The transaction ID of the query.
+     */
+    void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
+
+    /**
+     * Called when an error has happened when parsing a received mDNS response packet.
+     *
+     * @param receivedPacketNumber The packet sequence number of the received packet.
+     * @param errorCode            The error code, defined in {@link MdnsResponseErrorCode}.
+     */
+    void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+
+    /**
+     * Called when a mDNS service instance is discovered. This method would be called if the PTR
+     * record has been received.
+     *
+     * @param serviceInfo The discovered mDNS service instance.
+     */
+    void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo);
+
+    /**
+     * Called when a discovered mDNS service instance is no longer valid and removed.
+     *
+     * @param serviceInfo The service instance of the removed mDNS service.
+     */
+    void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo);
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/mdns/MdnsServiceInfo.java
new file mode 100644
index 0000000..938fc3f
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsServiceInfo.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.net.module.util.ByteUtils;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class representing a discovered mDNS service instance.
+ *
+ * @hide
+ */
+public class MdnsServiceInfo implements Parcelable {
+    private static final Charset US_ASCII = Charset.forName("us-ascii");
+    private static final Charset UTF_8 = Charset.forName("utf-8");
+
+    /** @hide */
+    public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
+            new Parcelable.Creator<MdnsServiceInfo>() {
+
+                @Override
+                public MdnsServiceInfo createFromParcel(Parcel source) {
+                    return new MdnsServiceInfo(
+                            source.readString(),
+                            source.createStringArray(),
+                            source.createStringArrayList(),
+                            source.createStringArray(),
+                            source.readInt(),
+                            source.readString(),
+                            source.readString(),
+                            source.createStringArrayList(),
+                            source.createTypedArrayList(TextEntry.CREATOR),
+                            source.readInt(),
+                            source.readParcelable(null));
+                }
+
+                @Override
+                public MdnsServiceInfo[] newArray(int size) {
+                    return new MdnsServiceInfo[size];
+                }
+            };
+
+    private final String serviceInstanceName;
+    private final String[] serviceType;
+    private final List<String> subtypes;
+    private final String[] hostName;
+    private final int port;
+    @Nullable
+    private final String ipv4Address;
+    @Nullable
+    private final String ipv6Address;
+    final List<String> textStrings;
+    @Nullable
+    final List<TextEntry> textEntries;
+    private final int interfaceIndex;
+
+    private final Map<String, byte[]> attributes;
+    @Nullable
+    private final Network network;
+
+    /** Constructs a {@link MdnsServiceInfo} object with default values. */
+    public MdnsServiceInfo(
+            String serviceInstanceName,
+            String[] serviceType,
+            @Nullable List<String> subtypes,
+            String[] hostName,
+            int port,
+            @Nullable String ipv4Address,
+            @Nullable String ipv6Address,
+            @Nullable List<String> textStrings) {
+        this(
+                serviceInstanceName,
+                serviceType,
+                subtypes,
+                hostName,
+                port,
+                ipv4Address,
+                ipv6Address,
+                textStrings,
+                /* textEntries= */ null,
+                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
+                /* network= */ null);
+    }
+
+    /** Constructs a {@link MdnsServiceInfo} object with default values. */
+    public MdnsServiceInfo(
+            String serviceInstanceName,
+            String[] serviceType,
+            List<String> subtypes,
+            String[] hostName,
+            int port,
+            @Nullable String ipv4Address,
+            @Nullable String ipv6Address,
+            @Nullable List<String> textStrings,
+            @Nullable List<TextEntry> textEntries) {
+        this(
+                serviceInstanceName,
+                serviceType,
+                subtypes,
+                hostName,
+                port,
+                ipv4Address,
+                ipv6Address,
+                textStrings,
+                textEntries,
+                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
+                /* network= */ null);
+    }
+
+    /**
+     * Constructs a {@link MdnsServiceInfo} object with default values.
+     *
+     * @hide
+     */
+    public MdnsServiceInfo(
+            String serviceInstanceName,
+            String[] serviceType,
+            @Nullable List<String> subtypes,
+            String[] hostName,
+            int port,
+            @Nullable String ipv4Address,
+            @Nullable String ipv6Address,
+            @Nullable List<String> textStrings,
+            @Nullable List<TextEntry> textEntries,
+            int interfaceIndex) {
+        this(
+                serviceInstanceName,
+                serviceType,
+                subtypes,
+                hostName,
+                port,
+                ipv4Address,
+                ipv6Address,
+                textStrings,
+                textEntries,
+                interfaceIndex,
+                /* network= */ null);
+    }
+
+    /**
+     * Constructs a {@link MdnsServiceInfo} object with default values.
+     *
+     * @hide
+     */
+    public MdnsServiceInfo(
+            String serviceInstanceName,
+            String[] serviceType,
+            @Nullable List<String> subtypes,
+            String[] hostName,
+            int port,
+            @Nullable String ipv4Address,
+            @Nullable String ipv6Address,
+            @Nullable List<String> textStrings,
+            @Nullable List<TextEntry> textEntries,
+            int interfaceIndex,
+            @Nullable Network network) {
+        this.serviceInstanceName = serviceInstanceName;
+        this.serviceType = serviceType;
+        this.subtypes = new ArrayList<>();
+        if (subtypes != null) {
+            this.subtypes.addAll(subtypes);
+        }
+        this.hostName = hostName;
+        this.port = port;
+        this.ipv4Address = ipv4Address;
+        this.ipv6Address = ipv6Address;
+        this.textStrings = new ArrayList<>();
+        if (textStrings != null) {
+            this.textStrings.addAll(textStrings);
+        }
+        this.textEntries = (textEntries == null) ? null : new ArrayList<>(textEntries);
+
+        // The module side sends both {@code textStrings} and {@code textEntries} for backward
+        // compatibility. We should prefer only {@code textEntries} if it's not null.
+        List<TextEntry> entries =
+                (this.textEntries != null) ? this.textEntries : parseTextStrings(this.textStrings);
+        Map<String, byte[]> attributes = new HashMap<>(entries.size());
+        for (TextEntry entry : entries) {
+            String key = entry.getKey().toLowerCase(Locale.ENGLISH);
+
+            // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4, only the first entry
+            // of the same key should be accepted:
+            // If a client receives a TXT record containing the same key more than once, then the
+            // client MUST silently ignore all but the first occurrence of that attribute.
+            if (!attributes.containsKey(key)) {
+                attributes.put(key, entry.getValue());
+            }
+        }
+        this.attributes = Collections.unmodifiableMap(attributes);
+        this.interfaceIndex = interfaceIndex;
+        this.network = network;
+    }
+
+    private static List<TextEntry> parseTextStrings(List<String> textStrings) {
+        List<TextEntry> list = new ArrayList(textStrings.size());
+        for (String textString : textStrings) {
+            TextEntry entry = TextEntry.fromString(textString);
+            if (entry != null) {
+                list.add(entry);
+            }
+        }
+        return Collections.unmodifiableList(list);
+    }
+
+    /** Returns the name of this service instance. */
+    public String getServiceInstanceName() {
+        return serviceInstanceName;
+    }
+
+    /** Returns the type of this service instance. */
+    public String[] getServiceType() {
+        return serviceType;
+    }
+
+    /** Returns the list of subtypes supported by this service instance. */
+    public List<String> getSubtypes() {
+        return new ArrayList<>(subtypes);
+    }
+
+    /** Returns {@code true} if this service instance supports any subtypes. */
+    public boolean hasSubtypes() {
+        return !subtypes.isEmpty();
+    }
+
+    /** Returns the host name of this service instance. */
+    public String[] getHostName() {
+        return hostName;
+    }
+
+    /** Returns the port number of this service instance. */
+    public int getPort() {
+        return port;
+    }
+
+    /** Returns the IPV4 address of this service instance. */
+    @Nullable
+    public String getIpv4Address() {
+        return ipv4Address;
+    }
+
+    /** Returns the IPV6 address of this service instance. */
+    @Nullable
+    public String getIpv6Address() {
+        return ipv6Address;
+    }
+
+    /**
+     * Returns the index of the network interface at which this response was received, or -1 if the
+     * index is not known.
+     */
+    public int getInterfaceIndex() {
+        return interfaceIndex;
+    }
+
+    /**
+     * Returns the network at which this response was received, or null if the network is unknown.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return network;
+    }
+
+    /**
+     * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
+     * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
+     * attribute value exists for {@code key}.
+     */
+    @Nullable
+    public String getAttributeByKey(@NonNull String key) {
+        byte[] value = getAttributeAsBytes(key);
+        if (value == null) {
+            return null;
+        }
+        return new String(value, UTF_8);
+    }
+
+    /**
+     * Returns the attribute value for {@code key} as a byte array. {@code null} will be returned if
+     * no attribute value exists for {@code key}.
+     */
+    @Nullable
+    public byte[] getAttributeAsBytes(@NonNull String key) {
+        return attributes.get(key.toLowerCase(Locale.ENGLISH));
+    }
+
+    /** Returns an immutable map of all attributes. */
+    public Map<String, String> getAttributes() {
+        Map<String, String> map = new HashMap<>(attributes.size());
+        for (Map.Entry<String, byte[]> kv : attributes.entrySet()) {
+            final byte[] value = kv.getValue();
+            map.put(kv.getKey(), value == null ? null : new String(value, UTF_8));
+        }
+        return Collections.unmodifiableMap(map);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(serviceInstanceName);
+        out.writeStringArray(serviceType);
+        out.writeStringList(subtypes);
+        out.writeStringArray(hostName);
+        out.writeInt(port);
+        out.writeString(ipv4Address);
+        out.writeString(ipv6Address);
+        out.writeStringList(textStrings);
+        out.writeTypedList(textEntries);
+        out.writeInt(interfaceIndex);
+        out.writeParcelable(network, 0);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.ROOT,
+                "Name: %s, subtypes: %s, ip: %s, port: %d",
+                serviceInstanceName,
+                TextUtils.join(",", subtypes),
+                ipv4Address,
+                port);
+    }
+
+
+    /** Represents a DNS TXT key-value pair defined by RFC 6763. */
+    public static final class TextEntry implements Parcelable {
+        public static final Parcelable.Creator<TextEntry> CREATOR =
+                new Parcelable.Creator<TextEntry>() {
+                    @Override
+                    public TextEntry createFromParcel(Parcel source) {
+                        return new TextEntry(source);
+                    }
+
+                    @Override
+                    public TextEntry[] newArray(int size) {
+                        return new TextEntry[size];
+                    }
+                };
+
+        private final String key;
+        private final byte[] value;
+
+        /** Creates a new {@link TextEntry} instance from a '=' separated string. */
+        @Nullable
+        public static TextEntry fromString(String textString) {
+            return fromBytes(textString.getBytes(UTF_8));
+        }
+
+        /** Creates a new {@link TextEntry} instance from a '=' separated byte array. */
+        @Nullable
+        public static TextEntry fromBytes(byte[] textBytes) {
+            int delimitPos = ByteUtils.indexOf(textBytes, (byte) '=');
+
+            // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4:
+            // 1. The key MUST be at least one character.  DNS-SD TXT record strings
+            // beginning with an '=' character (i.e., the key is missing) MUST be
+            // silently ignored.
+            // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
+            // boolean attribute, simply identified as being present, with no value.
+            if (delimitPos < 0) {
+                return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+            } else if (delimitPos == 0) {
+                return null;
+            }
+            return new TextEntry(
+                    new String(Arrays.copyOf(textBytes, delimitPos), US_ASCII),
+                    Arrays.copyOfRange(textBytes, delimitPos + 1, textBytes.length));
+        }
+
+        /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
+        public TextEntry(String key, String value) {
+            this(key, value == null ? null : value.getBytes(UTF_8));
+        }
+
+        /** Creates a new {@link TextEntry} with given key and value of a byte array. */
+        public TextEntry(String key, byte[] value) {
+            this.key = key;
+            this.value = value == null ? null : value.clone();
+        }
+
+        private TextEntry(Parcel in) {
+            key = in.readString();
+            value = in.createByteArray();
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public byte[] getValue() {
+            return value == null ? null : value.clone();
+        }
+
+        /** Converts this {@link TextEntry} instance to '=' separated byte array. */
+        public byte[] toBytes() {
+            final byte[] keyBytes = key.getBytes(US_ASCII);
+            if (value == null) {
+                return keyBytes;
+            }
+            return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
+        }
+
+        /** Converts this {@link TextEntry} instance to '=' separated string. */
+        @Override
+        public String toString() {
+            if (value == null) {
+                return key;
+            }
+            return key + "=" + new String(value, UTF_8);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            } else if (!(other instanceof TextEntry)) {
+                return false;
+            }
+            TextEntry otherEntry = (TextEntry) other;
+
+            return key.equals(otherEntry.key) && Arrays.equals(value, otherEntry.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * key.hashCode() + Arrays.hashCode(value);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeString(key);
+            out.writeByteArray(value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/mdns/MdnsServiceRecord.java
new file mode 100644
index 0000000..ebd8b77
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsServiceRecord.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "SRV" record, which contains service information. */
+@VisibleForTesting
+public class MdnsServiceRecord extends MdnsRecord {
+    public static final int PROTO_NONE = 0;
+    public static final int PROTO_TCP = 1;
+    public static final int PROTO_UDP = 2;
+    private static final String PROTO_TOKEN_TCP = "_tcp";
+    private static final String PROTO_TOKEN_UDP = "_udp";
+    private int servicePriority;
+    private int serviceWeight;
+    private int servicePort;
+    private String[] serviceHost;
+
+    public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        this(name, reader, false);
+    }
+
+    public MdnsServiceRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)
+            throws IOException {
+        super(name, TYPE_SRV, reader, isQuestion);
+    }
+
+    public MdnsServiceRecord(String[] name, long receiptTimeMillis, boolean cacheFlush,
+                    long ttlMillis, int servicePriority, int serviceWeight, int servicePort,
+                    String[] serviceHost) {
+        super(name, TYPE_SRV, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
+                ttlMillis);
+        this.servicePriority = servicePriority;
+        this.serviceWeight = serviceWeight;
+        this.servicePort = servicePort;
+        this.serviceHost = serviceHost;
+    }
+
+    /** Returns the service's port number. */
+    public int getServicePort() {
+        return servicePort;
+    }
+
+    /** Returns the service's host name. */
+    public String[] getServiceHost() {
+        return serviceHost;
+    }
+
+    /** Returns the service's priority. */
+    public int getServicePriority() {
+        return servicePriority;
+    }
+
+    /** Returns the service's weight. */
+    public int getServiceWeight() {
+        return serviceWeight;
+    }
+
+    // Format of name is <instance-name>.<service-name>.<protocol>.<domain>
+
+    /** Returns the service's instance name, which uniquely identifies the service instance. */
+    public String getServiceInstanceName() {
+        if (name.length < 1) {
+            return null;
+        }
+        return name[0];
+    }
+
+    /** Returns the service's name. */
+    public String getServiceName() {
+        if (name.length < 2) {
+            return null;
+        }
+        return name[1];
+    }
+
+    /** Returns the service's protocol. */
+    public int getServiceProtocol() {
+        if (name.length < 3) {
+            return PROTO_NONE;
+        }
+
+        String protocol = name[2];
+        if (protocol.equals(PROTO_TOKEN_TCP)) {
+            return PROTO_TCP;
+        }
+        if (protocol.equals(PROTO_TOKEN_UDP)) {
+            return PROTO_UDP;
+        }
+        return PROTO_NONE;
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        servicePriority = reader.readUInt16();
+        serviceWeight = reader.readUInt16();
+        servicePort = reader.readUInt16();
+        serviceHost = reader.readLabels();
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        writer.writeUInt16(servicePriority);
+        writer.writeUInt16(serviceWeight);
+        writer.writeUInt16(servicePort);
+        writer.writeLabels(serviceHost);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.ROOT,
+                "SRV: %s:%d (prio=%d, weight=%d)",
+                labelsToString(serviceHost),
+                servicePort,
+                servicePriority,
+                serviceWeight);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31)
+                + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+                servicePort);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsServiceRecord)) {
+            return false;
+        }
+        MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
+
+        return super.equals(other)
+                && (servicePriority == otherRecord.servicePriority)
+                && (serviceWeight == otherRecord.serviceWeight)
+                && Arrays.equals(serviceHost, otherRecord.serviceHost)
+                && (servicePort == otherRecord.servicePort);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/mdns/MdnsServiceTypeClient.java
new file mode 100644
index 0000000..d26fbdb
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsServiceTypeClient.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Instance of this class sends and receives mDNS packets of a given service type and invoke
+ * registered {@link MdnsServiceBrowserListener} instances.
+ */
+public class MdnsServiceTypeClient {
+
+    private static final int DEFAULT_MTU = 1500;
+    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsServiceTypeClient");
+
+    private final String serviceType;
+    private final String[] serviceTypeLabels;
+    private final MdnsSocketClientBase socketClient;
+    private final ScheduledExecutorService executor;
+    private final Object lock = new Object();
+    private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
+    private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+    private final boolean removeServiceAfterTtlExpires =
+            MdnsConfigs.removeServiceAfterTtlExpires();
+    private final boolean allowSearchOptionsToRemoveExpiredService =
+            MdnsConfigs.allowSearchOptionsToRemoveExpiredService();
+
+    @Nullable private MdnsSearchOptions searchOptions;
+
+    // The session ID increases when startSendAndReceive() is called where we schedule a
+    // QueryTask for
+    // new subtypes. It stays the same between packets for same subtypes.
+    private long currentSessionId = 0;
+
+    @GuardedBy("lock")
+    @Nullable
+    private Future<?> requestTaskFuture;
+
+    /**
+     * Constructor of {@link MdnsServiceTypeClient}.
+     *
+     * @param socketClient Sends and receives mDNS packet.
+     * @param executor         A {@link ScheduledExecutorService} used to schedule query tasks.
+     */
+    public MdnsServiceTypeClient(
+            @NonNull String serviceType,
+            @NonNull MdnsSocketClientBase socketClient,
+            @NonNull ScheduledExecutorService executor) {
+        this.serviceType = serviceType;
+        this.socketClient = socketClient;
+        this.executor = executor;
+        serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+    }
+
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
+            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+        String[] hostName = null;
+        int port = 0;
+        if (response.hasServiceRecord()) {
+            hostName = response.getServiceRecord().getServiceHost();
+            port = response.getServiceRecord().getServicePort();
+        }
+
+        String ipv4Address = null;
+        String ipv6Address = null;
+        if (response.hasInet4AddressRecord()) {
+            Inet4Address inet4Address = response.getInet4AddressRecord().getInet4Address();
+            ipv4Address = (inet4Address == null) ? null : inet4Address.getHostAddress();
+        }
+        if (response.hasInet6AddressRecord()) {
+            Inet6Address inet6Address = response.getInet6AddressRecord().getInet6Address();
+            ipv6Address = (inet6Address == null) ? null : inet6Address.getHostAddress();
+        }
+        String serviceInstanceName = response.getServiceInstanceName();
+        if (serviceInstanceName == null) {
+            throw new IllegalStateException(
+                    "mDNS response must have non-null service instance name");
+        }
+        List<String> textStrings = null;
+        List<MdnsServiceInfo.TextEntry> textEntries = null;
+        if (response.hasTextRecord()) {
+            textStrings = response.getTextRecord().getStrings();
+            textEntries = response.getTextRecord().getEntries();
+        }
+        // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+        return new MdnsServiceInfo(
+                serviceInstanceName,
+                serviceTypeLabels,
+                response.getSubtypes(),
+                hostName,
+                port,
+                ipv4Address,
+                ipv6Address,
+                textStrings,
+                textEntries,
+                response.getInterfaceIndex(),
+                response.getNetwork());
+    }
+
+    /**
+     * Registers {@code listener} for receiving discovery event of mDNS service instances, and
+     * starts
+     * (or continue) to send mDNS queries periodically.
+     *
+     * @param listener      The {@link MdnsServiceBrowserListener} to register.
+     * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
+     */
+    public void startSendAndReceive(
+            @NonNull MdnsServiceBrowserListener listener,
+            @NonNull MdnsSearchOptions searchOptions) {
+        synchronized (lock) {
+            this.searchOptions = searchOptions;
+            if (listeners.add(listener)) {
+                for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+                    final MdnsServiceInfo info =
+                            buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+                    listener.onServiceNameDiscovered(info);
+                    if (existingResponse.isComplete()) {
+                        listener.onServiceFound(info);
+                    }
+                }
+            }
+            // Cancel the next scheduled periodical task.
+            if (requestTaskFuture != null) {
+                requestTaskFuture.cancel(true);
+            }
+            // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
+            // interested anymore.
+            requestTaskFuture =
+                    executor.submit(
+                            new QueryTask(
+                                    new QueryTaskConfig(
+                                            searchOptions.getSubtypes(),
+                                            searchOptions.isPassiveMode(),
+                                            ++currentSessionId,
+                                            searchOptions.getNetwork())));
+        }
+    }
+
+    /**
+     * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
+     *
+     * @param listener The {@link MdnsServiceBrowserListener} to unregister.
+     * @return {@code true} if no listener is registered with this client after unregistering {@code
+     * listener}. Otherwise returns {@code false}.
+     */
+    public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
+        synchronized (lock) {
+            listeners.remove(listener);
+            if (listeners.isEmpty() && requestTaskFuture != null) {
+                requestTaskFuture.cancel(true);
+                requestTaskFuture = null;
+            }
+            return listeners.isEmpty();
+        }
+    }
+
+    public String[] getServiceTypeLabels() {
+        return serviceTypeLabels;
+    }
+
+    public synchronized void processResponse(@NonNull MdnsResponse response) {
+        if (shouldRemoveServiceAfterTtlExpires()) {
+            // Because {@link QueryTask} and {@link processResponse} are running in different
+            // threads. We need to synchronize {@link lock} to protect
+            // {@link instanceNameToResponse} won’t be modified at the same time.
+            synchronized (lock) {
+                if (response.isGoodbye()) {
+                    onGoodbyeReceived(response.getServiceInstanceName());
+                } else {
+                    onResponseReceived(response);
+                }
+            }
+        } else {
+            if (response.isGoodbye()) {
+                onGoodbyeReceived(response.getServiceInstanceName());
+            } else {
+                onResponseReceived(response);
+            }
+        }
+    }
+
+    public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+        for (MdnsServiceBrowserListener listener : listeners) {
+            listener.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+        }
+    }
+
+    private void onResponseReceived(@NonNull MdnsResponse response) {
+        MdnsResponse currentResponse;
+        currentResponse = instanceNameToResponse.get(response.getServiceInstanceName());
+
+        boolean newServiceFound = false;
+        boolean existingServiceChanged = false;
+        boolean serviceBecomesComplete = false;
+        if (currentResponse == null) {
+            newServiceFound = true;
+            currentResponse = response;
+            String serviceInstanceName = response.getServiceInstanceName();
+            if (serviceInstanceName != null) {
+                instanceNameToResponse.put(serviceInstanceName, currentResponse);
+            }
+        } else {
+            boolean before = currentResponse.isComplete();
+            existingServiceChanged = currentResponse.mergeRecordsFrom(response);
+            boolean after = currentResponse.isComplete();
+            serviceBecomesComplete = !before && after;
+        }
+        if (!newServiceFound && !existingServiceChanged) {
+            return;
+        }
+        MdnsServiceInfo serviceInfo =
+                buildMdnsServiceInfoFromResponse(currentResponse, serviceTypeLabels);
+
+        for (MdnsServiceBrowserListener listener : listeners) {
+            if (newServiceFound) {
+                listener.onServiceNameDiscovered(serviceInfo);
+            }
+
+            if (currentResponse.isComplete()) {
+                if (newServiceFound || serviceBecomesComplete) {
+                    listener.onServiceFound(serviceInfo);
+                } else {
+                    listener.onServiceUpdated(serviceInfo);
+                }
+            }
+        }
+    }
+
+    private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
+        final MdnsResponse response = instanceNameToResponse.remove(serviceInstanceName);
+        if (response == null) {
+            return;
+        }
+        for (MdnsServiceBrowserListener listener : listeners) {
+            final MdnsServiceInfo serviceInfo =
+                    buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+            if (response.isComplete()) {
+                listener.onServiceRemoved(serviceInfo);
+            }
+            listener.onServiceNameRemoved(serviceInfo);
+        }
+    }
+
+    private boolean shouldRemoveServiceAfterTtlExpires() {
+        if (removeServiceAfterTtlExpires) {
+            return true;
+        }
+        return allowSearchOptionsToRemoveExpiredService
+                && searchOptions != null
+                && searchOptions.removeExpiredService();
+    }
+
+    @VisibleForTesting
+    MdnsPacketWriter createMdnsPacketWriter() {
+        return new MdnsPacketWriter(DEFAULT_MTU);
+    }
+
+    // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+    // Call to getConfigForNextRun returns a config that can be used to build the next query task.
+    @VisibleForTesting
+    static class QueryTaskConfig {
+
+        private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+                (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+        private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+        private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+        private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+                (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+        private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+                (int) MdnsConfigs.queriesPerBurstPassive();
+        private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+        // The following fields are used by QueryTask so we need to test them.
+        @VisibleForTesting
+        final List<String> subtypes;
+        private final boolean alwaysAskForUnicastResponse =
+                MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
+        private final boolean usePassiveMode;
+        private final long sessionId;
+        @VisibleForTesting
+        int transactionId;
+        @VisibleForTesting
+        boolean expectUnicastResponse;
+        private int queriesPerBurst;
+        private int timeBetweenBurstsInMs;
+        private int burstCounter;
+        private int timeToRunNextTaskInMs;
+        private boolean isFirstBurst;
+        @Nullable private final Network network;
+
+        QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
+                long sessionId, @Nullable Network network) {
+            this.usePassiveMode = usePassiveMode;
+            this.subtypes = new ArrayList<>(subtypes);
+            this.queriesPerBurst = QUERIES_PER_BURST;
+            this.burstCounter = 0;
+            this.transactionId = 1;
+            this.expectUnicastResponse = true;
+            this.isFirstBurst = true;
+            this.sessionId = sessionId;
+            // Config the scan frequency based on the scan mode.
+            if (this.usePassiveMode) {
+                // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
+                // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+                // queries.
+                this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+            } else {
+                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+                this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+            }
+            this.network = network;
+        }
+
+        QueryTaskConfig getConfigForNextRun() {
+            if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
+                transactionId = 1;
+            }
+            // Only the first query expects uni-cast response.
+            expectUnicastResponse = false;
+            if (++burstCounter == queriesPerBurst) {
+                burstCounter = 0;
+
+                if (alwaysAskForUnicastResponse) {
+                    expectUnicastResponse = true;
+                }
+                // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
+                // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+                // queries.
+                if (isFirstBurst) {
+                    isFirstBurst = false;
+                    if (usePassiveMode) {
+                        queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+                    }
+                }
+                // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+                // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+                // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+                // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+                timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+                if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
+                    timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+                            TIME_BETWEEN_BURSTS_MS);
+                }
+            } else {
+                timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            }
+            return this;
+        }
+    }
+
+    // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
+    private class QueryTask implements Runnable {
+
+        private final QueryTaskConfig config;
+
+        QueryTask(@NonNull QueryTaskConfig config) {
+            this.config = config;
+        }
+
+        @Override
+        public void run() {
+            Pair<Integer, List<String>> result;
+            try {
+                result =
+                        new EnqueueMdnsQueryCallable(
+                                socketClient,
+                                createMdnsPacketWriter(),
+                                serviceType,
+                                config.subtypes,
+                                config.expectUnicastResponse,
+                                config.transactionId,
+                                config.network)
+                                .call();
+            } catch (RuntimeException e) {
+                LOGGER.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
+                        TextUtils.join(",", config.subtypes)), e);
+                result = null;
+            }
+            synchronized (lock) {
+                if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
+                    // In case that the task is not canceled successfully, use session ID to check
+                    // if this task should continue to schedule more.
+                    if (config.sessionId != currentSessionId) {
+                        return;
+                    }
+                }
+
+                if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
+                    if (requestTaskFuture == null) {
+                        // If requestTaskFuture is set to null, the task is cancelled. We can't use
+                        // isCancelled() here because this QueryTask is different from the future
+                        // that is returned from executor.schedule(). See b/71646910.
+                        return;
+                    }
+                }
+                if ((result != null)) {
+                    for (MdnsServiceBrowserListener listener : listeners) {
+                        listener.onDiscoveryQuerySent(result.second, result.first);
+                    }
+                }
+                if (shouldRemoveServiceAfterTtlExpires()) {
+                    Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
+                    while (iter.hasNext()) {
+                        MdnsResponse existingResponse = iter.next();
+                        if (existingResponse.hasServiceRecord()
+                                && existingResponse
+                                .getServiceRecord()
+                                .getRemainingTTL(SystemClock.elapsedRealtime())
+                                == 0) {
+                            iter.remove();
+                            for (MdnsServiceBrowserListener listener : listeners) {
+                                String serviceInstanceName =
+                                        existingResponse.getServiceInstanceName();
+                                if (serviceInstanceName != null) {
+                                    final MdnsServiceInfo serviceInfo =
+                                            buildMdnsServiceInfoFromResponse(
+                                                    existingResponse, serviceTypeLabels);
+                                    if (existingResponse.isComplete()) {
+                                        listener.onServiceRemoved(serviceInfo);
+                                    }
+                                    listener.onServiceNameRemoved(serviceInfo);
+                                }
+                            }
+                        }
+                    }
+                }
+                QueryTaskConfig config = this.config.getConfigForNextRun();
+                requestTaskFuture =
+                        executor.schedule(
+                                new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsSocket.java b/service-t/src/com/android/server/mdns/MdnsSocket.java
new file mode 100644
index 0000000..5fd1354
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsSocket.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+import java.util.List;
+
+/**
+ * {@link MdnsSocket} provides a similar interface to {@link MulticastSocket} and binds to all
+ * available multi-cast network interfaces.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ */
+public class MdnsSocket {
+    private static final MdnsLogger LOGGER = new MdnsLogger("MdnsSocket");
+
+    static final int INTERFACE_INDEX_UNSPECIFIED = -1;
+    public static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    public static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+    private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
+    private final MulticastSocket multicastSocket;
+    private boolean isOnIPv6OnlyNetwork;
+
+    public MdnsSocket(
+            @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+            throws IOException {
+        this(multicastNetworkInterfaceProvider, new MulticastSocket(port));
+    }
+
+    @VisibleForTesting
+    MdnsSocket(@NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider,
+            MulticastSocket multicastSocket) throws IOException {
+        this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
+        this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
+        this.multicastSocket = multicastSocket;
+        // RFC Spec: https://tools.ietf.org/html/rfc6762
+        // Time to live is set 255, which is similar to the jMDNS implementation.
+        multicastSocket.setTimeToLive(255);
+
+        // TODO (changed when importing code): consider tagging the socket for data usage
+        isOnIPv6OnlyNetwork = false;
+    }
+
+    public void send(DatagramPacket packet) throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.setNetworkInterface(networkInterface.getNetworkInterface());
+            multicastSocket.send(packet);
+        }
+    }
+
+    public void receive(DatagramPacket packet) throws IOException {
+        multicastSocket.receive(packet);
+    }
+
+    public void joinGroup() throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+        if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+            isOnIPv6OnlyNetwork = true;
+            multicastAddress = MULTICAST_IPV6_ADDRESS;
+        } else {
+            isOnIPv6OnlyNetwork = false;
+        }
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+        }
+    }
+
+    public void leaveGroup() throws IOException {
+        List<NetworkInterfaceWrapper> networkInterfaces =
+                multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+        InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+        if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+            multicastAddress = MULTICAST_IPV6_ADDRESS;
+        }
+        for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+            multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+        }
+    }
+
+    public void close() {
+        // This is a race with the use of the file descriptor (b/27403984).
+        multicastSocket.close();
+        multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
+    }
+
+    /**
+     * Returns the index of the network interface that this socket is bound to. If the interface
+     * cannot be determined, returns -1.
+     */
+    public int getInterfaceIndex() {
+        try {
+            return multicastSocket.getNetworkInterface().getIndex();
+        } catch (SocketException e) {
+            LOGGER.e("Failed to retrieve interface index for socket.", e);
+            return -1;
+        }
+    }
+
+    /**
+     * Returns the available network that this socket is used to, or null if the network is unknown.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return multicastNetworkInterfaceProvider.getAvailableNetwork();
+    }
+
+    public boolean isOnIPv6OnlyNetwork() {
+        return isOnIPv6OnlyNetwork;
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/mdns/MdnsSocketClient.java
new file mode 100644
index 0000000..907687e
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsSocketClient.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsSocketClientBase.Callback;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.Network;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link MdnsSocketClient} maintains separate threads to send and receive mDNS packets for all
+ * the requested service types.
+ *
+ * <p>See https://tools.ietf.org/html/rfc6763 (namely sections 4 and 5).
+ */
+public class MdnsSocketClient implements MdnsSocketClientBase {
+
+    private static final String TAG = "MdnsClient";
+    // TODO: The following values are copied from cast module. We need to think about the
+    // better way to share those.
+    private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
+    private static final String CAST_PREFS_NAME = "google_cast";
+    private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final String MULTICAST_TYPE = "multicast";
+    private static final String UNICAST_TYPE = "unicast";
+
+    private static final long SLEEP_TIME_FOR_SOCKET_THREAD_MS =
+            MdnsConfigs.sleepTimeForSocketThreadMs();
+    // A value of 0 leads to an infinite wait.
+    private static final long THREAD_JOIN_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
+    private static final int RECEIVER_BUFFER_SIZE = 2048;
+    @VisibleForTesting
+    final Queue<DatagramPacket> multicastPacketQueue = new ArrayDeque<>();
+    @VisibleForTesting
+    final Queue<DatagramPacket> unicastPacketQueue = new ArrayDeque<>();
+    private final Context context;
+    private final byte[] multicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+    @Nullable private final byte[] unicastReceiverBuffer;
+    private final MdnsResponseDecoder responseDecoder;
+    private final MulticastLock multicastLock;
+    private final boolean useSeparateSocketForUnicast =
+            MdnsConfigs.useSeparateSocketToSendUnicastQuery();
+    private final boolean checkMulticastResponse = MdnsConfigs.checkMulticastResponse();
+    private final long checkMulticastResponseIntervalMs =
+            MdnsConfigs.checkMulticastResponseIntervalMs();
+    private final boolean propagateInterfaceIndex =
+            MdnsConfigs.allowNetworkInterfaceIndexPropagation();
+    private final Object socketLock = new Object();
+    private final Object timerObject = new Object();
+    // If multicast response was received in the current session. The value is reset in the
+    // beginning of each session.
+    @VisibleForTesting
+    boolean receivedMulticastResponse;
+    // If unicast response was received in the current session. The value is reset in the beginning
+    // of each session.
+    @VisibleForTesting
+    boolean receivedUnicastResponse;
+    // If the phone is the bad state where it can't receive any multicast response.
+    @VisibleForTesting
+    AtomicBoolean cannotReceiveMulticastResponse = new AtomicBoolean(false);
+    @VisibleForTesting @Nullable volatile Thread sendThread;
+    @VisibleForTesting @Nullable Thread multicastReceiveThread;
+    @VisibleForTesting @Nullable Thread unicastReceiveThread;
+    private volatile boolean shouldStopSocketLoop;
+    @Nullable private Callback callback;
+    @Nullable private MdnsSocket multicastSocket;
+    @Nullable private MdnsSocket unicastSocket;
+    private int receivedPacketNumber = 0;
+    @Nullable private Timer logMdnsPacketTimer;
+    private AtomicInteger packetsCount;
+    @Nullable private Timer checkMulticastResponseTimer;
+
+    public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock) {
+        this.context = context;
+        this.multicastLock = multicastLock;
+        responseDecoder = new MdnsResponseDecoder(new MdnsResponseDecoder.Clock(), null);
+        if (useSeparateSocketForUnicast) {
+            unicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+        } else {
+            unicastReceiverBuffer = null;
+        }
+    }
+
+    @Override
+    public synchronized void setCallback(@Nullable Callback callback) {
+        this.callback = callback;
+    }
+
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    @Override
+    public synchronized void startDiscovery() throws IOException {
+        if (multicastSocket != null) {
+            LOGGER.w("Discovery is already in progress.");
+            return;
+        }
+
+        receivedMulticastResponse = false;
+        receivedUnicastResponse = false;
+        cannotReceiveMulticastResponse.set(false);
+
+        shouldStopSocketLoop = false;
+        try {
+            // TODO (changed when importing code): consider setting thread stats tag
+            multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+            multicastSocket.joinGroup();
+            if (useSeparateSocketForUnicast) {
+                // For unicast, use port 0 and the system will assign it with any available port.
+                unicastSocket = createMdnsSocket(0);
+            }
+            multicastLock.acquire();
+        } catch (IOException e) {
+            multicastLock.release();
+            if (multicastSocket != null) {
+                multicastSocket.close();
+                multicastSocket = null;
+            }
+            if (unicastSocket != null) {
+                unicastSocket.close();
+                unicastSocket = null;
+            }
+            throw e;
+        } finally {
+            // TODO (changed when importing code): consider resetting thread stats tag
+        }
+        createAndStartSendThread();
+        createAndStartReceiverThreads();
+    }
+
+    @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+    @Override
+    public void stopDiscovery() {
+        LOGGER.log("Stop discovery.");
+        if (multicastSocket == null && unicastSocket == null) {
+            return;
+        }
+
+        if (MdnsConfigs.clearMdnsPacketQueueAfterDiscoveryStops()) {
+            synchronized (multicastPacketQueue) {
+                multicastPacketQueue.clear();
+            }
+            synchronized (unicastPacketQueue) {
+                unicastPacketQueue.clear();
+            }
+        }
+
+        multicastLock.release();
+
+        shouldStopSocketLoop = true;
+        waitForSendThreadToStop();
+        waitForReceiverThreadsToStop();
+
+        synchronized (socketLock) {
+            multicastSocket = null;
+            unicastSocket = null;
+        }
+
+        synchronized (timerObject) {
+            if (checkMulticastResponseTimer != null) {
+                checkMulticastResponseTimer.cancel();
+                checkMulticastResponseTimer = null;
+            }
+        }
+    }
+
+    /** Sends a mDNS request packet that asks for multicast response. */
+    @Override
+    public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+        sendMdnsPacket(packet, multicastPacketQueue);
+    }
+
+    /** Sends a mDNS request packet that asks for unicast response. */
+    @Override
+    public void sendUnicastPacket(DatagramPacket packet) {
+        if (useSeparateSocketForUnicast) {
+            sendMdnsPacket(packet, unicastPacketQueue);
+        } else {
+            sendMdnsPacket(packet, multicastPacketQueue);
+        }
+    }
+
+    private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+        if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
+            LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+            return;
+        }
+        synchronized (packetQueueToUse) {
+            while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+                packetQueueToUse.remove();
+            }
+            packetQueueToUse.add(packet);
+        }
+        triggerSendThread();
+    }
+
+    private void createAndStartSendThread() {
+        if (sendThread != null) {
+            LOGGER.w("A socket thread already exists.");
+            return;
+        }
+        sendThread = new Thread(this::sendThreadMain);
+        sendThread.setName("mdns-send");
+        sendThread.start();
+    }
+
+    private void createAndStartReceiverThreads() {
+        if (multicastReceiveThread != null) {
+            LOGGER.w("A multicast receiver thread already exists.");
+            return;
+        }
+        multicastReceiveThread =
+                new Thread(() -> receiveThreadMain(multicastReceiverBuffer, multicastSocket));
+        multicastReceiveThread.setName("mdns-multicast-receive");
+        multicastReceiveThread.start();
+
+        if (useSeparateSocketForUnicast) {
+            unicastReceiveThread =
+                    new Thread(
+                            () -> {
+                                if (unicastReceiverBuffer != null) {
+                                    receiveThreadMain(unicastReceiverBuffer, unicastSocket);
+                                }
+                            });
+            unicastReceiveThread.setName("mdns-unicast-receive");
+            unicastReceiveThread.start();
+        }
+    }
+
+    private void triggerSendThread() {
+        LOGGER.log("Trigger send thread.");
+        Thread sendThread = this.sendThread;
+        if (sendThread != null) {
+            sendThread.interrupt();
+        } else {
+            LOGGER.w("Socket thread is null");
+        }
+    }
+
+    private void waitForReceiverThreadsToStop() {
+        if (multicastReceiveThread != null) {
+            waitForThread(multicastReceiveThread);
+            multicastReceiveThread = null;
+        }
+
+        if (unicastReceiveThread != null) {
+            waitForThread(unicastReceiveThread);
+            unicastReceiveThread = null;
+        }
+    }
+
+    private void waitForSendThreadToStop() {
+        LOGGER.log("wait For Send Thread To Stop");
+        if (sendThread == null) {
+            LOGGER.w("socket thread is already dead.");
+            return;
+        }
+        waitForThread(sendThread);
+        sendThread = null;
+    }
+
+    private void waitForThread(Thread thread) {
+        long startMs = SystemClock.elapsedRealtime();
+        long waitMs = THREAD_JOIN_TIMEOUT_MS;
+        while (thread.isAlive() && (waitMs > 0)) {
+            try {
+                thread.interrupt();
+                thread.join(waitMs);
+                if (thread.isAlive()) {
+                    LOGGER.w("Failed to join thread: " + thread);
+                }
+                break;
+            } catch (InterruptedException e) {
+                // Compute remaining time after at least a single join call, in case the clock
+                // resolution is poor.
+                waitMs = THREAD_JOIN_TIMEOUT_MS - (SystemClock.elapsedRealtime() - startMs);
+            }
+        }
+    }
+
+    private void sendThreadMain() {
+        List<DatagramPacket> multicastPacketsToSend = new ArrayList<>();
+        List<DatagramPacket> unicastPacketsToSend = new ArrayList<>();
+        boolean shouldThreadSleep;
+        try {
+            while (!shouldStopSocketLoop) {
+                try {
+                    // Make a local copy of all packets, and clear the queue.
+                    // Send packets that ask for multicast response.
+                    multicastPacketsToSend.clear();
+                    synchronized (multicastPacketQueue) {
+                        multicastPacketsToSend.addAll(multicastPacketQueue);
+                        multicastPacketQueue.clear();
+                    }
+
+                    // Send packets that ask for unicast response.
+                    if (useSeparateSocketForUnicast) {
+                        unicastPacketsToSend.clear();
+                        synchronized (unicastPacketQueue) {
+                            unicastPacketsToSend.addAll(unicastPacketQueue);
+                            unicastPacketQueue.clear();
+                        }
+                        if (unicastSocket != null) {
+                            sendPackets(unicastPacketsToSend, unicastSocket);
+                        }
+                    }
+
+                    // Send multicast packets.
+                    if (multicastSocket != null) {
+                        sendPackets(multicastPacketsToSend, multicastSocket);
+                    }
+
+                    // Sleep ONLY if no more packets have been added to the queue, while packets
+                    // were being sent.
+                    synchronized (multicastPacketQueue) {
+                        synchronized (unicastPacketQueue) {
+                            shouldThreadSleep =
+                                    multicastPacketQueue.isEmpty() && unicastPacketQueue.isEmpty();
+                        }
+                    }
+                    if (shouldThreadSleep) {
+                        Thread.sleep(SLEEP_TIME_FOR_SOCKET_THREAD_MS);
+                    }
+                } catch (InterruptedException e) {
+                    // Don't log the interruption as it's expected.
+                }
+            }
+        } finally {
+            LOGGER.log("Send thread stopped.");
+            try {
+                if (multicastSocket != null) {
+                    multicastSocket.leaveGroup();
+                }
+            } catch (Exception t) {
+                LOGGER.e("Failed to leave the group.", t);
+            }
+
+            // Close the socket first. This is the only way to interrupt a blocking receive.
+            try {
+                // This is a race with the use of the file descriptor (b/27403984).
+                if (multicastSocket != null) {
+                    multicastSocket.close();
+                }
+                if (unicastSocket != null) {
+                    unicastSocket.close();
+                }
+            } catch (RuntimeException t) {
+                LOGGER.e("Failed to close the mdns socket.", t);
+            }
+        }
+    }
+
+    private void receiveThreadMain(byte[] receiverBuffer, @Nullable MdnsSocket socket) {
+        DatagramPacket packet = new DatagramPacket(receiverBuffer, receiverBuffer.length);
+
+        while (!shouldStopSocketLoop) {
+            try {
+                // This is a race with the use of the file descriptor (b/27403984).
+                synchronized (socketLock) {
+                    // This checks is to make sure the socket was not set to null.
+                    if (socket != null && (socket == multicastSocket || socket == unicastSocket)) {
+                        socket.receive(packet);
+                    }
+                }
+
+                if (!shouldStopSocketLoop) {
+                    String responseType = socket == multicastSocket ? MULTICAST_TYPE : UNICAST_TYPE;
+                    processResponsePacket(
+                            packet,
+                            responseType,
+                            /* interfaceIndex= */ (socket == null || !propagateInterfaceIndex)
+                                    ? MdnsSocket.INTERFACE_INDEX_UNSPECIFIED
+                                    : socket.getInterfaceIndex(),
+                            /* network= */ socket.getNetwork());
+                }
+            } catch (IOException e) {
+                if (!shouldStopSocketLoop) {
+                    LOGGER.e("Failed to receive mDNS packets.", e);
+                }
+            }
+        }
+        LOGGER.log("Receive thread stopped.");
+    }
+
+    private int processResponsePacket(@NonNull DatagramPacket packet, String responseType,
+            int interfaceIndex, @Nullable Network network) {
+        int packetNumber = ++receivedPacketNumber;
+
+        List<MdnsResponse> responses = new LinkedList<>();
+        int errorCode = responseDecoder.decode(packet, responses, interfaceIndex, network);
+        if (errorCode == MdnsResponseDecoder.SUCCESS) {
+            if (responseType.equals(MULTICAST_TYPE)) {
+                receivedMulticastResponse = true;
+                if (cannotReceiveMulticastResponse.getAndSet(false)) {
+                    // If we are already in the bad state, receiving a multicast response means
+                    // we are recovered.
+                    LOGGER.e(
+                            "Recovered from the state where the phone can't receive any multicast"
+                                    + " response");
+                }
+            } else {
+                receivedUnicastResponse = true;
+            }
+            for (MdnsResponse response : responses) {
+                String serviceInstanceName = response.getServiceInstanceName();
+                LOGGER.log("mDNS %s response received: %s at ifIndex %d", responseType,
+                        serviceInstanceName, interfaceIndex);
+                if (callback != null) {
+                    callback.onResponseReceived(response);
+                }
+            }
+        } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+            LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+                    responseType, packetNumber, errorCode));
+            if (callback != null) {
+                callback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+            }
+        }
+        return errorCode;
+    }
+
+    @VisibleForTesting
+    MdnsSocket createMdnsSocket(int port) throws IOException {
+        return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+    }
+
+    private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
+        String requestType = socket == multicastSocket ? "multicast" : "unicast";
+        for (DatagramPacket packet : packets) {
+            if (shouldStopSocketLoop) {
+                break;
+            }
+            try {
+                LOGGER.log("Sending a %s mDNS packet...", requestType);
+                socket.send(packet);
+
+                // Start the timer task to monitor the response.
+                synchronized (timerObject) {
+                    if (socket == multicastSocket) {
+                        if (cannotReceiveMulticastResponse.get()) {
+                            // Don't schedule the timer task if we are already in the bad state.
+                            return;
+                        }
+                        if (checkMulticastResponseTimer != null) {
+                            // Don't schedule the timer task if it's already scheduled.
+                            return;
+                        }
+                        if (checkMulticastResponse && useSeparateSocketForUnicast) {
+                            // Only when useSeparateSocketForUnicast is true, we can tell if we
+                            // received a multicast or unicast response.
+                            checkMulticastResponseTimer = new Timer();
+                            checkMulticastResponseTimer.schedule(
+                                    new TimerTask() {
+                                        @Override
+                                        public void run() {
+                                            synchronized (timerObject) {
+                                                if (checkMulticastResponseTimer == null) {
+                                                    // Discovery already stopped.
+                                                    return;
+                                                }
+                                                if ((!receivedMulticastResponse)
+                                                        && receivedUnicastResponse) {
+                                                    LOGGER.e(String.format(
+                                                            "Haven't received multicast response"
+                                                                    + " in the last %d ms.",
+                                                            checkMulticastResponseIntervalMs));
+                                                    cannotReceiveMulticastResponse.set(true);
+                                                }
+                                                checkMulticastResponseTimer = null;
+                                            }
+                                        }
+                                    },
+                                    checkMulticastResponseIntervalMs);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+            }
+        }
+        packets.clear();
+    }
+
+    public boolean isOnIPv6OnlyNetwork() {
+        return multicastSocket != null && multicastSocket.isOnIPv6OnlyNetwork();
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/mdns/MdnsSocketClientBase.java
new file mode 100644
index 0000000..23504a0
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsSocketClientBase.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+
+/**
+ * Base class for multicast socket client.
+ *
+ * @hide
+ */
+public interface MdnsSocketClientBase {
+    /*** Start mDns discovery on given network. */
+    default void startDiscovery() throws IOException { }
+
+    /*** Stop mDns discovery. */
+    default void stopDiscovery() { }
+
+    /*** Set callback for receiving mDns response */
+    void setCallback(@Nullable Callback callback);
+
+    /*** Sends a mDNS request packet that asks for multicast response. */
+    void sendMulticastPacket(@NonNull DatagramPacket packet);
+
+    /**
+     * Sends a mDNS request packet via given network that asks for multicast response. Null network
+     * means sending packet via all networks.
+     */
+    default void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        throw new UnsupportedOperationException(
+                "This socket client doesn't support per-network sending");
+    }
+
+    /*** Sends a mDNS request packet that asks for unicast response. */
+    void sendUnicastPacket(@NonNull DatagramPacket packet);
+
+    /**
+     * Sends a mDNS request packet via given network that asks for unicast response. Null network
+     * means sending packet via all networks.
+     */
+    default void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+        throw new UnsupportedOperationException(
+                "This socket client doesn't support per-network sending");
+    }
+
+    /*** Notify that the given network is requested for mdns discovery / resolution */
+    default void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
+            @Nullable Network network) { }
+
+    /*** Notify that the network is unrequested */
+    default void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) { }
+
+    /*** Callback for mdns response  */
+    interface Callback {
+        /*** Receive a mdns response */
+        void onResponseReceived(@NonNull MdnsResponse response);
+
+        /*** Parse a mdns response failed */
+        void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/mdns/MdnsSocketProvider.java
new file mode 100644
index 0000000..9298852
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsSocketProvider.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.INetd;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkRequest;
+import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.os.Handler;
+import android.os.Looper;
+import android.system.OsConstants;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.ip.NetlinkMonitor;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link MdnsSocketProvider} manages the multiple sockets for mDns.
+ *
+ * <p>This class is not thread safe, it is intended to be used only from the looper thread.
+ * However, the constructor is an exception, as it is called on another thread;
+ * therefore for thread safety all members of this class MUST either be final or initialized
+ * to their default value (0, false or null).
+ *
+ */
+public class MdnsSocketProvider {
+    private static final String TAG = MdnsSocketProvider.class.getSimpleName();
+    private static final boolean DBG = MdnsDiscoveryManager.DBG;
+    // This buffer size matches what MdnsSocketClient uses currently.
+    // But 1440 should generally be enough because of standard Ethernet.
+    // Note: mdnsresponder mDNSEmbeddedAPI.h uses 8940 for Ethernet jumbo frames.
+    private static final int READ_BUFFER_SIZE = 2048;
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    @NonNull private final Context mContext;
+    @NonNull private final Looper mLooper;
+    @NonNull private final Handler mHandler;
+    @NonNull private final Dependencies mDependencies;
+    @NonNull private final NetworkCallback mNetworkCallback;
+    @NonNull private final TetheringEventCallback mTetheringEventCallback;
+    @NonNull private final NetlinkMonitor mNetlinkMonitor;
+    private final ArrayMap<Network, SocketInfo> mNetworkSockets = new ArrayMap<>();
+    private final ArrayMap<String, SocketInfo> mTetherInterfaceSockets = new ArrayMap<>();
+    private final ArrayMap<Network, LinkProperties> mActiveNetworksLinkProperties =
+            new ArrayMap<>();
+    private final ArrayMap<SocketCallback, Network> mCallbacksToRequestedNetworks =
+            new ArrayMap<>();
+    private final List<String> mLocalOnlyInterfaces = new ArrayList<>();
+    private final List<String> mTetheredInterfaces = new ArrayList<>();
+    private final byte[] mPacketReadBuffer = new byte[READ_BUFFER_SIZE];
+    private boolean mMonitoringSockets = false;
+
+    public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper) {
+        this(context, looper, new Dependencies());
+    }
+
+    MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
+            @NonNull Dependencies deps) {
+        mContext = context;
+        mLooper = looper;
+        mHandler = new Handler(looper);
+        mDependencies = deps;
+        mNetworkCallback = new NetworkCallback() {
+            @Override
+            public void onLost(Network network) {
+                mActiveNetworksLinkProperties.remove(network);
+                removeSocket(network, null /* interfaceName */);
+            }
+
+            @Override
+            public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+                handleLinkPropertiesChanged(network, lp);
+            }
+        };
+        mTetheringEventCallback = new TetheringEventCallback() {
+            @Override
+            public void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {
+                handleTetherInterfacesChanged(mLocalOnlyInterfaces, interfaces);
+            }
+
+            @Override
+            public void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {
+                handleTetherInterfacesChanged(mTetheredInterfaces, interfaces);
+            }
+        };
+
+        mNetlinkMonitor = new SocketNetlinkMonitor(mHandler);
+    }
+
+    /**
+     * Dependencies of MdnsSocketProvider, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /*** Get network interface by given interface name */
+        public NetworkInterfaceWrapper getNetworkInterfaceByName(@NonNull String interfaceName)
+                throws SocketException {
+            final NetworkInterface ni = NetworkInterface.getByName(interfaceName);
+            return ni == null ? null : new NetworkInterfaceWrapper(ni);
+        }
+
+        /*** Check whether given network interface can support mdns */
+        public boolean canScanOnInterface(@NonNull NetworkInterfaceWrapper networkInterface) {
+            return MulticastNetworkInterfaceProvider.canScanOnInterface(networkInterface);
+        }
+
+        /*** Create a MdnsInterfaceSocket */
+        public MdnsInterfaceSocket createMdnsInterfaceSocket(
+                @NonNull NetworkInterface networkInterface, int port, @NonNull Looper looper,
+                @NonNull byte[] packetReadBuffer) throws IOException {
+            return new MdnsInterfaceSocket(networkInterface, port, looper, packetReadBuffer);
+        }
+    }
+
+    /*** Data class for storing socket related info  */
+    private static class SocketInfo {
+        final MdnsInterfaceSocket mSocket;
+        final List<LinkAddress> mAddresses;
+
+        SocketInfo(MdnsInterfaceSocket socket, List<LinkAddress> addresses) {
+            mSocket = socket;
+            mAddresses = new ArrayList<>(addresses);
+        }
+    }
+
+    private static class SocketNetlinkMonitor extends NetlinkMonitor {
+        SocketNetlinkMonitor(Handler handler) {
+            super(handler, LOGGER.mLog, TAG, OsConstants.NETLINK_ROUTE,
+                    NetlinkConstants.RTMGRP_IPV4_IFADDR | NetlinkConstants.RTMGRP_IPV6_IFADDR);
+        }
+
+        @Override
+        public void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
+            // TODO: Handle netlink message.
+        }
+    }
+
+    /*** Ensure that current running thread is same as given handler thread */
+    public static void ensureRunningOnHandlerThread(Handler handler) {
+        if (handler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on Handler thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    /*** Start monitoring sockets by listening callbacks for sockets creation or removal */
+    public void startMonitoringSockets() {
+        ensureRunningOnHandlerThread(mHandler);
+        if (mMonitoringSockets) {
+            Log.d(TAG, "Already monitoring sockets.");
+            return;
+        }
+        if (DBG) Log.d(TAG, "Start monitoring sockets.");
+        mContext.getSystemService(ConnectivityManager.class).registerNetworkCallback(
+                new NetworkRequest.Builder().clearCapabilities().build(),
+                mNetworkCallback, mHandler);
+
+        final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class);
+        tetheringManager.registerTetheringEventCallback(mHandler::post, mTetheringEventCallback);
+
+        mHandler.post(mNetlinkMonitor::start);
+        mMonitoringSockets = true;
+    }
+
+    /*** Stop monitoring sockets and unregister callbacks */
+    public void stopMonitoringSockets() {
+        ensureRunningOnHandlerThread(mHandler);
+        if (!mMonitoringSockets) {
+            Log.d(TAG, "Monitoring sockets hasn't been started.");
+            return;
+        }
+        if (DBG) Log.d(TAG, "Stop monitoring sockets.");
+        mContext.getSystemService(ConnectivityManager.class)
+                .unregisterNetworkCallback(mNetworkCallback);
+
+        final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class);
+        tetheringManager.unregisterTetheringEventCallback(mTetheringEventCallback);
+
+        mHandler.post(mNetlinkMonitor::stop);
+        mMonitoringSockets = false;
+    }
+
+    /*** Check whether the target network is matched current network */
+    public static boolean isNetworkMatched(@Nullable Network targetNetwork,
+            @NonNull Network currentNetwork) {
+        return targetNetwork == null || targetNetwork.equals(currentNetwork);
+    }
+
+    private boolean matchRequestedNetwork(Network network) {
+        return hasAllNetworksRequest()
+                || mCallbacksToRequestedNetworks.containsValue(network);
+    }
+
+    private boolean hasAllNetworksRequest() {
+        return mCallbacksToRequestedNetworks.containsValue(null);
+    }
+
+    private void handleLinkPropertiesChanged(Network network, LinkProperties lp) {
+        mActiveNetworksLinkProperties.put(network, lp);
+        if (!matchRequestedNetwork(network)) {
+            if (DBG) {
+                Log.d(TAG, "Ignore LinkProperties change. There is no request for the"
+                        + " Network:" + network);
+            }
+            return;
+        }
+
+        final SocketInfo socketInfo = mNetworkSockets.get(network);
+        if (socketInfo == null) {
+            createSocket(network, lp);
+        } else {
+            // Update the addresses of this socket.
+            final List<LinkAddress> addresses = lp.getLinkAddresses();
+            socketInfo.mAddresses.clear();
+            socketInfo.mAddresses.addAll(addresses);
+            // Try to join the group again.
+            socketInfo.mSocket.joinGroup(addresses);
+
+            notifyAddressesChanged(network, socketInfo.mSocket, lp);
+        }
+    }
+
+    private static LinkProperties createLPForTetheredInterface(String interfaceName) {
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(interfaceName);
+        // TODO: Use NetlinkMonitor to update addresses for tethering interfaces.
+        return linkProperties;
+    }
+
+    private void handleTetherInterfacesChanged(List<String> current, List<String> updated) {
+        if (!hasAllNetworksRequest()) {
+            // Currently, the network for tethering can not be requested, so the sockets for
+            // tethering are only created if there is a request for all networks (interfaces).
+            // Therefore, this change can skip if there is no such request.
+            if (DBG) {
+                Log.d(TAG, "Ignore tether interfaces change. There is no request for all"
+                        + " networks.");
+            }
+            return;
+        }
+
+        final CompareResult<String> interfaceDiff = new CompareResult<>(
+                current, updated);
+        for (String name : interfaceDiff.added) {
+            createSocket(new Network(INetd.LOCAL_NET_ID), createLPForTetheredInterface(name));
+        }
+        for (String name : interfaceDiff.removed) {
+            removeSocket(new Network(INetd.LOCAL_NET_ID), name);
+        }
+        current.clear();
+        current.addAll(updated);
+    }
+
+    private void createSocket(Network network, LinkProperties lp) {
+        final String interfaceName = lp.getInterfaceName();
+        if (interfaceName == null) {
+            Log.e(TAG, "Can not create socket with null interface name.");
+            return;
+        }
+
+        try {
+            final NetworkInterfaceWrapper networkInterface =
+                    mDependencies.getNetworkInterfaceByName(interfaceName);
+            if (networkInterface == null || !mDependencies.canScanOnInterface(networkInterface)) {
+                return;
+            }
+
+            if (DBG) {
+                Log.d(TAG, "Create a socket on network:" + network
+                        + " with interfaceName:" + interfaceName);
+            }
+            final MdnsInterfaceSocket socket = mDependencies.createMdnsInterfaceSocket(
+                    networkInterface.getNetworkInterface(), MdnsConstants.MDNS_PORT, mLooper,
+                    mPacketReadBuffer);
+            final List<LinkAddress> addresses;
+            if (network.netId == INetd.LOCAL_NET_ID) {
+                addresses = CollectionUtils.map(
+                        networkInterface.getInterfaceAddresses(), LinkAddress::new);
+                mTetherInterfaceSockets.put(interfaceName, new SocketInfo(socket, addresses));
+            } else {
+                addresses = lp.getLinkAddresses();
+                mNetworkSockets.put(network, new SocketInfo(socket, addresses));
+            }
+            // Try to join IPv4/IPv6 group.
+            socket.joinGroup(addresses);
+
+            // Notify the listeners which need this socket.
+            notifySocketCreated(network, socket, addresses);
+        } catch (IOException e) {
+            Log.e(TAG, "Create a socket failed with interface=" + interfaceName, e);
+        }
+    }
+
+    private void removeSocket(Network network, String interfaceName) {
+        final SocketInfo socketInfo = network.netId == INetd.LOCAL_NET_ID
+                ? mTetherInterfaceSockets.remove(interfaceName)
+                : mNetworkSockets.remove(network);
+        if (socketInfo == null) return;
+
+        socketInfo.mSocket.destroy();
+        notifyInterfaceDestroyed(network, socketInfo.mSocket);
+    }
+
+    private void notifySocketCreated(Network network, MdnsInterfaceSocket socket,
+            List<LinkAddress> addresses) {
+        for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
+            final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
+            if (isNetworkMatched(requestedNetwork, network)) {
+                mCallbacksToRequestedNetworks.keyAt(i).onSocketCreated(network, socket, addresses);
+            }
+        }
+    }
+
+    private void notifyInterfaceDestroyed(Network network, MdnsInterfaceSocket socket) {
+        for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
+            final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
+            if (isNetworkMatched(requestedNetwork, network)) {
+                mCallbacksToRequestedNetworks.keyAt(i).onInterfaceDestroyed(network, socket);
+            }
+        }
+    }
+
+    private void notifyAddressesChanged(Network network, MdnsInterfaceSocket socket,
+            LinkProperties lp) {
+        for (int i = 0; i < mCallbacksToRequestedNetworks.size(); i++) {
+            final Network requestedNetwork = mCallbacksToRequestedNetworks.valueAt(i);
+            if (isNetworkMatched(requestedNetwork, network)) {
+                mCallbacksToRequestedNetworks.keyAt(i)
+                        .onAddressesChanged(network, socket, lp.getLinkAddresses());
+            }
+        }
+    }
+
+    private void retrieveAndNotifySocketFromNetwork(Network network, SocketCallback cb) {
+        final SocketInfo socketInfo = mNetworkSockets.get(network);
+        if (socketInfo == null) {
+            final LinkProperties lp = mActiveNetworksLinkProperties.get(network);
+            if (lp == null) {
+                // The requested network is not existed. Maybe wait for LinkProperties change later.
+                if (DBG) Log.d(TAG, "There is no LinkProperties for this network:" + network);
+                return;
+            }
+            createSocket(network, lp);
+        } else {
+            // Notify the socket for requested network.
+            cb.onSocketCreated(network, socketInfo.mSocket, socketInfo.mAddresses);
+        }
+    }
+
+    private void retrieveAndNotifySocketFromInterface(String interfaceName, SocketCallback cb) {
+        final SocketInfo socketInfo = mTetherInterfaceSockets.get(interfaceName);
+        if (socketInfo == null) {
+            createSocket(
+                    new Network(INetd.LOCAL_NET_ID), createLPForTetheredInterface(interfaceName));
+        } else {
+            // Notify the socket for requested network.
+            cb.onSocketCreated(
+                    new Network(INetd.LOCAL_NET_ID), socketInfo.mSocket, socketInfo.mAddresses);
+        }
+    }
+
+    /**
+     * Request a socket for given network.
+     *
+     * @param network the required network for a socket. Null means create sockets on all possible
+     *                networks (interfaces).
+     * @param cb the callback to listen the socket creation.
+     */
+    public void requestSocket(@Nullable Network network, @NonNull SocketCallback cb) {
+        ensureRunningOnHandlerThread(mHandler);
+        mCallbacksToRequestedNetworks.put(cb, network);
+        if (network == null) {
+            // Does not specify a required network, create sockets for all possible
+            // networks (interfaces).
+            for (int i = 0; i < mActiveNetworksLinkProperties.size(); i++) {
+                retrieveAndNotifySocketFromNetwork(mActiveNetworksLinkProperties.keyAt(i), cb);
+            }
+
+            for (String localInterface : mLocalOnlyInterfaces) {
+                retrieveAndNotifySocketFromInterface(localInterface, cb);
+            }
+
+            for (String tetheredInterface : mTetheredInterfaces) {
+                retrieveAndNotifySocketFromInterface(tetheredInterface, cb);
+            }
+        } else {
+            retrieveAndNotifySocketFromNetwork(network, cb);
+        }
+    }
+
+    /*** Unrequest the socket */
+    public void unrequestSocket(@NonNull SocketCallback cb) {
+        ensureRunningOnHandlerThread(mHandler);
+        mCallbacksToRequestedNetworks.remove(cb);
+        if (hasAllNetworksRequest()) {
+            // Still has a request for all networks (interfaces).
+            return;
+        }
+
+        // Check if remaining requests are matched any of sockets.
+        for (int i = mNetworkSockets.size() - 1; i >= 0; i--) {
+            final Network network = mNetworkSockets.keyAt(i);
+            if (matchRequestedNetwork(network)) continue;
+            final SocketInfo info = mNetworkSockets.removeAt(i);
+            info.mSocket.destroy();
+            // Still notify to unrequester for socket destroy.
+            cb.onInterfaceDestroyed(network, info.mSocket);
+        }
+
+        // Remove all sockets for tethering interface because these sockets do not have associated
+        // networks, and they should invoke by a request for all networks (interfaces). If there is
+        // no such request, the sockets for tethering interface should be removed.
+        for (int i = mTetherInterfaceSockets.size() - 1; i >= 0; i--) {
+            final SocketInfo info = mTetherInterfaceSockets.valueAt(i);
+            info.mSocket.destroy();
+            // Still notify to unrequester for socket destroy.
+            cb.onInterfaceDestroyed(new Network(INetd.LOCAL_NET_ID), info.mSocket);
+        }
+        mTetherInterfaceSockets.clear();
+    }
+
+    /*** Callbacks for listening socket changes */
+    public interface SocketCallback {
+        /*** Notify the socket is created */
+        default void onSocketCreated(@NonNull Network network, @NonNull MdnsInterfaceSocket socket,
+                @NonNull List<LinkAddress> addresses) {}
+        /*** Notify the interface is destroyed */
+        default void onInterfaceDestroyed(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket) {}
+        /*** Notify the addresses is changed on the network */
+        default void onAddressesChanged(@NonNull Network network,
+                @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {}
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/mdns/MdnsTextRecord.java
new file mode 100644
index 0000000..4149dbe
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MdnsTextRecord.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
+@VisibleForTesting
+public class MdnsTextRecord extends MdnsRecord {
+    private List<TextEntry> entries;
+
+    public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
+        this(name, reader, false);
+    }
+
+    public MdnsTextRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)
+            throws IOException {
+        super(name, TYPE_TXT, reader, isQuestion);
+    }
+
+    public MdnsTextRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis,
+            List<TextEntry> entries) {
+        super(name, TYPE_TXT, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
+                ttlMillis);
+        this.entries = entries;
+    }
+
+    /** Returns the list of strings. */
+    public List<String> getStrings() {
+        final List<String> list = new ArrayList<>(entries.size());
+        for (TextEntry entry : entries) {
+            list.add(entry.toString());
+        }
+        return Collections.unmodifiableList(list);
+    }
+
+    /** Returns the list of TXT key-value pairs. */
+    public List<TextEntry> getEntries() {
+        return Collections.unmodifiableList(entries);
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        entries = new ArrayList<>();
+        while (reader.getRemaining() > 0) {
+            TextEntry entry = reader.readTextEntry();
+            if (entry != null) {
+                entries.add(entry);
+            }
+        }
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        if (entries != null) {
+            for (TextEntry entry : entries) {
+                writer.writeTextEntry(entry);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("TXT: {");
+        if (entries != null) {
+            for (TextEntry entry : entries) {
+                sb.append(' ').append(entry);
+            }
+        }
+        sb.append("}");
+
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31) + Objects.hash(entries);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsTextRecord)) {
+            return false;
+        }
+
+        return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MulticastNetworkInterfaceProvider.java b/service-t/src/com/android/server/mdns/MulticastNetworkInterfaceProvider.java
new file mode 100644
index 0000000..ade7b95
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MulticastNetworkInterfaceProvider.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.Network;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class is used by the {@link MdnsSocket} to monitor the list of {@link NetworkInterface}
+ * instances that are currently available for multi-cast messaging.
+ */
+public class MulticastNetworkInterfaceProvider {
+
+    private static final String TAG = "MdnsNIProvider";
+    private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+    private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
+
+    private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
+    // Only modifiable from tests.
+    @VisibleForTesting
+    ConnectivityMonitor connectivityMonitor;
+    private volatile boolean connectivityChanged = true;
+
+    @SuppressWarnings("nullness:methodref.receiver.bound")
+    public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+        // IMPORT CHANGED
+        this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
+                context, this::onConnectivityChanged);
+    }
+
+    private synchronized void onConnectivityChanged() {
+        connectivityChanged = true;
+    }
+
+    /**
+     * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+     * network interfaces available for multi-cast messaging has changed.
+     */
+    public void startWatchingConnectivityChanges() {
+        connectivityMonitor.startWatchingConnectivityChanges();
+    }
+
+    /** Stops monitoring changes of connectivity. */
+    public void stopWatchingConnectivityChanges() {
+        connectivityMonitor.stopWatchingConnectivityChanges();
+    }
+
+    /**
+     * Returns the list of {@link NetworkInterfaceWrapper} instances available for multi-cast
+     * messaging.
+     */
+    public synchronized List<NetworkInterfaceWrapper> getMulticastNetworkInterfaces() {
+        if (connectivityChanged) {
+            connectivityChanged = false;
+            updateMulticastNetworkInterfaces();
+            if (multicastNetworkInterfaces.isEmpty()) {
+                LOGGER.log("No network interface available for mDNS scanning.");
+            }
+        }
+        return new ArrayList<>(multicastNetworkInterfaces);
+    }
+
+    private void updateMulticastNetworkInterfaces() {
+        multicastNetworkInterfaces.clear();
+        List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
+        for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
+            if (canScanOnInterface(interfaceWrapper)) {
+                multicastNetworkInterfaces.add(interfaceWrapper);
+            }
+        }
+    }
+
+    public boolean isOnIpV6OnlyNetwork(List<NetworkInterfaceWrapper> networkInterfaces) {
+        if (networkInterfaces.isEmpty()) {
+            return false;
+        }
+
+        // TODO(b/79866499): Remove this when the bug is resolved.
+        if (PREFER_IPV6) {
+            return true;
+        }
+        boolean hasAtleastOneIPv6Address = false;
+        for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaces) {
+            for (InterfaceAddress ifAddr : interfaceWrapper.getInterfaceAddresses()) {
+                if (!(ifAddr.getAddress() instanceof Inet6Address)) {
+                    return false;
+                } else {
+                    hasAtleastOneIPv6Address = true;
+                }
+            }
+        }
+        return hasAtleastOneIPv6Address;
+    }
+
+    @VisibleForTesting
+    List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+        List<NetworkInterfaceWrapper> networkInterfaceWrappers = new ArrayList<>();
+        try {
+            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+            if (interfaces != null) {
+                while (interfaces.hasMoreElements()) {
+                    networkInterfaceWrappers.add(
+                            new NetworkInterfaceWrapper(interfaces.nextElement()));
+                }
+            }
+        } catch (SocketException e) {
+            LOGGER.e("Failed to get network interfaces.", e);
+        } catch (NullPointerException e) {
+            // Android R has a bug that could lead to a NPE. See b/159277702.
+            LOGGER.e("Failed to call getNetworkInterfaces API", e);
+        }
+
+        return networkInterfaceWrappers;
+    }
+
+    @Nullable
+    public Network getAvailableNetwork() {
+        return connectivityMonitor.getAvailableNetwork();
+    }
+
+    /*** Check whether given network interface can support mdns */
+    public static boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+        try {
+            if ((networkInterface == null)
+                    || networkInterface.isLoopback()
+                    || networkInterface.isPointToPoint()
+                    || networkInterface.isVirtual()
+                    || !networkInterface.isUp()
+                    || !networkInterface.supportsMulticast()) {
+                return false;
+            }
+            return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
+        } catch (IOException e) {
+            LOGGER.e(String.format("Failed to check interface %s.",
+                    networkInterface.getNetworkInterface().getDisplayName()), e);
+        }
+
+        return false;
+    }
+
+    private static boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+        for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+            if (ifAddr.getAddress() instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+        for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+            if (ifAddr.getAddress() instanceof Inet6Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/mdns/MulticastPacketReader.java
new file mode 100644
index 0000000..20cc47f
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/MulticastPacketReader.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.server.connectivity.mdns.MdnsSocketProvider.ensureRunningOnHandlerThread;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.ArraySet;
+
+import com.android.net.module.util.FdEventsReader;
+
+import java.io.FileDescriptor;
+import java.net.InetSocketAddress;
+import java.util.Set;
+
+/** Simple reader for mDNS packets. */
+public class MulticastPacketReader extends FdEventsReader<MulticastPacketReader.RecvBuffer> {
+    @NonNull
+    private final String mLogTag;
+    @NonNull
+    private final ParcelFileDescriptor mSocket;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final Set<PacketHandler> mPacketHandlers = new ArraySet<>();
+
+    interface PacketHandler {
+        void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
+    }
+
+    public static final class RecvBuffer {
+        final byte[] data;
+        final InetSocketAddress src;
+
+        private RecvBuffer(byte[] data, InetSocketAddress src) {
+            this.data = data;
+            this.src = src;
+        }
+    }
+
+    /**
+     * Create a new {@link MulticastPacketReader}.
+     * @param socket Socket to read from. This will *not* be closed when the reader terminates.
+     * @param buffer Buffer to read packets into. Will only be used from the handler thread.
+     */
+    protected MulticastPacketReader(@NonNull String interfaceTag,
+            @NonNull ParcelFileDescriptor socket, @NonNull Handler handler,
+            @NonNull byte[] buffer) {
+        super(handler, new RecvBuffer(buffer, new InetSocketAddress()));
+        mLogTag = MulticastPacketReader.class.getSimpleName() + "/" + interfaceTag;
+        mSocket = socket;
+        mHandler = handler;
+    }
+
+    @Override
+    protected int recvBufSize(@NonNull RecvBuffer buffer) {
+        return buffer.data.length;
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        // Keep a reference to the PFD as it would close the fd in its finalizer otherwise
+        return mSocket.getFileDescriptor();
+    }
+
+    @Override
+    protected void onStop() {
+        // Do nothing (do not close the FD)
+    }
+
+    @Override
+    protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
+            throws Exception {
+        return Os.recvfrom(
+                fd, buffer.data, 0, buffer.data.length, 0 /* flags */, buffer.src);
+    }
+
+    @Override
+    protected void handlePacket(@NonNull RecvBuffer recvbuf, int length) {
+        for (PacketHandler handler : mPacketHandlers) {
+            handler.handlePacket(recvbuf.data, length, recvbuf.src);
+        }
+    }
+
+    /**
+     * Add a packet handler to deal with received packets. If the handler is already set,
+     * this is a no-op.
+     */
+    public void addPacketHandler(@NonNull PacketHandler handler) {
+        ensureRunningOnHandlerThread(mHandler);
+        mPacketHandlers.add(handler);
+    }
+}
+
diff --git a/service-t/src/com/android/server/mdns/NameConflictException.java b/service-t/src/com/android/server/mdns/NameConflictException.java
new file mode 100644
index 0000000..c123d02
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/NameConflictException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * An exception thrown when a service name conflicts with an existing service.
+ */
+public class NameConflictException extends Exception {
+    /**
+     * ID of the existing service that conflicted.
+     */
+    public final int conflictingServiceId;
+    public NameConflictException(int conflictingServiceId) {
+        this.conflictingServiceId = conflictingServiceId;
+    }
+}
diff --git a/service-t/src/com/android/server/mdns/NetworkInterfaceWrapper.java b/service-t/src/com/android/server/mdns/NetworkInterfaceWrapper.java
new file mode 100644
index 0000000..0ecae48
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/NetworkInterfaceWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.List;
+
+/** A wrapper class of {@link NetworkInterface} to be mocked in unit tests. */
+public class NetworkInterfaceWrapper {
+    private final NetworkInterface networkInterface;
+
+    public NetworkInterfaceWrapper(NetworkInterface networkInterface) {
+        this.networkInterface = networkInterface;
+    }
+
+    public NetworkInterface getNetworkInterface() {
+        return networkInterface;
+    }
+
+    public boolean isUp() throws SocketException {
+        return networkInterface.isUp();
+    }
+
+    public boolean isLoopback() throws SocketException {
+        return networkInterface.isLoopback();
+    }
+
+    public boolean isPointToPoint() throws SocketException {
+        return networkInterface.isPointToPoint();
+    }
+
+    public boolean isVirtual() {
+        return networkInterface.isVirtual();
+    }
+
+    public boolean supportsMulticast() throws SocketException {
+        return networkInterface.supportsMulticast();
+    }
+
+    public List<InterfaceAddress> getInterfaceAddresses() {
+        return networkInterface.getInterfaceAddresses();
+    }
+
+    @Override
+    public String toString() {
+        return networkInterface.toString();
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/mdns/util/MdnsLogger.java b/service-t/src/com/android/server/mdns/util/MdnsLogger.java
new file mode 100644
index 0000000..63107e5
--- /dev/null
+++ b/service-t/src/com/android/server/mdns/util/MdnsLogger.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns.util;
+
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The logger used in mDNS.
+ */
+public class MdnsLogger {
+    // Make this logger public for other level logging than dogfood.
+    public final SharedLog mLog;
+
+    /**
+     * Constructs a new {@link MdnsLogger} with the given logging tag.
+     *
+     * @param tag The log tag that will be used by this logger
+     */
+    public MdnsLogger(String tag) {
+        mLog = new SharedLog(tag);
+    }
+
+    public void log(String message) {
+        mLog.log(message);
+    }
+
+    public void log(String message, @Nullable Object... args) {
+        mLog.log(message + " ; " + TextUtils.join(" ; ", args));
+    }
+
+    public void d(String message) {
+        mLog.log(message);
+    }
+
+    public void e(String message) {
+        mLog.e(message);
+    }
+
+    public void e(String message, Throwable e) {
+        mLog.e(message, e);
+    }
+
+    public void w(String message) {
+        mLog.w(message);
+    }
+}