Add mdns files and unit tests

- Put mdns code to service/mdns because we don't want to impact
  coverage for code that's not yet in use.
- Also fix some lint errors
  - Common typo errors
  - Lines longer than 100 characters
- Update IgnoreUpTo from S to SC_V2 because the tests should be
  ignored until T.

Bug: 232766079
Test: atest FrameworksNetTests
Change-Id: I19af3bf7ae004bd37960f2ad9014cf0c6a804c8d
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 3ea27f7..9d746b5 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -69,6 +69,7 @@
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
         "java/com/android/server/net/ipmemorystore/*.java",
+        "java/com/android/server/connectivity/mdns/**/*.java",
     ]
 }
 
@@ -143,6 +144,7 @@
     static_libs: [
         "services.core",
         "services.net",
+        "service-mdns",
     ],
     jni_libs: [
         "libandroid_net_connectivity_com_android_net_module_util_jni",
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
new file mode 100644
index 0000000..f84e2d8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
@@ -0,0 +1,138 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link ConnectivityMonitor}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class ConnectivityMonitorWithConnectivityManagerTests {
+    @Mock private Context mContext;
+    @Mock private ConnectivityMonitor.Listener mockListener;
+    @Mock private ConnectivityManager mConnectivityManager;
+
+    private ConnectivityMonitorWithConnectivityManager monitor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mConnectivityManager).when(mContext)
+                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
+    }
+
+    @Test
+    public void testInitialState_shouldNotRegisterNetworkCallback() {
+        verifyNetworkCallbackRegistered(0 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStartDiscovery_shouldRegisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStartDiscoveryTwice_shouldRegisterOneNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.startWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(0 /* time */);
+    }
+
+    @Test
+    public void testStopDiscovery_shouldUnregisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.stopWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(1 /* time */);
+    }
+
+    @Test
+    public void testStopDiscoveryTwice_shouldUnregisterNetworkCallback() {
+        monitor.startWatchingConnectivityChanges();
+        monitor.stopWatchingConnectivityChanges();
+
+        verifyNetworkCallbackRegistered(1 /* time */);
+        verifyNetworkCallbackUnregistered(1 /* time */);
+    }
+
+    @Test
+    public void testIntentFired_shouldNotifyListener() {
+        InOrder inOrder = inOrder(mockListener);
+        monitor.startWatchingConnectivityChanges();
+
+        final ArgumentCaptor<NetworkCallback> callbackCaptor =
+                ArgumentCaptor.forClass(NetworkCallback.class);
+        verify(mConnectivityManager, times(1)).registerNetworkCallback(
+                any(NetworkRequest.class), callbackCaptor.capture());
+
+        final NetworkCallback callback = callbackCaptor.getValue();
+        final Network testNetwork = new Network(1 /* netId */);
+
+        // Simulate network available.
+        callback.onAvailable(testNetwork);
+        inOrder.verify(mockListener).onConnectivityChanged();
+
+        // Simulate network lost.
+        callback.onLost(testNetwork);
+        inOrder.verify(mockListener).onConnectivityChanged();
+
+        // Simulate network unavailable.
+        callback.onUnavailable();
+        inOrder.verify(mockListener).onConnectivityChanged();
+    }
+
+    private void verifyNetworkCallbackRegistered(int time) {
+        verify(mConnectivityManager, times(time)).registerNetworkCallback(
+                any(NetworkRequest.class), any(NetworkCallback.class));
+    }
+
+    private void verifyNetworkCallbackUnregistered(int time) {
+        verify(mConnectivityManager, times(time))
+                .unregisterNetworkCallback(any(NetworkCallback.class));
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
new file mode 100644
index 0000000..3e3c3bf
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -0,0 +1,135 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsDiscoveryManager}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsDiscoveryManagerTests {
+
+    private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
+    private static final String SERVICE_TYPE_2 = "_test._tcp.local";
+
+    @Mock private ExecutorProvider executorProvider;
+    @Mock private MdnsSocketClient socketClient;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+
+    @Mock MdnsServiceBrowserListener mockListenerOne;
+    @Mock MdnsServiceBrowserListener mockListenerTwo;
+    private MdnsDiscoveryManager discoveryManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mockServiceTypeClientOne.getServiceTypeLabels())
+                .thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
+        when(mockServiceTypeClientTwo.getServiceTypeLabels())
+                .thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
+
+        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
+                    @Override
+                    MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+                        if (serviceType.equals(SERVICE_TYPE_1)) {
+                            return mockServiceTypeClientOne;
+                        } else if (serviceType.equals(SERVICE_TYPE_2)) {
+                            return mockServiceTypeClientTwo;
+                        }
+                        return null;
+                    }
+                };
+    }
+
+    @Test
+    public void registerListener_unregisterListener() throws IOException {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        verify(socketClient).startDiscovery();
+        verify(mockServiceTypeClientOne)
+                .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
+        discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
+        verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+        verify(socketClient).stopDiscovery();
+    }
+
+    @Test
+    public void registerMultipleListeners() throws IOException {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        verify(socketClient).startDiscovery();
+        verify(mockServiceTypeClientOne)
+                .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        discoveryManager.registerListener(
+                SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+        verify(mockServiceTypeClientTwo)
+                .startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+    }
+
+    @Test
+    public void onResponseReceived() {
+        discoveryManager.registerListener(
+                SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        discoveryManager.registerListener(
+                SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse responseForServiceTypeOne = createMockResponse(SERVICE_TYPE_1);
+        discoveryManager.onResponseReceived(responseForServiceTypeOne);
+        verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne);
+
+        MdnsResponse responseForServiceTypeTwo = createMockResponse(SERVICE_TYPE_2);
+        discoveryManager.onResponseReceived(responseForServiceTypeTwo);
+        verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo);
+
+        MdnsResponse responseForSubtype = createMockResponse("subtype._sub._googlecast._tcp.local");
+        discoveryManager.onResponseReceived(responseForSubtype);
+        verify(mockServiceTypeClientOne).processResponse(responseForSubtype);
+    }
+
+    private MdnsResponse createMockResponse(String serviceType) {
+        MdnsPointerRecord mockPointerRecord = mock(MdnsPointerRecord.class);
+        MdnsResponse mockResponse = mock(MdnsResponse.class);
+        when(mockResponse.getPointerRecords())
+                .thenReturn(Collections.singletonList(mockPointerRecord));
+        when(mockPointerRecord.getName()).thenReturn(TextUtils.split(serviceType, "\\."));
+        return mockResponse;
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
new file mode 100644
index 0000000..19d8a00
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -0,0 +1,86 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Locale;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsPacketReaderTests {
+
+    @Test
+    public void testLimits() throws IOException {
+        byte[] data = new byte[25];
+        DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+
+        // After creating a new reader, confirm that the remaining is equal to the packet length
+        // (or that there is no temporary limit).
+        MdnsPacketReader packetReader = new MdnsPacketReader(datagramPacket);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to 0.
+        packetReader.setLimit(0);
+        assertEquals(0, packetReader.getRemaining());
+
+        // Confirm that we can clear the temporary limit, and restore to the length of the packet.
+        packetReader.clearLimit();
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to the actual length of the packet.
+        // While parsing packets, it is common to set the limit to the length of the packet.
+        packetReader.setLimit(data.length);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we ignore negative limits.
+        packetReader.setLimit(-10);
+        assertEquals(data.length, packetReader.getRemaining());
+
+        // Confirm that we can set the temporary limit to something less than the packet length.
+        packetReader.setLimit(data.length / 2);
+        assertEquals(data.length / 2, packetReader.getRemaining());
+
+        // Confirm that we throw an exception if trying to set the temporary limit beyond the
+        // packet length.
+        packetReader.clearLimit();
+        try {
+            packetReader.setLimit(data.length * 2 + 1);
+            fail("Should have thrown an IOException when trying to set the temporary limit beyond "
+                    + "the packet length");
+        } catch (IOException e) {
+            // Expected
+        } catch (Exception e) {
+            fail(String.format(
+                    Locale.ROOT,
+                    "Should not have thrown any other exception except " + "for IOException: %s",
+                    e.getMessage()));
+        }
+        assertEquals(data.length, packetReader.getRemaining());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
new file mode 100644
index 0000000..fdb4d4a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -0,0 +1,324 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.util.Log;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsRecordTests {
+    private static final String TAG = "MdnsRecordTests";
+    private static final int MAX_PACKET_SIZE = 4096;
+    private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+    private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+            new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+
+    @Test
+    public void testInet4AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000001" + "0001000011940004" + "0A010203");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        assertEquals("test", name[0]);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_A, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+        Inet4Address addr = record.getInet4Address();
+        assertEquals("/10.1.2.3", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTypeAAAInet6AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400001C"
+                        + "0001000011940010"
+                        + "AABBCCDD11223344"
+                        + "A0B0C0D010203040");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+                reader);
+        assertNull(record.getInet4Address());
+        Inet6Address addr = record.getInet6Address();
+        assertEquals("/aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV6_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTypeAAAInet4AddressRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400001C"
+                        + "0001000011940010"
+                        + "0000000000000000"
+                        + "0000FFFF10203040");
+        assertNotNull(dataIn);
+        HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+                reader);
+        assertNull(record.getInet6Address());
+        Inet4Address addr = record.getInet4Address();
+        assertEquals("/16.32.48.64", addr.toString());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        final byte[] expectedDataIn =
+                HexDump.hexStringToByteArray("047465737400001C000100001194000410203040");
+        assertNotNull(expectedDataIn);
+        String expectedDataInText = HexDump.dumpHexString(expectedDataIn, 0, expectedDataIn.length);
+
+        assertEquals(expectedDataInText, dataOutText);
+    }
+
+    @Test
+    public void testPointerRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "047465737400000C"
+                        + "000100001194000E"
+                        + "03666F6F03626172"
+                        + "047175787800");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_PTR, type);
+
+        MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+        String[] pointer = record.getPointer();
+        assertEquals("foo.bar.quxx", MdnsRecord.labelsToString(pointer));
+
+        assertFalse(record.hasSubtype());
+        assertNull(record.getSubtype());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testServiceRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000021"
+                        + "0001000011940014"
+                        + "000100FF1F480366"
+                        + "6F6F036261720471"
+                        + "75787800");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_SRV, type);
+
+        MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+
+        int servicePort = record.getServicePort();
+        assertEquals(8008, servicePort);
+
+        String serviceHost = MdnsRecord.labelsToString(record.getServiceHost());
+        assertEquals("foo.bar.quxx", serviceHost);
+
+        assertEquals(1, record.getServicePriority());
+        assertEquals(255, record.getServiceWeight());
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+
+    @Test
+    public void testTextRecord() throws IOException {
+        final byte[] dataIn = HexDump.hexStringToByteArray(
+                "0474657374000010"
+                        + "0001000011940024"
+                        + "0D613D68656C6C6F"
+                        + "2074686572650C62"
+                        + "3D31323334353637"
+                        + "3839300878797A3D"
+                        + "21402324");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(1, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_TXT, type);
+
+        MdnsTextRecord record = new MdnsTextRecord(name, reader);
+
+        List<String> strings = record.getStrings();
+        assertNotNull(strings);
+        assertEquals(3, strings.size());
+
+        assertEquals("a=hello there", strings.get(0));
+        assertEquals("b=1234567890", strings.get(1));
+        assertEquals("xyz=!@#$", strings.get(2));
+
+        // Encode
+        MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+        record.write(writer, record.getReceiptTime());
+
+        packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+        byte[] dataOut = packet.getData();
+
+        String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+        Log.d(TAG, dataOutText);
+
+        assertEquals(dataInText, dataOutText);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
new file mode 100644
index 0000000..ea9156c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -0,0 +1,237 @@
+/*
+ * 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.MdnsResponseDecoder.Clock;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseDecoderTests {
+    private static final byte[] data = HexDump.hexStringToByteArray(
+            "0000840000000004"
+            + "00000003134A6F68"
+            + "6E6E792773204368"
+            + "726F6D6563617374"
+            + "0B5F676F6F676C65"
+            + "63617374045F7463"
+            + "70056C6F63616C00"
+            + "0010800100001194"
+            + "006C2369643D3937"
+            + "3062663534376237"
+            + "3533666336336332"
+            + "6432613336626238"
+            + "3936616261380576"
+            + "653D30320D6D643D"
+            + "4368726F6D656361"
+            + "73741269633D2F73"
+            + "657475702F69636F"
+            + "6E2E706E6716666E"
+            + "3D4A6F686E6E7927"
+            + "73204368726F6D65"
+            + "636173740463613D"
+            + "350473743D30095F"
+            + "7365727669636573"
+            + "075F646E732D7364"
+            + "045F756470C03100"
+            + "0C00010000119400"
+            + "02C020C020000C00"
+            + "01000011940002C0"
+            + "0CC00C0021800100"
+            + "000078001C000000"
+            + "001F49134A6F686E"
+            + "6E79277320436872"
+            + "6F6D6563617374C0"
+            + "31C0F30001800100"
+            + "0000780004C0A864"
+            + "68C0F3002F800100"
+            + "0000780005C0F300"
+            + "0140C00C002F8001"
+            + "000011940009C00C"
+            + "00050000800040");
+
+    private static final byte[] data6 = HexDump.hexStringToByteArray(
+            "0000840000000001000000030B5F676F6F676C656361737404"
+            + "5F746370056C6F63616C00000C000100000078003330476F6F676C"
+            + "652D486F6D652D4D61782D61363836666331323961366638636265"
+            + "31643636353139343065336164353766C00CC02E00108001000011"
+            + "9400C02369643D6136383666633132396136663863626531643636"
+            + "3531393430653361643537662363643D4133304233303032363546"
+            + "36384341313233353532434639344141353742314613726D3D4335"
+            + "35393134383530383841313638330576653D3035126D643D476F6F"
+            + "676C6520486F6D65204D61781269633D2F73657475702F69636F6E"
+            + "2E706E6710666E3D417474696320737065616B65720863613D3130"
+            + "3234340473743D320F62733D464138464341363734453537046E66"
+            + "3D320372733DC02E0021800100000078002D000000001F49246136"
+            + "3836666331322D396136662D386362652D316436362D3531393430"
+            + "65336164353766C01DC13F001C8001000000780010200033330000"
+            + "0000DA6C63FFFE7C74830109018001000000780004C0A801026C6F"
+            + "63616C0000018001000000780004C0A8010A000001800100000078"
+            + "0004C0A8010A00000000000000");
+
+    private static final String DUMMY_CAST_SERVICE_NAME = "_googlecast";
+    private static final String[] DUMMY_CAST_SERVICE_TYPE =
+            new String[] {DUMMY_CAST_SERVICE_NAME, "_tcp", "local"};
+
+    private final List<MdnsResponse> responses = new LinkedList<>();
+
+    private final Clock mClock = mock(Clock.class);
+
+    @Before
+    public void setUp() {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+        assertNotNull(data);
+        DatagramPacket packet = new DatagramPacket(data, data.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+        assertEquals(1, responses.size());
+    }
+
+    @Test
+    public void testDecodeWithNullServiceType() {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+        assertNotNull(data);
+        DatagramPacket packet = new DatagramPacket(data, data.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+        assertEquals(2, responses.size());
+    }
+
+    @Test
+    public void testDecodeMultipleAnswerPacket() throws IOException {
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        MdnsInetAddressRecord inet4AddressRecord = response.getInet4AddressRecord();
+        Inet4Address inet4Addr = inet4AddressRecord.getInet4Address();
+
+        assertNotNull(inet4Addr);
+        assertEquals("/192.168.100.104", inet4Addr.toString());
+
+        MdnsServiceRecord serviceRecord = response.getServiceRecord();
+        String serviceName = serviceRecord.getServiceName();
+        assertEquals(DUMMY_CAST_SERVICE_NAME, serviceName);
+
+        String serviceInstanceName = serviceRecord.getServiceInstanceName();
+        assertEquals("Johnny's Chromecast", serviceInstanceName);
+
+        String serviceHost = MdnsRecord.labelsToString(serviceRecord.getServiceHost());
+        assertEquals("Johnny's Chromecast.local", serviceHost);
+
+        int serviceProto = serviceRecord.getServiceProtocol();
+        assertEquals(MdnsServiceRecord.PROTO_TCP, serviceProto);
+
+        int servicePort = serviceRecord.getServicePort();
+        assertEquals(8009, servicePort);
+
+        int servicePriority = serviceRecord.getServicePriority();
+        assertEquals(0, servicePriority);
+
+        int serviceWeight = serviceRecord.getServiceWeight();
+        assertEquals(0, serviceWeight);
+
+        MdnsTextRecord textRecord = response.getTextRecord();
+        List<String> textStrings = textRecord.getStrings();
+        assertEquals(7, textStrings.size());
+        assertEquals("id=970bf547b753fc63c2d2a36bb896aba8", textStrings.get(0));
+        assertEquals("ve=02", textStrings.get(1));
+        assertEquals("md=Chromecast", textStrings.get(2));
+        assertEquals("ic=/setup/icon.png", textStrings.get(3));
+        assertEquals("fn=Johnny's Chromecast", textStrings.get(4));
+        assertEquals("ca=5", textStrings.get(5));
+        assertEquals("st=0", textStrings.get(6));
+    }
+
+    @Test
+    public void testDecodeIPv6AnswerPacket() throws IOException {
+        MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+        assertNotNull(data6);
+        DatagramPacket packet = new DatagramPacket(data6, data6.length);
+        packet.setSocketAddress(
+                new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+
+        responses.clear();
+        int errorCode = decoder.decode(packet, responses);
+        assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
+        assertNotNull(inet6AddressRecord);
+        Inet4Address inet4Addr = inet6AddressRecord.getInet4Address();
+        assertNull(inet4Addr);
+
+        Inet6Address inet6Addr = inet6AddressRecord.getInet6Address();
+        assertNotNull(inet6Addr);
+        assertEquals(inet6Addr.getHostAddress(), "2000:3333::da6c:63ff:fe7c:7483");
+    }
+
+    @Test
+    public void testIsComplete() {
+        MdnsResponse response = responses.get(0);
+        assertTrue(response.isComplete());
+
+        response.clearPointerRecords();
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setInet4AddressRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setInet6AddressRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setServiceRecord(null);
+        assertFalse(response.isComplete());
+
+        response = responses.get(0);
+        response.setTextRecord(null);
+        assertFalse(response.isComplete());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
new file mode 100644
index 0000000..ae16f2b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -0,0 +1,305 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseTests {
+    private static final String TAG = "MdnsResponseTests";
+    // MDNS response packet for name "test" with an IPv4 address of 10.1.2.3
+    private static final byte[] dataIn_ipv4_1 = HexDump.hexStringToByteArray(
+            "0474657374000001" + "0001000011940004" + "0A010203");
+    // MDNS response packet for name "tess" with an IPv4 address of 10.1.2.4
+    private static final byte[] dataIn_ipv4_2 = HexDump.hexStringToByteArray(
+            "0474657373000001" + "0001000011940004" + "0A010204");
+    // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040
+    private static final byte[] dataIn_ipv6_1 = HexDump.hexStringToByteArray(
+            "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203040");
+    // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3030
+    private static final byte[] dataIn_ipv6_2 = HexDump.hexStringToByteArray(
+            "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203030");
+    // MDNS response w/name "test" & PTR to foo.bar.quxx
+    private static final byte[] dataIn_ptr_1 = HexDump.hexStringToByteArray(
+            "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787800");
+    // MDNS response w/name "test" & PTR to foo.bar.quxy
+    private static final byte[] dataIn_ptr_2 = HexDump.hexStringToByteArray(
+            "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787900");
+    // MDNS response w/name "test" & Service for host foo.bar.quxx
+    private static final byte[] dataIn_service_1 = HexDump.hexStringToByteArray(
+            "0474657374000021"
+            + "0001000011940014"
+            + "000100FF1F480366"
+            + "6F6F036261720471"
+            + "75787800");
+    // MDNS response w/name "test" & Service for host test
+    private static final byte[] dataIn_service_2 = HexDump.hexStringToByteArray(
+            "0474657374000021" + "000100001194000B" + "000100FF1F480474" + "657374");
+    // MDNS response w/name "test" & the following text strings:
+    // "a=hello there", "b=1234567890", and "xyz=!$$$"
+    private static final byte[] dataIn_text_1 = HexDump.hexStringToByteArray(
+            "0474657374000010"
+            + "0001000011940024"
+            + "0D613D68656C6C6F"
+            + "2074686572650C62"
+            + "3D31323334353637"
+            + "3839300878797A3D"
+            + "21242424");
+    // MDNS response w/name "test" & the following text strings:
+    // "a=hello there", "b=1234567890", and "xyz=!@#$"
+    private static final byte[] dataIn_text_2 = HexDump.hexStringToByteArray(
+            "0474657374000010"
+            + "0001000011940024"
+            + "0D613D68656C6C6F"
+            + "2074686572650C62"
+            + "3D31323334353637"
+            + "3839300878797A3D"
+            + "21402324");
+
+    // The following helper classes act as wrappers so that IPv4 and IPv6 address records can
+    // be explicitly created by type using same constructor signature as all other records.
+    static class MdnsInet4AddressRecord extends MdnsInetAddressRecord {
+        public MdnsInet4AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+            super(name, MdnsRecord.TYPE_A, reader);
+        }
+    }
+
+    static class MdnsInet6AddressRecord extends MdnsInetAddressRecord {
+        public MdnsInet6AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+            super(name, MdnsRecord.TYPE_AAAA, reader);
+        }
+    }
+
+    // This helper class just wraps the data bytes of a response packet with the contained record
+    // type.
+    // Its only purpose is to make the test code a bit more readable.
+    static class PacketAndRecordClass {
+        public final byte[] packetData;
+        public final Class<?> recordClass;
+
+        public PacketAndRecordClass() {
+            packetData = null;
+            recordClass = null;
+        }
+
+        public PacketAndRecordClass(byte[] data, Class<?> c) {
+            packetData = data;
+            recordClass = c;
+        }
+    }
+
+    // Construct an MdnsResponse with the specified data packets applied.
+    private MdnsResponse makeMdnsResponse(long time, List<PacketAndRecordClass> responseList)
+            throws IOException {
+        MdnsResponse response = new MdnsResponse(time);
+        for (PacketAndRecordClass responseData : responseList) {
+            DatagramPacket packet =
+                    new DatagramPacket(responseData.packetData, responseData.packetData.length);
+            MdnsPacketReader reader = new MdnsPacketReader(packet);
+            String[] name = reader.readLabels();
+            reader.skip(2); // skip record type indication.
+            // Apply the right kind of record to the response.
+            if (responseData.recordClass == MdnsInet4AddressRecord.class) {
+                response.setInet4AddressRecord(new MdnsInet4AddressRecord(name, reader));
+            } else if (responseData.recordClass == MdnsInet6AddressRecord.class) {
+                response.setInet6AddressRecord(new MdnsInet6AddressRecord(name, reader));
+            } else if (responseData.recordClass == MdnsPointerRecord.class) {
+                response.addPointerRecord(new MdnsPointerRecord(name, reader));
+            } else if (responseData.recordClass == MdnsServiceRecord.class) {
+                response.setServiceRecord(new MdnsServiceRecord(name, reader));
+            } else if (responseData.recordClass == MdnsTextRecord.class) {
+                response.setTextRecord(new MdnsTextRecord(name, reader));
+            } else {
+                fail("Unsupported/unexpected MdnsRecord subtype used in test - invalid test!");
+            }
+        }
+        return response;
+    }
+
+    @Test
+    public void getInet4AddressRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ipv4_1, dataIn_ipv4_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasInet4AddressRecord());
+        assertTrue(response.setInet4AddressRecord(record));
+        assertEquals(response.getInet4AddressRecord(), record);
+    }
+
+    @Test
+    public void getInet6AddressRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ipv6_1, dataIn_ipv6_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsInetAddressRecord record =
+                new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasInet6AddressRecord());
+        assertTrue(response.setInet6AddressRecord(record));
+        assertEquals(response.getInet6AddressRecord(), record);
+    }
+
+    @Test
+    public void getPointerRecords_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_ptr_1, dataIn_ptr_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasPointerRecords());
+        assertTrue(response.addPointerRecord(record));
+        List<MdnsPointerRecord> recordList = response.getPointerRecords();
+        assertNotNull(recordList);
+        assertEquals(1, recordList.size());
+        assertEquals(record, recordList.get(0));
+    }
+
+    @Test
+    public void getServiceRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_service_1, dataIn_service_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasServiceRecord());
+        assertTrue(response.setServiceRecord(record));
+        assertEquals(response.getServiceRecord(), record);
+    }
+
+    @Test
+    public void getTextRecord_returnsAddedRecord() throws IOException {
+        DatagramPacket packet = new DatagramPacket(dataIn_text_1, dataIn_text_1.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+        String[] name = reader.readLabels();
+        reader.skip(2); // skip record type indication.
+        MdnsTextRecord record = new MdnsTextRecord(name, reader);
+        MdnsResponse response = new MdnsResponse(0);
+        assertFalse(response.hasTextRecord());
+        assertTrue(response.setTextRecord(record));
+        assertEquals(response.getTextRecord(), record);
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_ipv4_address() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_2, MdnsInet4AddressRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_ipv6_address() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv6_2, MdnsInet6AddressRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_text() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_text_2, MdnsTextRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_service() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_service_1, MdnsServiceRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    public void mergeRecordsFrom_indicates_change_on_pointer() throws IOException {
+        MdnsResponse response = makeMdnsResponse(
+                0,
+                Arrays.asList(new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class)));
+        // Now create a new response that updates the address.
+        MdnsResponse response2 = makeMdnsResponse(
+                100,
+                Arrays.asList(new PacketAndRecordClass(dataIn_ptr_2, MdnsPointerRecord.class)));
+        assertTrue(response.mergeRecordsFrom(response2));
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void mergeRecordsFrom_indicates_noChange() throws IOException {
+        //MdnsConfigsFlagsImpl.useReducedMergeRecordUpdateEvents.override(true);
+        List<PacketAndRecordClass> recordList =
+                Arrays.asList(
+                        new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class),
+                        new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class),
+                        new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class),
+                        new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class),
+                        new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class));
+        // Create a two identical responses.
+        MdnsResponse response = makeMdnsResponse(0, recordList);
+        MdnsResponse response2 = makeMdnsResponse(100, recordList);
+        // Merging should not indicate any change.
+        assertFalse(response.mergeRecordsFrom(response2));
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
new file mode 100644
index 0000000..5843fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -0,0 +1,770 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+
+import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link MdnsServiceTypeClient}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsServiceTypeClientTests {
+
+    private static final String SERVICE_TYPE = "_googlecast._tcp.local";
+
+    @Mock
+    private MdnsServiceBrowserListener mockListenerOne;
+    @Mock
+    private MdnsServiceBrowserListener mockListenerTwo;
+    @Mock
+    private MdnsPacketWriter mockPacketWriter;
+    @Mock
+    private MdnsSocketClient mockSocketClient;
+    @Captor
+    private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
+
+    private final byte[] buf = new byte[10];
+
+    private DatagramPacket[] expectedPackets;
+    private ScheduledFuture<?>[] expectedSendFutures;
+    private FakeExecutor currentThreadExecutor = new FakeExecutor();
+
+    private MdnsServiceTypeClient client;
+
+    @Before
+    @SuppressWarnings("DoNotMock")
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+
+        expectedPackets = new DatagramPacket[16];
+        expectedSendFutures = new ScheduledFuture<?>[16];
+
+        for (int i = 0; i < expectedSendFutures.length; ++i) {
+            expectedPackets[i] = new DatagramPacket(buf, 0, 5);
+            expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
+        }
+        when(mockPacketWriter.getPacket(any(SocketAddress.class)))
+                .thenReturn(expectedPackets[0])
+                .thenReturn(expectedPackets[1])
+                .thenReturn(expectedPackets[2])
+                .thenReturn(expectedPackets[3])
+                .thenReturn(expectedPackets[4])
+                .thenReturn(expectedPackets[5])
+                .thenReturn(expectedPackets[6])
+                .thenReturn(expectedPackets[7])
+                .thenReturn(expectedPackets[8])
+                .thenReturn(expectedPackets[9])
+                .thenReturn(expectedPackets[10])
+                .thenReturn(expectedPackets[11])
+                .thenReturn(expectedPackets[12])
+                .thenReturn(expectedPackets[13])
+                .thenReturn(expectedPackets[14])
+                .thenReturn(expectedPackets[15]);
+
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+    }
+
+    @Test
+    public void sendQueries_activeScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, 3 queries.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
+        verifyAndSendQuery(
+                3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
+        verifyAndSendQuery(
+                6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
+        verifyAndSendQuery(
+                9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Fifth burst will be sent after timeBetweenBurstsMs, 3 queries.
+        verifyAndSendQuery(12, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+        verifyAndSendQuery(
+                13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[15]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_reentry_activeScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, first query is sent.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+        // After the first query is sent, change the subtypes, and restart.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(false)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // The previous scheduled task should be canceled.
+        verify(expectedSendFutures[1]).cancel(true);
+
+        // Queries should continue to be sent.
+        verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_passiveScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, 3 query.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        // Second burst will be sent after timeBetweenBurstsMs, 1 query.
+        verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+        // Third burst will be sent after timeBetweenBurstsMs, 1 query.
+        verifyAndSendQuery(4, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+                false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    public void sendQueries_reentry_passiveScanMode() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // First burst, first query is sent.
+        verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+        // After the first query is sent, change the subtypes, and restart.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(true)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // The previous scheduled task should be canceled.
+        verify(expectedSendFutures[1]).cancel(true);
+
+        // Queries should continue to be sent.
+        verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+        verifyAndSendQuery(
+                2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+        verifyAndSendQuery(
+                3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+        // Stop sending packets.
+        client.stopSendAndReceive(mockListenerOne);
+        verify(expectedSendFutures[5]).cancel(true);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
+        //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        QueryTaskConfig config =
+                new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+        // This is the first query. We will ask for unicast response.
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, 1);
+
+        // For the rest of queries in this burst, we will NOT ask for unicast response.
+        for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+            int oldTransactionId = config.transactionId;
+            config = config.getConfigForNextRun();
+            assertFalse(config.expectUnicastResponse);
+            assertEquals(config.subtypes, searchOptions.getSubtypes());
+            assertEquals(config.transactionId, oldTransactionId + 1);
+        }
+
+        // This is the first query of a new burst. We will ask for unicast response.
+        int oldTransactionId = config.transactionId;
+        config = config.getConfigForNextRun();
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, oldTransactionId + 1);
+    }
+
+    @Test
+    public void testQueryTaskConfig_askForUnicastInFirstQuery() {
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+        QueryTaskConfig config =
+                new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+        // This is the first query. We will ask for unicast response.
+        assertTrue(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, 1);
+
+        // For the rest of queries in this burst, we will NOT ask for unicast response.
+        for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+            int oldTransactionId = config.transactionId;
+            config = config.getConfigForNextRun();
+            assertFalse(config.expectUnicastResponse);
+            assertEquals(config.subtypes, searchOptions.getSubtypes());
+            assertEquals(config.transactionId, oldTransactionId + 1);
+        }
+
+        // This is the first query of a new burst. We will NOT ask for unicast response.
+        int oldTransactionId = config.transactionId;
+        config = config.getConfigForNextRun();
+        assertFalse(config.expectUnicastResponse);
+        assertEquals(config.subtypes, searchOptions.getSubtypes());
+        assertEquals(config.transactionId, oldTransactionId + 1);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
+        //MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Change the sutypes and start a new session.
+        searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .addSubtype("12345")
+                        .addSubtype("abcde")
+                        .setIsPassiveMode(true)
+                        .build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the first mdns task is not successful canceled and it gets
+        // executed anyway.
+        firstMdnsTask.run();
+
+        // Although it gets executes, no more task gets scheduled.
+        assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testIfPreviousTaskIsCanceledWhenSessionStops() {
+        //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
+        MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+        client.startSendAndReceive(mockListenerOne, searchOptions);
+        // Change the sutypes and start a new session.
+        client.stopSendAndReceive(mockListenerOne);
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the first mdns task is not successful canceled and it gets
+        // executed anyway.
+        currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+        // Although it gets executes, no more task gets scheduled.
+        assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+    }
+
+    @Test
+    public void processResponse_incompleteResponse() {
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse response = mock(MdnsResponse.class);
+        when(response.getServiceInstanceName()).thenReturn("service-instance-1");
+        when(response.isComplete()).thenReturn(false);
+
+        client.processResponse(response);
+
+        verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
+        verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
+    }
+
+    @Test
+    public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
+        final String ipV4Address = "192.168.1.1";
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV4Address,
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        // Process a second response with a different port and updated text attributes.
+        MdnsResponse secondResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV4Address,
+                        5354,
+                        Collections.singletonList("ABCDE"),
+                        Collections.singletonMap("key", "value"));
+        client.processResponse(secondResponse);
+
+        // Verify onServiceFound was called once for the initial response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
+        assertEquals(initialServiceInfo.getPort(), 5353);
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+        // Verify onServiceUpdated was called once for the second response.
+        verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
+        assertEquals(updatedServiceInfo.getPort(), 5354);
+        assertTrue(updatedServiceInfo.hasSubtypes());
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+    }
+
+    @Test
+    public void processIPv6Response_getCorrectServiceInfo() throws Exception {
+        final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV6Address,
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        // Process a second response with a different port and updated text attributes.
+        MdnsResponse secondResponse =
+                createResponse(
+                        "service-instance-1",
+                        ipV6Address,
+                        5354,
+                        Collections.singletonList("ABCDE"),
+                        Collections.singletonMap("key", "value"));
+        client.processResponse(secondResponse);
+
+        System.out.println("secondResponses ip"
+                + secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
+
+        // Verify onServiceFound was called once for the initial response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
+        assertEquals(initialServiceInfo.getPort(), 5353);
+        assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+        // Verify onServiceUpdated was called once for the second response.
+        verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+        MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+        assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
+        assertEquals(updatedServiceInfo.getPort(), 5354);
+        assertTrue(updatedServiceInfo.hasSubtypes());
+        assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+    }
+
+    @Test
+    public void processResponse_goodBye() {
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        MdnsResponse response = mock(MdnsResponse.class);
+        when(response.getServiceInstanceName()).thenReturn("goodbye-service-instance-name");
+        when(response.isGoodbye()).thenReturn(true);
+        client.processResponse(response);
+
+        verify(mockListenerOne).onServiceRemoved("goodbye-service-instance-name");
+        verify(mockListenerTwo).onServiceRemoved("goodbye-service-instance-name");
+    }
+
+    @Test
+    public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        "service-instance-1",
+                        "192.168.1.1",
+                        5353,
+                        Collections.singletonList("ABCDE"),
+                        Collections.emptyMap());
+        client.processResponse(initialResponse);
+
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+        // Verify onServiceFound was called once for the existing response.
+        verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+        MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+        assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
+        assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
+        assertEquals(existingServiceInfo.getPort(), 5353);
+        assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+        assertNull(existingServiceInfo.getAttributeByKey("key"));
+
+        // Process a goodbye message for the existing response.
+        MdnsResponse goodByeResponse = mock(MdnsResponse.class);
+        when(goodByeResponse.getServiceInstanceName()).thenReturn("service-instance-1");
+        when(goodByeResponse.isGoodbye()).thenReturn(true);
+        client.processResponse(goodByeResponse);
+
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        // Verify onServiceFound was not called on the newly registered listener after the existing
+        // response is gone.
+        verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
+    }
+
+    @Test
+    public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
+        final String serviceInstanceName = "service-instance-1";
+        client.startSendAndReceive(
+                mockListenerOne,
+                MdnsSearchOptions.newBuilder().build());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
+            throws Exception {
+        //MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is under TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was called.
+        verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
+            throws Exception {
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void processResponse_removeServiceAfterTtlExpiresEnabled_shouldRemove()
+            throws Exception {
+        //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
+        final String serviceInstanceName = "service-instance-1";
+        client =
+                new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+                    @Override
+                    MdnsPacketWriter createMdnsPacketWriter() {
+                        return mockPacketWriter;
+                    }
+                };
+        client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+        // Process the initial response.
+        MdnsResponse initialResponse =
+                createResponse(
+                        serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+                        Map.of());
+        client.processResponse(initialResponse);
+
+        // Clear the scheduled runnable.
+        currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+        // Simulate the case where the response is after TTL.
+        when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+        firstMdnsTask.run();
+
+        // Verify onServiceRemoved was not called.
+        verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+    }
+
+    // verifies that the right query was enqueued with the right delay, and send query by executing
+    // the runnable.
+    private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
+        assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        if (expectsUnicastResponse) {
+            verify(mockSocketClient).sendUnicastPacket(expectedPackets[index]);
+        } else {
+            verify(mockSocketClient).sendMulticastPacket(expectedPackets[index]);
+        }
+    }
+
+    // A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
+    // time.
+    private class FakeExecutor extends ScheduledThreadPoolExecutor {
+        private long lastScheduledDelayInMs;
+        private Runnable lastScheduledRunnable;
+        private Runnable lastSubmittedRunnable;
+        private int futureIndex;
+
+        FakeExecutor() {
+            super(1);
+            lastScheduledDelayInMs = -1;
+        }
+
+        @Override
+        public Future<?> submit(Runnable command) {
+            Future<?> future = super.submit(command);
+            lastSubmittedRunnable = command;
+            return future;
+        }
+
+        // Don't call through the real implementation, just track the scheduled Runnable, and
+        // returns a ScheduledFuture.
+        @Override
+        public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+            lastScheduledDelayInMs = delay;
+            lastScheduledRunnable = command;
+            return expectedSendFutures[futureIndex++];
+        }
+
+        // Returns the delay of the last scheduled task, and clear it.
+        long getAndClearLastScheduledDelayInMs() {
+            long val = lastScheduledDelayInMs;
+            lastScheduledDelayInMs = -1;
+            return val;
+        }
+
+        // Returns the last scheduled task, and clear it.
+        Runnable getAndClearLastScheduledRunnable() {
+            Runnable val = lastScheduledRunnable;
+            lastScheduledRunnable = null;
+            return val;
+        }
+
+        Runnable getAndClearSubmittedRunnable() {
+            Runnable val = lastSubmittedRunnable;
+            lastSubmittedRunnable = null;
+            return val;
+        }
+    }
+
+    // Creates a complete mDNS response.
+    private MdnsResponse createResponse(
+            @NonNull String serviceInstanceName,
+            @NonNull String host,
+            int port,
+            @NonNull List<String> subtypes,
+            @NonNull Map<String, String> textAttributes)
+            throws UnknownHostException {
+        String[] hostName = new String[]{"hostname"};
+        MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
+        when(serviceRecord.getServiceHost()).thenReturn(hostName);
+        when(serviceRecord.getServicePort()).thenReturn(port);
+
+        MdnsResponse response = spy(new MdnsResponse(0));
+
+        MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class);
+        if (host.contains(":")) {
+            when(inetAddressRecord.getInet6Address())
+                    .thenReturn((Inet6Address) Inet6Address.getByName(host));
+            response.setInet6AddressRecord(inetAddressRecord);
+        } else {
+            when(inetAddressRecord.getInet4Address())
+                    .thenReturn((Inet4Address) Inet4Address.getByName(host));
+            response.setInet4AddressRecord(inetAddressRecord);
+        }
+
+        MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
+        List<String> textStrings = new ArrayList<>();
+        for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
+            textStrings.add(kv.getKey() + "=" + kv.getValue());
+        }
+        when(textRecord.getStrings()).thenReturn(textStrings);
+
+        response.setServiceRecord(serviceRecord);
+        response.setTextRecord(textRecord);
+
+        doReturn(false).when(response).isGoodbye();
+        doReturn(true).when(response).isComplete();
+        doReturn(serviceInstanceName).when(response).getServiceInstanceName();
+        doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
+        return response;
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
new file mode 100644
index 0000000..21ed7eb
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -0,0 +1,493 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest.permission;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.text.format.DateUtils;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests for {@link MdnsSocketClient} */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketClientTests {
+    private static final long TIMEOUT = 500;
+    private final byte[] buf = new byte[10];
+    final AtomicBoolean enableMulticastResponse = new AtomicBoolean(true);
+    final AtomicBoolean enableUnicastResponse = new AtomicBoolean(true);
+
+    @Mock private Context mContext;
+    @Mock private WifiManager mockWifiManager;
+    @Mock private MdnsSocket mockMulticastSocket;
+    @Mock private MdnsSocket mockUnicastSocket;
+    @Mock private MulticastLock mockMulticastLock;
+    @Mock private MdnsSocketClient.Callback mockCallback;
+
+    private MdnsSocketClient mdnsClient;
+
+    @Before
+    public void setup() throws RuntimeException, IOException {
+        MockitoAnnotations.initMocks(this);
+
+        when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
+                .thenReturn(mockMulticastLock);
+
+        mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+                    @Override
+                    MdnsSocket createMdnsSocket(int port) throws IOException {
+                        if (port == MdnsConstants.MDNS_PORT) {
+                            return mockMulticastSocket;
+                        }
+                        return mockUnicastSocket;
+                    }
+                };
+        mdnsClient.setCallback(mockCallback);
+
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray(
+                            "0000840000000004"
+                            + "00000003134A6F68"
+                            + "6E6E792773204368"
+                            + "726F6D6563617374"
+                            + "0B5F676F6F676C65"
+                            + "63617374045F7463"
+                            + "70056C6F63616C00"
+                            + "0010800100001194"
+                            + "006C2369643D3937"
+                            + "3062663534376237"
+                            + "3533666336336332"
+                            + "6432613336626238"
+                            + "3936616261380576"
+                            + "653D30320D6D643D"
+                            + "4368726F6D656361"
+                            + "73741269633D2F73"
+                            + "657475702F69636F"
+                            + "6E2E706E6716666E"
+                            + "3D4A6F686E6E7927"
+                            + "73204368726F6D65"
+                            + "636173740463613D"
+                            + "350473743D30095F"
+                            + "7365727669636573"
+                            + "075F646E732D7364"
+                            + "045F756470C03100"
+                            + "0C00010000119400"
+                            + "02C020C020000C00"
+                            + "01000011940002C0"
+                            + "0CC00C0021800100"
+                            + "000078001C000000"
+                            + "001F49134A6F686E"
+                            + "6E79277320436872"
+                            + "6F6D6563617374C0"
+                            + "31C0F30001800100"
+                            + "0000780004C0A864"
+                            + "68C0F3002F800100"
+                            + "0000780005C0F300"
+                            + "0140C00C002F8001"
+                            + "000011940009C00C"
+                            + "00050000800040");
+                    if (enableMulticastResponse.get()) {
+                        DatagramPacket packet = invocationOnMock.getArgument(0);
+                        packet.setData(dataIn);
+                    }
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray(
+                            "0000840000000004"
+                            + "00000003134A6F68"
+                            + "6E6E792773204368"
+                            + "726F6D6563617374"
+                            + "0B5F676F6F676C65"
+                            + "63617374045F7463"
+                            + "70056C6F63616C00"
+                            + "0010800100001194"
+                            + "006C2369643D3937"
+                            + "3062663534376237"
+                            + "3533666336336332"
+                            + "6432613336626238"
+                            + "3936616261380576"
+                            + "653D30320D6D643D"
+                            + "4368726F6D656361"
+                            + "73741269633D2F73"
+                            + "657475702F69636F"
+                            + "6E2E706E6716666E"
+                            + "3D4A6F686E6E7927"
+                            + "73204368726F6D65"
+                            + "636173740463613D"
+                            + "350473743D30095F"
+                            + "7365727669636573"
+                            + "075F646E732D7364"
+                            + "045F756470C03100"
+                            + "0C00010000119400"
+                            + "02C020C020000C00"
+                            + "01000011940002C0"
+                            + "0CC00C0021800100"
+                            + "000078001C000000"
+                            + "001F49134A6F686E"
+                            + "6E79277320436872"
+                            + "6F6D6563617374C0"
+                            + "31C0F30001800100"
+                            + "0000780004C0A864"
+                            + "68C0F3002F800100"
+                            + "0000780005C0F300"
+                            + "0140C00C002F8001"
+                            + "000011940009C00C"
+                            + "00050000800040");
+                    if (enableUnicastResponse.get()) {
+                        DatagramPacket packet = invocationOnMock.getArgument(0);
+                        packet.setData(dataIn);
+                    }
+                    return null;
+                })
+                .when(mockUnicastSocket)
+                .receive(any(DatagramPacket.class));
+    }
+
+    @After
+    public void tearDown() {
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testSendPackets_useSeparateSocketForUnicast()
+            throws InterruptedException, IOException {
+        //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+        //        .override(DateUtils.SECOND_IN_MILLIS);
+        mdnsClient.startDiscovery();
+        Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+        Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+        Thread sendThread = mdnsClient.sendThread;
+
+        assertTrue(multicastReceiverThread.isAlive());
+        assertTrue(sendThread.isAlive());
+        assertTrue(unicastReceiverThread.isAlive());
+
+        // Sends a packet.
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendMulticastPacket(packet);
+        // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+        // it may not be called yet. So timeout is added.
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Verify the packet is sent by the unicast socket.
+        mdnsClient.sendUnicastPacket(packet);
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
+
+        // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+        // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+        // for a long time (the foreground thread can fail the test early).
+        final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+        Thread testThread =
+                new Thread(
+                        new Runnable() {
+                            @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+                            @Override
+                            public void run() {
+                                mdnsClient.stopDiscovery();
+                                stopDiscoveryLatch.countDown();
+                            }
+                        });
+        testThread.start();
+        assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+        // We should be able to join in a reasonable amount of time, to prove that the
+        // the MdnsClient exited without sending the large queue of packets.
+        testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+        assertFalse(multicastReceiverThread.isAlive());
+        assertFalse(sendThread.isAlive());
+        assertFalse(unicastReceiverThread.isAlive());
+    }
+
+    @Test
+    public void testSendPackets_useSameSocketForMulticastAndUnicast()
+            throws InterruptedException, IOException {
+        mdnsClient.startDiscovery();
+        Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+        Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+        Thread sendThread = mdnsClient.sendThread;
+
+        assertTrue(multicastReceiverThread.isAlive());
+        assertTrue(sendThread.isAlive());
+        assertNull(unicastReceiverThread);
+
+        // Sends a packet.
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendMulticastPacket(packet);
+        // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+        // it may not be called yet. So timeout is added.
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Verify the packet is sent by the multicast socket as well.
+        mdnsClient.sendUnicastPacket(packet);
+        verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
+        verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+        // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+        // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+        // for a long time (the foreground thread can fail the test early).
+        final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+        Thread testThread =
+                new Thread(
+                        new Runnable() {
+                            @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+                            @Override
+                            public void run() {
+                                mdnsClient.stopDiscovery();
+                                stopDiscoveryLatch.countDown();
+                            }
+                        });
+        testThread.start();
+        assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+        // We should be able to join in a reasonable amount of time, to prove that the
+        // the MdnsClient exited without sending the large queue of packets.
+        testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+        assertFalse(multicastReceiverThread.isAlive());
+        assertFalse(sendThread.isAlive());
+        assertNull(unicastReceiverThread);
+    }
+
+    @Test
+    public void testStartStop() throws IOException {
+        for (int i = 0; i < 5; i++) {
+            mdnsClient.startDiscovery();
+
+            Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+            Thread socketThread = mdnsClient.sendThread;
+
+            assertTrue(multicastReceiverThread.isAlive());
+            assertTrue(socketThread.isAlive());
+
+            mdnsClient.stopDiscovery();
+
+            assertFalse(multicastReceiverThread.isAlive());
+            assertFalse(socketThread.isAlive());
+        }
+    }
+
+    @Test
+    public void testStopDiscovery_queueIsCleared() throws IOException {
+        mdnsClient.startDiscovery();
+        mdnsClient.stopDiscovery();
+        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+        }
+    }
+
+    @Test
+    public void testSendPacket_afterDiscoveryStops() throws IOException {
+        mdnsClient.startDiscovery();
+        mdnsClient.stopDiscovery();
+        mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+        }
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testSendPacket_queueReachesSizeLimit() throws IOException {
+        //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
+        mdnsClient.startDiscovery();
+        for (int i = 0; i < 100; i++) {
+            mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+        }
+
+        synchronized (mdnsClient.multicastPacketQueue) {
+            assertTrue(mdnsClient.multicastPacketQueue.size() <= 2);
+        }
+    }
+
+    @Test
+    public void testMulticastResponseReceived_useSeparateSocketForUnicast() throws IOException {
+        mdnsClient.setCallback(mockCallback);
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onResponseReceived(any(MdnsResponse.class));
+    }
+
+    @Test
+    public void testMulticastResponseReceived_useSameSocketForMulticastAndUnicast()
+            throws Exception {
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
+                .onResponseReceived(any(MdnsResponse.class));
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    public void testFailedToParseMdnsResponse_useSeparateSocketForUnicast() throws IOException {
+        mdnsClient.setCallback(mockCallback);
+
+        // Both multicast socket and unicast socket receive malformed responses.
+        byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    // Malformed data.
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    // Malformed data.
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockUnicastSocket)
+                .receive(any(DatagramPacket.class));
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onFailedToParseMdnsResponse(anyInt(), eq(MdnsResponseErrorCode.ERROR_END_OF_FILE));
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    public void testFailedToParseMdnsResponse_useSameSocketForMulticastAndUnicast()
+            throws IOException {
+        doAnswer(
+                (InvocationOnMock invocationOnMock) -> {
+                    final byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+                    DatagramPacket packet = invocationOnMock.getArgument(0);
+                    packet.setData(dataIn);
+                    return null;
+                })
+                .when(mockMulticastSocket)
+                .receive(any(DatagramPacket.class));
+
+        mdnsClient.startDiscovery();
+
+        verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+                .onFailedToParseMdnsResponse(1, MdnsResponseErrorCode.ERROR_END_OF_FILE);
+
+        mdnsClient.stopDiscovery();
+    }
+
+    @Test
+    @Ignore("MdnsConfigs is not configurable currently.")
+    public void testMulticastResponseIsNotReceived() throws IOException, InterruptedException {
+        //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+        //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+        //        .override(DateUtils.SECOND_IN_MILLIS);
+        //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+        enableMulticastResponse.set(false);
+        enableUnicastResponse.set(true);
+
+        mdnsClient.startDiscovery();
+        DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendMulticastPacket(packet);
+
+        // Wait for the timer to be triggered.
+        Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertTrue(mdnsClient.receivedUnicastResponse);
+        assertTrue(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        // Allow multicast response and verify the states again.
+        enableMulticastResponse.set(true);
+        Thread.sleep(DateUtils.SECOND_IN_MILLIS);
+
+        // Verify cannotReceiveMulticastResponse is reset to false.
+        assertTrue(mdnsClient.receivedMulticastResponse);
+        assertTrue(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        // Stop the discovery and start a new session. Don't respond the unicsat query either in
+        // this session.
+        enableMulticastResponse.set(false);
+        enableUnicastResponse.set(false);
+        mdnsClient.stopDiscovery();
+        mdnsClient.startDiscovery();
+
+        // Verify the states are reset.
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertFalse(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+        mdnsClient.sendUnicastPacket(packet);
+        mdnsClient.sendMulticastPacket(packet);
+        Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+        // Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
+        // unicast response either. This is expected for users who don't have any cast device.
+        assertFalse(mdnsClient.receivedMulticastResponse);
+        assertFalse(mdnsClient.receivedUnicastResponse);
+        assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
new file mode 100644
index 0000000..9f11a4b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
@@ -0,0 +1,172 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsSocket}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketTests {
+
+    @Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
+    @Mock private MulticastSocket mockMulticastSocket;
+    @Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
+    private SocketAddress socketIPv4Address;
+    private SocketAddress socketIPv6Address;
+
+    private byte[] data = new byte[25];
+    private final DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+    private NetworkInterface networkInterface;
+
+    private MdnsSocket mdnsSocket;
+
+    @Before
+    public void setUp() throws SocketException, UnknownHostException {
+        MockitoAnnotations.initMocks(this);
+
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+        socketIPv4Address = new InetSocketAddress(
+                InetAddress.getByName("224.0.0.251"), MdnsConstants.MDNS_PORT);
+        socketIPv6Address = new InetSocketAddress(
+                InetAddress.getByName("FF02::FB"), MdnsConstants.MDNS_PORT);
+    }
+
+    @Test
+    public void testMdnsSocket() throws IOException {
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+        mdnsSocket.send(datagramPacket);
+        verify(mockMulticastSocket).setNetworkInterface(networkInterface);
+        verify(mockMulticastSocket).send(datagramPacket);
+
+        mdnsSocket.receive(datagramPacket);
+        verify(mockMulticastSocket).receive(datagramPacket);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv4Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv4Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    @Test
+    public void testIPv6OnlyNetwork_IPv6Enabled() throws IOException {
+        // Have mockMulticastNetworkInterfaceProvider send back an IPv6Only networkInterfaceWrapper
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+
+        when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+                Collections.singletonList(mockNetworkInterfaceWrapper)))
+                .thenReturn(true);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    @Test
+    public void testIPv6OnlyNetwork_IPv6Toggle() throws IOException {
+        // Have mockMulticastNetworkInterfaceProvider send back a networkInterfaceWrapper
+        networkInterface = createEmptyNetworkInterface();
+        when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+        when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+                .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+        mdnsSocket =
+                new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+                    @Override
+                    MulticastSocket createMulticastSocket(int port) throws IOException {
+                        return mockMulticastSocket;
+                    }
+                };
+
+        when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+                Collections.singletonList(mockNetworkInterfaceWrapper)))
+                .thenReturn(true);
+
+        mdnsSocket.joinGroup();
+        verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.leaveGroup();
+        verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+        mdnsSocket.close();
+        verify(mockMulticastSocket).close();
+    }
+
+    private NetworkInterface createEmptyNetworkInterface() {
+        try {
+            Constructor<NetworkInterface> constructor =
+                    NetworkInterface.class.getDeclaredConstructor();
+            constructor.setAccessible(true);
+            return constructor.newInstance();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
new file mode 100644
index 0000000..2268dfe
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
@@ -0,0 +1,275 @@
+/*
+ * 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.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MulticastNetworkInterfaceProvider}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MulticastNetworkInterfaceProviderTests {
+
+    @Mock private NetworkInterfaceWrapper loopbackInterface;
+    @Mock private NetworkInterfaceWrapper pointToPointInterface;
+    @Mock private NetworkInterfaceWrapper virtualInterface;
+    @Mock private NetworkInterfaceWrapper inactiveMulticastInterface;
+    @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterface;
+    @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterfaceTwo;
+    @Mock private NetworkInterfaceWrapper nonMulticastInterface;
+    @Mock private NetworkInterfaceWrapper multicastInterfaceOne;
+    @Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
+
+    private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
+    private MulticastNetworkInterfaceProvider provider;
+    private Context context;
+
+    @Before
+    public void setUp() throws SocketException, UnknownHostException {
+        MockitoAnnotations.initMocks(this);
+        context = InstrumentationRegistry.getContext();
+
+        setupNetworkInterface(
+                loopbackInterface,
+                true /* isUp */,
+                true /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                pointToPointInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                true /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                virtualInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                true /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                inactiveMulticastInterface,
+                false /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                nonMulticastInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                false /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                activeIpv6MulticastInterface,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                true /* isIpv6 */);
+
+        setupNetworkInterface(
+                activeIpv6MulticastInterfaceTwo,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                true /* isIpv6 */);
+
+        setupNetworkInterface(
+                multicastInterfaceOne,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        setupNetworkInterface(
+                multicastInterfaceTwo,
+                true /* isUp */,
+                false /* isLoopBack */,
+                false /* isPointToPoint */,
+                false /* isVirtual */,
+                true /* supportsMulticast */,
+                false /* isIpv6 */);
+
+        provider =
+                new MulticastNetworkInterfaceProvider(context) {
+                    @Override
+                    List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+                        return networkInterfaces;
+                    }
+                };
+    }
+
+    @Test
+    public void testGetMulticastNetworkInterfaces() {
+        // getNetworkInterfaces returns 1 multicast interface and 5 interfaces that can not be used
+        // to send and receive multicast packets.
+        networkInterfaces.add(loopbackInterface);
+        networkInterfaces.add(pointToPointInterface);
+        networkInterfaces.add(virtualInterface);
+        networkInterfaces.add(inactiveMulticastInterface);
+        networkInterfaces.add(nonMulticastInterface);
+        networkInterfaces.add(multicastInterfaceOne);
+
+        assertEquals(Collections.singletonList(multicastInterfaceOne),
+                provider.getMulticastNetworkInterfaces());
+
+        // getNetworkInterfaces returns 2 multicast interfaces after a connectivity change.
+        networkInterfaces.clear();
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(multicastInterfaceTwo);
+
+        provider.connectivityMonitor.notifyConnectivityChange();
+
+        assertEquals(networkInterfaces, provider.getMulticastNetworkInterfaces());
+    }
+
+    @Test
+    public void testStartWatchingConnectivityChanges() {
+        ConnectivityMonitor mockMonitor = mock(ConnectivityMonitor.class);
+        provider.connectivityMonitor = mockMonitor;
+
+        InOrder inOrder = inOrder(mockMonitor);
+
+        provider.startWatchingConnectivityChanges();
+        inOrder.verify(mockMonitor).startWatchingConnectivityChanges();
+
+        provider.stopWatchingConnectivityChanges();
+        inOrder.verify(mockMonitor).stopWatchingConnectivityChanges();
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_EmptyNetwork() {
+        // getNetworkInterfaces returns no network interfaces.
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv4Only() {
+        // getNetworkInterfaces returns two IPv4 network interface.
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(multicastInterfaceTwo);
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_MixedNetwork() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        networkInterfaces.add(multicastInterfaceOne);
+        networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+        networkInterfaces.add(multicastInterfaceTwo);
+        assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv6Only() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+        assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+    }
+
+    @Test
+    public void testIpV6OnlyNetwork_IPv6Enabled() {
+        // getNetworkInterfaces returns one IPv6 network interface.
+        networkInterfaces.add(activeIpv6MulticastInterface);
+        assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+
+        final List<NetworkInterfaceWrapper> interfaces = provider.getMulticastNetworkInterfaces();
+        assertEquals(Collections.singletonList(activeIpv6MulticastInterface), interfaces);
+    }
+
+    private void setupNetworkInterface(
+            @NonNull NetworkInterfaceWrapper networkInterfaceWrapper,
+            boolean isUp,
+            boolean isLoopback,
+            boolean isPointToPoint,
+            boolean isVirtual,
+            boolean supportsMulticast,
+            boolean isIpv6)
+            throws SocketException, UnknownHostException {
+        when(networkInterfaceWrapper.isUp()).thenReturn(isUp);
+        when(networkInterfaceWrapper.isLoopback()).thenReturn(isLoopback);
+        when(networkInterfaceWrapper.isPointToPoint()).thenReturn(isPointToPoint);
+        when(networkInterfaceWrapper.isVirtual()).thenReturn(isVirtual);
+        when(networkInterfaceWrapper.supportsMulticast()).thenReturn(supportsMulticast);
+        if (isIpv6) {
+            InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+            InetAddress ip6Address = Inet6Address.getByName("2001:4860:0:1001::68");
+            when(interfaceAddress.getAddress()).thenReturn(ip6Address);
+            when(networkInterfaceWrapper.getInterfaceAddresses())
+                    .thenReturn(Collections.singletonList(interfaceAddress));
+        } else {
+            Inet4Address ip = (Inet4Address) Inet4Address.getByName("192.168.0.1");
+            InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+            when(interfaceAddress.getAddress()).thenReturn(ip);
+            when(networkInterfaceWrapper.getInterfaceAddresses())
+                    .thenReturn(Collections.singletonList(interfaceAddress));
+        }
+    }
+}
\ No newline at end of file