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