Merge "Refactor BpfNetMaps and getChainEnabled"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4eeaf51..4774866 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -171,23 +171,6 @@
]
}
],
- "auto-postsubmit": [
- // Test tag for automotive targets. These are only running in postsubmit so as to harden the
- // automotive targets to avoid introducing additional test flake and build time. The plan for
- // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
- // Additionally, this tag is used in targeted test suites to limit resource usage on the test
- // infra during the hardening phase.
- // TODO: this tag to be removed once the above is no longer an issue.
- {
- "name": "FrameworksNetTests"
- },
- {
- "name": "FrameworksNetIntegrationTests"
- },
- {
- "name": "FrameworksNetDeflakeTest"
- }
- ],
"imports": [
{
"path": "frameworks/base/core/java/android/net"
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index c403548..1368eee 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -944,6 +944,8 @@
* be allowed to be accessed on the handler thread.
*/
public void dump(@NonNull IndentingPrintWriter pw) {
+ // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
+ // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
pw.println("Stats provider " + (mStatsProvider != null
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index af017f3..f613b73 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1406,7 +1406,9 @@
private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
boolean isNcm) {
ensureIpServerStarted(ifname, tetheringType, isNcm);
- changeInterfaceState(ifname, ipServingMode);
+ if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start tethering on iface " + ifname);
+ }
}
private void disableWifiIpServingCommon(int tetheringType, String ifname) {
@@ -1551,27 +1553,6 @@
}
}
- private void changeInterfaceState(String ifname, int requestedState) {
- final int result;
- switch (requestedState) {
- case IpServer.STATE_UNAVAILABLE:
- case IpServer.STATE_AVAILABLE:
- result = untether(ifname);
- break;
- case IpServer.STATE_TETHERED:
- case IpServer.STATE_LOCAL_ONLY:
- result = tether(ifname, requestedState);
- break;
- default:
- Log.wtf(TAG, "Unknown interface state: " + requestedState);
- return;
- }
- if (result != TETHER_ERROR_NO_ERROR) {
- Log.e(TAG, "unable start or stop tethering on iface " + ifname);
- return;
- }
- }
-
TetheringConfiguration getTetheringConfiguration() {
return mConfig;
}
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index e25f2ae..d8e631e 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -69,7 +69,6 @@
/** Update Tethering stats about caller's package name and downstream type. */
public void createBuilder(final int downstreamType, final String callerPkg) {
- mBuilderMap.clear();
NetworkTetheringReported.Builder statsBuilder =
NetworkTetheringReported.newBuilder();
statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 819936d..7ccb7f5 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -1125,12 +1125,18 @@
@IgnoreUpTo(Build.VERSION_CODES.R)
public void testTetherUdpV4AfterR() throws Exception {
final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
- boolean usingBpf = isUdpOffloadSupportedByKernel(kernelVersion);
- if (!usingBpf) {
+ final boolean isUdpOffloadSupported = isUdpOffloadSupportedByKernel(kernelVersion);
+ if (!isUdpOffloadSupported) {
Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
+ kernelVersion);
}
- runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)), usingBpf);
+ final boolean isTetherConfigBpfOffloadEnabled = isTetherConfigBpfOffloadEnabled();
+ if (!isTetherConfigBpfOffloadEnabled) {
+ Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test "
+ + "because tethering config doesn't enable BPF offload.");
+ }
+ runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
+ isUdpOffloadSupported && isTetherConfigBpfOffloadEnabled);
}
@Nullable
@@ -1189,6 +1195,21 @@
return null;
}
+ private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+ final String dumpStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short");
+
+ // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+ // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+ // RRO to override the enabled default value. Get the tethering config via dumpsys.
+ // $ dumpsys tethering
+ // mIsBpfEnabled: true
+ boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+ if (!enabled) {
+ Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+ }
+ return enabled;
+ }
+
@NonNull
private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
throws Exception {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index c34cf5f..6a85718 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -81,6 +81,22 @@
mTetheringMetrics = spy(new MockTetheringMetrics());
}
+ private void verifyReport(DownstreamType downstream, ErrorCode error, UserType user)
+ throws Exception {
+ final NetworkTetheringReported expectedReport =
+ mStatsBuilder.setDownstreamType(downstream)
+ .setUserType(user)
+ .setUpstreamType(UpstreamType.UT_UNKNOWN)
+ .setErrorCode(error)
+ .build();
+ verify(mTetheringMetrics).write(expectedReport);
+ }
+
+ private void updateErrorAndSendReport(int downstream, int error) {
+ mTetheringMetrics.updateErrorCode(downstream, error);
+ mTetheringMetrics.sendReport(downstream);
+ }
+
private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
throws Exception {
for (Pair<Integer, DownstreamType> testPair : testPairs) {
@@ -88,15 +104,8 @@
final DownstreamType expectedResult = testPair.second;
mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
- mTetheringMetrics.updateErrorCode(type, TETHER_ERROR_NO_ERROR);
- mTetheringMetrics.sendReport(type);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(expectedResult)
- .setUserType(UserType.USER_UNKNOWN)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(ErrorCode.EC_NO_ERROR)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(type, TETHER_ERROR_NO_ERROR);
+ verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN);
reset(mTetheringMetrics);
}
}
@@ -118,15 +127,8 @@
final ErrorCode expectedResult = testPair.second;
mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.updateErrorCode(TETHERING_WIFI, errorCode);
- mTetheringMetrics.sendReport(TETHERING_WIFI);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
- .setUserType(UserType.USER_UNKNOWN)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(expectedResult)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(TETHERING_WIFI, errorCode);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN);
reset(mTetheringMetrics);
}
}
@@ -163,15 +165,8 @@
final UserType expectedResult = testPair.second;
mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
- mTetheringMetrics.updateErrorCode(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
- mTetheringMetrics.sendReport(TETHERING_WIFI);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
- .setUserType(expectedResult)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(ErrorCode.EC_NO_ERROR)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult);
reset(mTetheringMetrics);
}
}
@@ -183,4 +178,23 @@
new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
new Pair<>(GMS_PKG, UserType.USER_GMS));
}
+
+ @Test
+ public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
+ mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_DHCPSERVER_ERROR,
+ UserType.USER_SETTINGS);
+
+ updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_ENABLE_FORWARDING_ERROR,
+ UserType.USER_SYSTEMUI);
+
+ updateErrorAndSendReport(TETHERING_BLUETOOTH, TETHER_ERROR_TETHER_IFACE_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_BLUETOOTH, ErrorCode.EC_TETHER_IFACE_ERROR,
+ UserType.USER_GMS);
+ }
}
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 7b18765..9cae9e6 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -236,6 +236,8 @@
/**
* Create a tap interface with or without carrier for testing purposes.
*
+ * Note: setting carrierUp = false is not supported until kernel version 5.0.
+ *
* @param carrierUp whether the created interface has a carrier or not.
* @param bringUp whether to bring up the interface before returning it.
* @hide
@@ -254,7 +256,6 @@
* Enable / disable carrier on TestNetworkInterface
*
* Note: TUNSETCARRIER is not supported until kernel version 5.0.
- * TODO: add RequiresApi annotation.
*
* @param iface the interface to configure.
* @param enabled true to turn carrier on, false to turn carrier off.
diff --git a/service/Android.bp b/service/Android.bp
index 45e43bc..c2dbce1 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -181,6 +181,25 @@
],
}
+// TODO: Remove this temporary library and put code into module when test coverage is enough.
+java_library {
+ name: "service-mdns",
+ sdk_version: "system_server_current",
+ min_sdk_version: "30",
+ srcs: [
+ "mdns/**/*.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity-pre-jarjar",
+ "framework-wifi.stubs.module_lib",
+ "service-connectivity-pre-jarjar",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity/tests:__subpackages__",
+ ],
+}
+
java_library {
name: "service-connectivity-protos",
sdk_version: "system_current",
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
new file mode 100644
index 0000000..2b99d0a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/** Interface for monitoring connectivity changes. */
+public interface ConnectivityMonitor {
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ void startWatchingConnectivityChanges();
+
+ /** Stops monitoring changes of connectivity. */
+ void stopWatchingConnectivityChanges();
+
+ void notifyConnectivityChange();
+
+ /** Listener interface for receiving connectivity changes. */
+ interface Listener {
+ void onConnectivityChanged();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
new file mode 100644
index 0000000..3563d61
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
+public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
+ private static final String TAG = "ConnMntrWConnMgr";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+
+ private final Listener listener;
+ private final ConnectivityManager.NetworkCallback networkCallback;
+ private final ConnectivityManager connectivityManager;
+ // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
+ // the receiver is unregistered twice yet.
+ private boolean isCallbackRegistered = false;
+
+ @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+ this.listener = listener;
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ LOGGER.log("network available.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onLost(Network network) {
+ LOGGER.log("network lost.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onUnavailable() {
+ LOGGER.log("network unavailable.");
+ notifyConnectivityChange();
+ }
+ };
+ }
+
+ @Override
+ public void notifyConnectivityChange() {
+ listener.onConnectivityChanged();
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void startWatchingConnectivityChanges() {
+ LOGGER.log("Start watching connectivity changes");
+ if (isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.registerNetworkCallback(
+ new NetworkRequest.Builder().addTransportType(
+ NetworkCapabilities.TRANSPORT_WIFI).build(),
+ networkCallback);
+ isCallbackRegistered = true;
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void stopWatchingConnectivityChanges() {
+ LOGGER.log("Stop watching connectivity changes");
+ if (!isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.unregisterNetworkCallback(networkCallback);
+ isCallbackRegistered = false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
new file mode 100644
index 0000000..3db1b22
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
+ * query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
+ * and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
+ * it can not be enqueued, then call to {@link #call()} returns {@code null}.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
+
+ private static final String TAG = "MdnsQueryCallable";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final List<Integer> castShellEmulatorMdnsPorts;
+
+ static {
+ castShellEmulatorMdnsPorts = new ArrayList<>();
+ String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
+
+ for (String port : stringPorts) {
+ try {
+ castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
+ } catch (NumberFormatException e) {
+ // Ignore.
+ }
+ }
+ }
+
+ private final WeakReference<MdnsSocketClient> weakRequestSender;
+ private final MdnsPacketWriter packetWriter;
+ private final String[] serviceTypeLabels;
+ private final List<String> subtypes;
+ private final boolean expectUnicastResponse;
+ private final int transactionId;
+
+ EnqueueMdnsQueryCallable(
+ @NonNull MdnsSocketClient requestSender,
+ @NonNull MdnsPacketWriter packetWriter,
+ @NonNull String serviceType,
+ @NonNull Collection<String> subtypes,
+ boolean expectUnicastResponse,
+ int transactionId) {
+ weakRequestSender = new WeakReference<>(requestSender);
+ this.packetWriter = packetWriter;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ this.subtypes = new ArrayList<>(subtypes);
+ this.expectUnicastResponse = expectUnicastResponse;
+ this.transactionId = transactionId;
+ }
+
+ @Override
+ public Pair<Integer, List<String>> call() {
+ try {
+ MdnsSocketClient requestSender = weakRequestSender.get();
+ if (requestSender == null) {
+ return null;
+ }
+
+ int numQuestions = 1;
+ if (!subtypes.isEmpty()) {
+ numQuestions += subtypes.size();
+ }
+
+ // Header.
+ packetWriter.writeUInt16(transactionId); // transaction ID
+ packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
+ packetWriter.writeUInt16(numQuestions); // number of questions
+ packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
+ packetWriter.writeUInt16(0); // number of authority entries
+ packetWriter.writeUInt16(0); // number of additional records
+
+ // Question(s). There will be one question for each (fqdn+subtype, recordType)
+ // combination,
+ // as well as one for each (fqdn, recordType) combination.
+
+ for (String subtype : subtypes) {
+ String[] labels = new String[serviceTypeLabels.length + 2];
+ labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+ labels[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+ packetWriter.writeLabels(labels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+ }
+
+ packetWriter.writeLabels(serviceTypeLabels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+
+ InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
+ if (requestSender.isOnIPv6OnlyNetwork()) {
+ mdnsAddress = MdnsConstants.getMdnsIPv6Address();
+ }
+
+ sendPacketTo(requestSender,
+ new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
+ for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+ sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
+ }
+ return Pair.create(transactionId, subtypes);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+ TextUtils.join(",", subtypes)), e);
+ return null;
+ }
+ }
+
+ private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
+ throws IOException {
+ DatagramPacket packet = packetWriter.getPacket(address);
+ if (expectUnicastResponse) {
+ requestSender.sendUnicastPacket(packet);
+ } else {
+ requestSender.sendMulticastPacket(packet);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
new file mode 100644
index 0000000..72b65e0
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
+ * instances, and provides method to shutdown all the created executors.
+ */
+public class ExecutorProvider {
+
+ private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
+ new ArraySet<>();
+
+ /** Returns a new {@link ScheduledExecutorService} instance. */
+ public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
+ // TODO: actually use a pool ?
+ ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
+ serviceTypeClientSchedulerExecutors.add(executor);
+ return executor;
+ }
+
+ /** Shuts down all the created {@link ScheduledExecutorService} instances. */
+ public void shutdownAll() {
+ for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+ executor.shutdownNow();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
new file mode 100644
index 0000000..922037b
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * mDNS configuration values.
+ *
+ * TODO: consider making some of these adjustable via flags.
+ */
+public class MdnsConfigs {
+ public static String[] castShellEmulatorMdnsPorts() {
+ return new String[0];
+ }
+
+ public static long initialTimeBetweenBurstsMs() {
+ return 5000L;
+ }
+
+ public static long timeBetweenBurstsMs() {
+ return 20_000L;
+ }
+
+ public static int queriesPerBurst() {
+ return 3;
+ }
+
+ public static long timeBetweenQueriesInBurstMs() {
+ return 1000L;
+ }
+
+ public static int queriesPerBurstPassive() {
+ return 1;
+ }
+
+ public static boolean alwaysAskForUnicastResponseInEachBurst() {
+ return false;
+ }
+
+ public static boolean useSessionIdToScheduleMdnsTask() {
+ return false;
+ }
+
+ public static boolean shouldCancelScanTaskWhenFutureIsNull() {
+ return false;
+ }
+
+ public static long sleepTimeForSocketThreadMs() {
+ return 20_000L;
+ }
+
+ public static boolean checkMulticastResponse() {
+ return false;
+ }
+
+ public static boolean useSeparateSocketToSendUnicastQuery() {
+ return false;
+ }
+
+ public static long checkMulticastResponseIntervalMs() {
+ return 10_000L;
+ }
+
+ public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
+ return true;
+ }
+
+ public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
+ return false;
+ }
+
+ public static int mdnsPacketQueueMaxSize() {
+ return Integer.MAX_VALUE;
+ }
+
+ public static boolean preferIpv6() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
new file mode 100644
index 0000000..ed28700
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/** mDNS-related constants. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public final class MdnsConstants {
+ public static final int MDNS_PORT = 5353;
+ // Flags word format is:
+ // 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+ // QR [ Opcode ] AA TC RD RA Z AD CD [ Rcode ]
+ // See http://www.networksorcery.com/enp/protocol/dns.htm
+ // For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
+ // should be 0.
+ public static final int FLAGS_QUERY = 0x0000;
+ public static final int FLAGS_RESPONSE_MASK = 0xF80F;
+ public static final int FLAGS_RESPONSE = 0x8000;
+ public static final int QCLASS_INTERNET = 0x0001;
+ public static final int QCLASS_UNICAST = 0x8000;
+ public static final String SUBTYPE_LABEL = "_sub";
+ public static final String SUBTYPE_PREFIX = "_";
+ private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
+ private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ private static InetAddress mdnsAddress;
+ private static Charset utf8Charset;
+ private MdnsConstants() {
+ }
+
+ public static InetAddress getMdnsIPv4Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static InetAddress getMdnsIPv6Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static Charset getUtf8Charset() {
+ synchronized (MdnsConstants.class) {
+ if (utf8Charset == null) {
+ utf8Charset = getUtf8CharsetOnKitKat();
+ }
+ return utf8Charset;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static Charset getUtf8CharsetOnKitKat() {
+ return StandardCharsets.UTF_8;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
new file mode 100644
index 0000000..1faa6ce
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
+ * notify them when a mDNS service instance is found, updated, or removed?
+ */
+public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
+
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
+
+ private final ExecutorProvider executorProvider;
+ private final MdnsSocketClient socketClient;
+
+ private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
+
+ public MdnsDiscoveryManager(
+ @NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
+ this.executorProvider = executorProvider;
+ this.socketClient = socketClient;
+ }
+
+ /**
+ * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
+ * {@code listener} for receiving mDNS service discovery responses.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
+ * serviceType}.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void registerListener(
+ @NonNull String serviceType,
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ LOGGER.log(
+ "Registering listener for subtypes: %s",
+ TextUtils.join(",", searchOptions.getSubtypes()));
+ if (serviceTypeClients.isEmpty()) {
+ // First listener. Starts the socket client.
+ try {
+ socketClient.startDiscovery();
+ } catch (IOException e) {
+ LOGGER.e("Failed to start discover.", e);
+ return;
+ }
+ }
+ // All listeners of the same service types shares the same MdnsServiceTypeClient.
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ serviceTypeClient = createServiceTypeClient(serviceType);
+ serviceTypeClients.put(serviceType, serviceTypeClient);
+ }
+ serviceTypeClient.startSendAndReceive(listener, searchOptions);
+ }
+
+ /**
+ * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
+ * registered for the given service type, stops discovery for the service type.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void unregisterListener(
+ @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+ LOGGER.log("Unregistering listener for service type: %s", serviceType);
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ return;
+ }
+ if (serviceTypeClient.stopSendAndReceive(listener)) {
+ // No listener is registered for the service type anymore, remove it from the list of
+ // the
+ // service type clients.
+ serviceTypeClients.remove(serviceType);
+ if (serviceTypeClients.isEmpty()) {
+ // No discovery request. Stops the socket client.
+ socketClient.stopDiscovery();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
+ String[] name =
+ response.getPointerRecords().isEmpty()
+ ? null
+ : response.getPointerRecords().get(0).getName();
+ if (name != null) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ String[] serviceType = serviceTypeClient.getServiceTypeLabels();
+ if ((Arrays.equals(name, serviceType)
+ || ((name.length == (serviceType.length + 2))
+ && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(serviceType, name)))) {
+ serviceTypeClient.processResponse(response);
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+ return new MdnsServiceTypeClient(
+ serviceType, socketClient,
+ executorProvider.newServiceTypeClientSchedulerExecutor());
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
new file mode 100644
index 0000000..e35743c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsInetAddressRecord extends MdnsRecord {
+ private Inet6Address inet6Address;
+ private Inet4Address inet4Address;
+
+ /**
+ * Constructs the {@link MdnsRecord}
+ *
+ * @param name the service host name
+ * @param type the type of record (either Type 'AAAA' or Type 'A')
+ * @param reader the reader to read the record from.
+ */
+ public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
+ throws IOException {
+ super(name, type, reader);
+ }
+
+ /** Returns the IPv6 address. */
+ public Inet6Address getInet6Address() {
+ return inet6Address;
+ }
+
+ /** Returns the IPv4 address. */
+ public Inet4Address getInet4Address() {
+ return inet4Address;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ int size = 4;
+ if (super.getType() == MdnsRecord.TYPE_AAAA) {
+ size = 16;
+ }
+ byte[] buf = new byte[size];
+ reader.readBytes(buf);
+ try {
+ InetAddress address = InetAddress.getByAddress(buf);
+ if (address instanceof Inet4Address) {
+ inet4Address = (Inet4Address) address;
+ inet6Address = null;
+ } else if (address instanceof Inet6Address) {
+ inet4Address = null;
+ inet6Address = (Inet6Address) address;
+ } else {
+ inet4Address = null;
+ inet6Address = null;
+ }
+ } catch (UnknownHostException e) {
+ // Ignore exception
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ byte[] buf = null;
+ if (inet4Address != null) {
+ buf = inet4Address.getAddress();
+ } else if (inet6Address != null) {
+ buf = inet6Address.getAddress();
+ }
+ if (buf != null) {
+ writer.writeBytes(buf);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String type = "AAAA";
+ if (super.getType() == MdnsRecord.TYPE_A) {
+ type = "A";
+ }
+ return String.format(
+ Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
+ inet6Address);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hashCode(inet4Address)
+ + Objects.hashCode(inet6Address);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsInetAddressRecord)) {
+ return false;
+ }
+
+ return super.equals(other)
+ && Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
+ && Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
new file mode 100644
index 0000000..61c5f5a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.SparseArray;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Simple decoder for mDNS packets. */
+public class MdnsPacketReader {
+ private final byte[] buf;
+ private final int count;
+ private final SparseArray<LabelEntry> labelDictionary;
+ private int pos;
+ private int limit;
+
+ /** Constructs a reader for the given packet. */
+ public MdnsPacketReader(DatagramPacket packet) {
+ buf = packet.getData();
+ count = packet.getLength();
+ pos = 0;
+ limit = -1;
+ labelDictionary = new SparseArray<>(16);
+ }
+
+ /**
+ * Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
+ * read past this limit will result in an EOFException.
+ *
+ * @param limit The new limit.
+ * @throws IOException If there is insufficient data for the new limit.
+ */
+ public void setLimit(int limit) throws IOException {
+ if (limit >= 0) {
+ if (pos + limit <= count) {
+ this.limit = pos + limit;
+ } else {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "attempt to set limit beyond available data: %d exceeds %d",
+ pos + limit,
+ count));
+ }
+ }
+ }
+
+ /** Clears the limit set by {@link #setLimit}. */
+ public void clearLimit() {
+ limit = -1;
+ }
+
+ /**
+ * Returns the number of bytes left to read, between the current read position and either the
+ * limit (if set) or the end of the packet.
+ */
+ public int getRemaining() {
+ return (limit >= 0 ? limit : count) - pos;
+ }
+
+ /**
+ * Reads an unsigned 8-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt8() throws EOFException {
+ checkRemaining(1);
+ byte val = buf[pos++];
+ return val & 0xFF;
+ }
+
+ /**
+ * Reads an unsigned 16-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt16() throws EOFException {
+ checkRemaining(2);
+ int val = (buf[pos++] & 0xFF) << 8;
+ val |= (buf[pos++]) & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads an unsigned 32-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public long readUInt32() throws EOFException {
+ checkRemaining(4);
+ long val = (long) (buf[pos++] & 0xFF) << 24;
+ val |= (long) (buf[pos++] & 0xFF) << 16;
+ val |= (long) (buf[pos++] & 0xFF) << 8;
+ val |= buf[pos++] & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
+ * either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
+ * pointer, or a pointer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ * @throws IOException If invalid data is read.
+ */
+ public String[] readLabels() throws IOException {
+ List<String> result = new ArrayList<>(5);
+ LabelEntry previousEntry = null;
+
+ while (getRemaining() > 0) {
+ byte nextByte = peekByte();
+
+ if (nextByte == 0) {
+ // A NUL byte terminates a sequence of labels.
+ skip(1);
+ break;
+ }
+
+ int currentOffset = pos;
+
+ boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
+ if (isLabelPointer) {
+ // A pointer terminates a sequence of labels. Store the pointer value in the
+ // previous label entry.
+ int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
+ if (previousEntry != null) {
+ previousEntry.nextOffset = labelOffset;
+ }
+
+ // Follow the chain of labels starting at this pointer, adding all of them onto the
+ // result.
+ while (labelOffset != 0) {
+ LabelEntry entry = labelDictionary.get(labelOffset);
+ if (entry == null) {
+ throw new IOException(
+ String.format(Locale.ROOT, "Invalid label pointer: %04X",
+ labelOffset));
+ }
+ result.add(entry.label);
+ labelOffset = entry.nextOffset;
+ }
+ break;
+ } else {
+ // It's an ordinary label. Chain it onto the previous label entry (if any), and add
+ // it onto the result.
+ String val = readString();
+ LabelEntry newEntry = new LabelEntry(val);
+ labelDictionary.put(currentOffset, newEntry);
+
+ if (previousEntry != null) {
+ previousEntry.nextOffset = currentOffset;
+ }
+ previousEntry = newEntry;
+ result.add(val);
+ }
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ /**
+ * Reads a length-prefixed string.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public String readString() throws EOFException {
+ int len = readUInt8();
+ checkRemaining(len);
+ String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
+ pos += len;
+ return val;
+ }
+
+ /**
+ * Reads a specific number of bytes.
+ *
+ * @param bytes The array to fill.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void readBytes(byte[] bytes) throws EOFException {
+ checkRemaining(bytes.length);
+ System.arraycopy(buf, pos, bytes, 0, bytes.length);
+ pos += bytes.length;
+ }
+
+ /**
+ * Skips over the given number of bytes.
+ *
+ * @param count The number of bytes to read and discard.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void skip(int count) throws EOFException {
+ checkRemaining(count);
+ pos += count;
+ }
+
+ /**
+ * Peeks at and returns the next byte in the packet, without advancing the read position.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public byte peekByte() throws EOFException {
+ checkRemaining(1);
+ return buf[pos];
+ }
+
+ /** Returns the current byte position of the reader for the data packet. */
+ public int getPosition() {
+ return pos;
+ }
+
+ // Checks if the number of remaining bytes to be read in the packet is at least |count|.
+ private void checkRemaining(int count) throws EOFException {
+ if (getRemaining() < count) {
+ throw new EOFException();
+ }
+ }
+
+ private static class LabelEntry {
+ public final String label;
+ public int nextOffset = 0;
+
+ public LabelEntry(String label) {
+ this.label = label;
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
new file mode 100644
index 0000000..2fed36d
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Simple encoder for mDNS packets. */
+public class MdnsPacketWriter {
+ private static final int MDNS_POINTER_MASK = 0xC000;
+ private final byte[] data;
+ private final Map<Integer, String[]> labelDictionary;
+ private int pos = 0;
+ private int savedWritePos = -1;
+
+ /**
+ * Constructs a writer for a new packet.
+ *
+ * @param maxSize The maximum size of a packet.
+ */
+ public MdnsPacketWriter(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("invalid size");
+ }
+
+ data = new byte[maxSize];
+ labelDictionary = new HashMap<>();
+ }
+
+ /** Returns the current write position. */
+ public int getWritePosition() {
+ return pos;
+ }
+
+ /**
+ * Saves the current write position and then rewinds the write position by the given number of
+ * bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
+ * nested.
+ *
+ * @param position The position to rewind to.
+ * @throws IOException If the count would go beyond the beginning of the packet, or if there is
+ * already a rewind in effect.
+ */
+ public void rewind(int position) throws IOException {
+ if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
+ throw new IOException("invalid rewind");
+ }
+
+ savedWritePos = pos;
+ pos = position;
+ }
+
+ /**
+ * Sets the current write position to what it was prior to the last rewind.
+ *
+ * @throws IOException If there was no rewind in effect.
+ */
+ public void unrewind() throws IOException {
+ if (savedWritePos == -1) {
+ throw new IOException("no rewind is in effect");
+ }
+ pos = savedWritePos;
+ savedWritePos = -1;
+ }
+
+ /** Clears any rewind state. */
+ public void clearRewind() {
+ savedWritePos = -1;
+ }
+
+ /**
+ * Writes an unsigned 8-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt8(int value) throws IOException {
+ checkRemaining(1);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 16-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt16(int value) throws IOException {
+ checkRemaining(2);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 32-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt32(long value) throws IOException {
+ checkRemaining(4);
+ data[pos++] = (byte) ((value >>> 24) & 0xFF);
+ data[pos++] = (byte) ((value >>> 16) & 0xFF);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes a specific number of bytes.
+ *
+ * @param data The array to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeBytes(byte[] data) throws IOException {
+ checkRemaining(data.length);
+ System.arraycopy(data, 0, this.data, pos, data.length);
+ pos += data.length;
+ }
+
+ /**
+ * Writes a string.
+ *
+ * @param value The string to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeString(String value) throws IOException {
+ byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
+ writeUInt8(utf8.length);
+ writeBytes(utf8);
+ }
+
+ /**
+ * Writes a series of labels. Uses name compression.
+ *
+ * @param labels The labels to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeLabels(String[] labels) throws IOException {
+ // See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
+ // of the name compression method used here.
+
+ int suffixLength = 0;
+ int suffixPointer = 0;
+
+ for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
+ int existingOffset = entry.getKey();
+ String[] existingLabels = entry.getValue();
+
+ if (Arrays.equals(existingLabels, labels)) {
+ writePointer(existingOffset);
+ return;
+ } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
+ // Keep track of the longest matching suffix so far.
+ if (existingLabels.length > suffixLength) {
+ suffixLength = existingLabels.length;
+ suffixPointer = existingOffset;
+ }
+ }
+ }
+
+ if (suffixLength > 0) {
+ for (int i = 0; i < (labels.length - suffixLength); ++i) {
+ writeString(labels[i]);
+ }
+ writePointer(suffixPointer);
+ } else {
+ int[] offsets = new int[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ offsets[i] = getWritePosition();
+ writeString(labels[i]);
+ }
+ writeUInt8(0); // NUL terminator
+
+ // Add entries to the label dictionary for each suffix of the label list, including
+ // the whole list itself.
+ for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
+ String[] value = new String[len];
+ System.arraycopy(labels, i, value, 0, len);
+ labelDictionary.put(offsets[i], value);
+ }
+ }
+ }
+
+ /** Returns the number of bytes that can still be written. */
+ public int getRemaining() {
+ return data.length - pos;
+ }
+
+ // Writes a pointer to a label.
+ private void writePointer(int offset) throws IOException {
+ writeUInt16(MDNS_POINTER_MASK | offset);
+ }
+
+ // Checks if the remaining space in the packet is at least |count|.
+ private void checkRemaining(int count) throws IOException {
+ if (getRemaining() < count) {
+ throw new IOException();
+ }
+ }
+
+ /** Builds and returns the packet. */
+ public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
+ return new DatagramPacket(data, pos, destAddress);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
new file mode 100644
index 0000000..2b36a3c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "PTR" record, which holds a name (the "pointer"). */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsPointerRecord extends MdnsRecord {
+ private String[] pointer;
+
+ public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_PTR, reader);
+ }
+
+ /** Returns the pointer as an array of labels. */
+ public String[] getPointer() {
+ return pointer;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ pointer = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeLabels(pointer);
+ }
+
+ public boolean hasSubtype() {
+ return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+ }
+
+ public String getSubtype() {
+ return hasSubtype() ? name[0] : null;
+ }
+
+ @Override
+ public String toString() {
+ return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsPointerRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
new file mode 100644
index 0000000..4bfdb2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Abstract base class for mDNS records. Stores the header fields and provides methods for reading
+ * the record from and writing it to a packet.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public abstract class MdnsRecord {
+ public static final int TYPE_A = 0x0001;
+ public static final int TYPE_AAAA = 0x001C;
+ public static final int TYPE_PTR = 0x000C;
+ public static final int TYPE_SRV = 0x0021;
+ public static final int TYPE_TXT = 0x0010;
+
+ /** Status indicating that the record is current. */
+ public static final int STATUS_OK = 0;
+ /** Status indicating that the record has expired (TTL reached 0). */
+ public static final int STATUS_EXPIRED = 1;
+ /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
+ public static final int STATUS_NEEDS_REFRESH = 2;
+
+ protected final String[] name;
+ private final int type;
+ private final int cls;
+ private final long receiptTimeMillis;
+ private final long ttlMillis;
+ private Object key;
+
+ /**
+ * Constructs a new record with the given name and type.
+ *
+ * @param reader The reader to read the record from.
+ * @throws IOException If an error occurs while reading the packet.
+ */
+ protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
+ this.name = name;
+ this.type = type;
+ cls = reader.readUInt16();
+ ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
+ int dataLength = reader.readUInt16();
+
+ receiptTimeMillis = SystemClock.elapsedRealtime();
+
+ reader.setLimit(dataLength);
+ readData(reader);
+ reader.clearLimit();
+ }
+
+ /**
+ * Converts an array of labels into their dot-separated string representation. This method
+ * should
+ * be used for logging purposes only.
+ */
+ public static String labelsToString(String[] labels) {
+ if (labels == null) {
+ return null;
+ }
+ return TextUtils.join(".", labels);
+ }
+
+ /** Tests if |list1| is a suffix of |list2|. */
+ public static boolean labelsAreSuffix(String[] list1, String[] list2) {
+ int offset = list2.length - list1.length;
+
+ if (offset < 1) {
+ return false;
+ }
+
+ for (int i = 0; i < list1.length; ++i) {
+ if (!list1[i].equals(list2[i + offset])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns the record's receipt (creation) time. */
+ public final long getReceiptTime() {
+ return receiptTimeMillis;
+ }
+
+ /** Returns the record's name. */
+ public String[] getName() {
+ return name;
+ }
+
+ /** Returns the record's original TTL, in milliseconds. */
+ public final long getTtl() {
+ return ttlMillis;
+ }
+
+ /** Returns the record's type. */
+ public final int getType() {
+ return type;
+ }
+
+ /**
+ * Returns the record's remaining TTL.
+ *
+ * @param now The current system time.
+ * @return The remaning TTL, in milliseconds.
+ */
+ public long getRemainingTTL(final long now) {
+ long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return 0;
+ }
+
+ return ttlMillis - age;
+ }
+
+ /**
+ * Reads the record's payload from a packet.
+ *
+ * @param reader The reader to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void readData(MdnsPacketReader reader) throws IOException;
+
+ /**
+ * Writes the record to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current system time. This is used when writing the updated TTL.
+ */
+ @VisibleForTesting
+ public final void write(MdnsPacketWriter writer, long now) throws IOException {
+ writer.writeLabels(name);
+ writer.writeUInt16(type);
+ writer.writeUInt16(cls);
+
+ writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
+
+ int dataLengthPos = writer.getWritePosition();
+ writer.writeUInt16(0); // data length
+ int dataPos = writer.getWritePosition();
+
+ writeData(writer);
+
+ // Calculate amount of data written, and overwrite the data field earlier in the packet.
+ int endPos = writer.getWritePosition();
+ int dataLength = endPos - dataPos;
+ writer.rewind(dataLengthPos);
+ writer.writeUInt16(dataLength);
+ writer.unrewind();
+ }
+
+ /**
+ * Writes the record's payload to a packet.
+ *
+ * @param writer The writer to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
+
+ /** Gets the status of the record. */
+ public int getStatus(final long now) {
+ final long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return STATUS_EXPIRED;
+ }
+ if (age > (ttlMillis / 2)) {
+ return STATUS_NEEDS_REFRESH;
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MdnsRecord)) {
+ return false;
+ }
+
+ MdnsRecord otherRecord = (MdnsRecord) other;
+
+ return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(name), type);
+ }
+
+ /**
+ * Returns an opaque object that uniquely identifies this record through a combination of its
+ * type
+ * and name. Suitable for use as a key in caches.
+ */
+ public final Object getKey() {
+ if (key == null) {
+ key = new Key(type, name);
+ }
+ return key;
+ }
+
+ private static final class Key {
+ private final int recordType;
+ private final String[] recordName;
+
+ public Key(int recordType, String[] recordName) {
+ this.recordType = recordType;
+ this.recordName = recordName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Key)) {
+ return false;
+ }
+
+ Key otherKey = (Key) other;
+
+ return (recordType == otherKey.recordType) && Arrays.equals(recordName,
+ otherKey.recordName);
+ }
+
+ @Override
+ public int hashCode() {
+ return (recordType * 31) + Arrays.hashCode(recordName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
new file mode 100644
index 0000000..1305e07
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/** An mDNS response. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponse {
+ private final List<MdnsRecord> records;
+ private final List<MdnsPointerRecord> pointerRecords;
+ private MdnsServiceRecord serviceRecord;
+ private MdnsTextRecord textRecord;
+ private MdnsInetAddressRecord inet4AddressRecord;
+ private MdnsInetAddressRecord inet6AddressRecord;
+ private long lastUpdateTime;
+
+ /** Constructs a new, empty response. */
+ public MdnsResponse(long now) {
+ lastUpdateTime = now;
+ records = new LinkedList<>();
+ pointerRecords = new LinkedList<>();
+ }
+
+ // This generic typed helper compares records for equality.
+ // Returns True if records are the same.
+ private <T> boolean recordsAreSame(T a, T b) {
+ return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
+ }
+
+ /**
+ * Adds a pointer record.
+ *
+ * @return <code>true</code> if the record was added, or <code>false</code> if a matching
+ * pointer
+ * record is already present in the response.
+ */
+ public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
+ if (!pointerRecords.contains(pointerRecord)) {
+ pointerRecords.add(pointerRecord);
+ records.add(pointerRecord);
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Gets the pointer records. */
+ public synchronized List<MdnsPointerRecord> getPointerRecords() {
+ // Returns a shallow copy.
+ return new LinkedList<>(pointerRecords);
+ }
+
+ public synchronized boolean hasPointerRecords() {
+ return !pointerRecords.isEmpty();
+ }
+
+ @VisibleForTesting
+ /* package */ synchronized void clearPointerRecords() {
+ pointerRecords.clear();
+ }
+
+ public synchronized boolean hasSubtypes() {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized List<String> getSubtypes() {
+ List<String> subtypes = null;
+
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ if (subtypes == null) {
+ subtypes = new LinkedList<>();
+ }
+ subtypes.add(pointerRecord.getSubtype());
+ }
+ }
+
+ return subtypes;
+ }
+
+ @VisibleForTesting
+ public synchronized void removeSubtypes() {
+ Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
+ while (iter.hasNext()) {
+ MdnsPointerRecord pointerRecord = iter.next();
+ if (pointerRecord.hasSubtype()) {
+ iter.remove();
+ }
+ }
+ }
+
+ /** Sets the service record. */
+ public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
+ if (recordsAreSame(this.serviceRecord, serviceRecord)) {
+ return false;
+ }
+ if (this.serviceRecord != null) {
+ records.remove(this.serviceRecord);
+ }
+ this.serviceRecord = serviceRecord;
+ if (this.serviceRecord != null) {
+ records.add(this.serviceRecord);
+ }
+ return true;
+ }
+
+ /** Gets the service record. */
+ public synchronized MdnsServiceRecord getServiceRecord() {
+ return serviceRecord;
+ }
+
+ public synchronized boolean hasServiceRecord() {
+ return serviceRecord != null;
+ }
+
+ /** Sets the text record. */
+ public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
+ if (recordsAreSame(this.textRecord, textRecord)) {
+ return false;
+ }
+ if (this.textRecord != null) {
+ records.remove(this.textRecord);
+ }
+ this.textRecord = textRecord;
+ if (this.textRecord != null) {
+ records.add(this.textRecord);
+ }
+ return true;
+ }
+
+ /** Gets the text record. */
+ public synchronized MdnsTextRecord getTextRecord() {
+ return textRecord;
+ }
+
+ public synchronized boolean hasTextRecord() {
+ return textRecord != null;
+ }
+
+ /** Sets the IPv4 address record. */
+ public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
+ if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
+ return false;
+ }
+ if (this.inet4AddressRecord != null) {
+ records.remove(this.inet4AddressRecord);
+ }
+ if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
+ this.inet4AddressRecord = newInet4AddressRecord;
+ records.add(this.inet4AddressRecord);
+ }
+ return true;
+ }
+
+ /** Gets the IPv4 address record. */
+ public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
+ return inet4AddressRecord;
+ }
+
+ public synchronized boolean hasInet4AddressRecord() {
+ return inet4AddressRecord != null;
+ }
+
+ /** Sets the IPv6 address record. */
+ public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
+ if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
+ return false;
+ }
+ if (this.inet6AddressRecord != null) {
+ records.remove(this.inet6AddressRecord);
+ }
+ if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
+ this.inet6AddressRecord = newInet6AddressRecord;
+ records.add(this.inet6AddressRecord);
+ }
+ return true;
+ }
+
+
+ /** Gets the IPv6 address record. */
+ public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
+ return inet6AddressRecord;
+ }
+
+ public synchronized boolean hasInet6AddressRecord() {
+ return inet6AddressRecord != null;
+ }
+
+ /** Gets all of the records. */
+ public synchronized List<MdnsRecord> getRecords() {
+ return new LinkedList<>(records);
+ }
+
+ /**
+ * Merges any records that are present in another response into this one.
+ *
+ * @return <code>true</code> if any records were added or updated.
+ */
+ public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
+ lastUpdateTime = other.lastUpdateTime;
+
+ boolean updated = false;
+
+ List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
+ if (pointerRecords != null) {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (addPointerRecord(pointerRecord)) {
+ updated = true;
+ }
+ }
+ }
+
+ MdnsServiceRecord serviceRecord = other.getServiceRecord();
+ if (serviceRecord != null) {
+ if (setServiceRecord(serviceRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsTextRecord textRecord = other.getTextRecord();
+ if (textRecord != null) {
+ if (setTextRecord(textRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
+ if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
+ if (setInet4AddressRecord(otherInet4AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
+ if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
+ if (setInet6AddressRecord(otherInet6AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ // If the hostname in the service record no longer matches the hostname in either of the
+ // address records, then drop the address records.
+ if (this.serviceRecord != null) {
+ boolean dropAddressRecords = false;
+
+ if (this.inet4AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+ if (this.inet6AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+
+ if (dropAddressRecords) {
+ setInet4AddressRecord(null);
+ setInet6AddressRecord(null);
+ updated = true;
+ }
+ }
+
+ return updated;
+ }
+
+ /**
+ * Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
+ * TXT, and A (for IPv4) or AAAA (for IPv6) records.
+ */
+ public synchronized boolean isComplete() {
+ return !pointerRecords.isEmpty()
+ && (serviceRecord != null)
+ && (textRecord != null)
+ && (inet4AddressRecord != null || inet6AddressRecord != null);
+ }
+
+ /**
+ * Returns the key for this response. The key uniquely identifies the response by its service
+ * name.
+ */
+ public synchronized String getServiceInstanceName() {
+ if (pointerRecords.isEmpty()) {
+ return null;
+ }
+ String[] pointers = pointerRecords.get(0).getPointer();
+ return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
+ }
+
+ /**
+ * Tests if this response is a goodbye message. This will be true if a service record is present
+ * and any of the records have a TTL of 0.
+ */
+ public synchronized boolean isGoodbye() {
+ if (getServiceInstanceName() != null) {
+ for (MdnsRecord record : records) {
+ // Expiring PTR records with subtypes just signal a change in known supported
+ // criteria, not the device itself going offline, so ignore those.
+ if ((record instanceof MdnsPointerRecord)
+ && ((MdnsPointerRecord) record).hasSubtype()) {
+ continue;
+ }
+
+ if (record.getTtl() == 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Writes the response to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current time. This is used to write updated TTLs that reflect the remaining
+ * TTL
+ * since the response was received.
+ * @return The number of records that were written.
+ * @throws IOException If an error occurred while writing (typically indicating overflow).
+ */
+ public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
+ int count = 0;
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ pointerRecord.write(writer, now);
+ ++count;
+ }
+
+ if (serviceRecord != null) {
+ serviceRecord.write(writer, now);
+ ++count;
+ }
+
+ if (textRecord != null) {
+ textRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet4AddressRecord != null) {
+ inet4AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet6AddressRecord != null) {
+ inet6AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ return count;
+ }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
new file mode 100644
index 0000000..72c3156
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/** A class that decodes mDNS responses from UDP packets. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponseDecoder {
+
+ public static final int SUCCESS = 0;
+ private static final String TAG = "MdnsResponseDecoder";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private final String[] serviceType;
+ private final Clock clock;
+
+ /** Constructs a new decoder that will extract responses for the given service type. */
+ public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+ this.clock = clock;
+ this.serviceType = serviceType;
+ }
+
+ private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
+ reader.skip(2 + 4); // skip the class and TTL
+ int dataLength = reader.readUInt16();
+ reader.skip(dataLength);
+ }
+
+ private static MdnsResponse findResponseWithPointer(
+ List<MdnsResponse> responses, String[] pointer) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
+ if (pointerRecords == null) {
+ continue;
+ }
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
+ return response;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static MdnsResponse findResponseWithHostName(
+ List<MdnsResponse> responses, String[] hostName) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ MdnsServiceRecord serviceRecord = response.getServiceRecord();
+ if (serviceRecord == null) {
+ continue;
+ }
+ if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+ return response;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decodes all mDNS responses for the desired service type from a packet. The class does not
+ * check
+ * the responses for completeness; the caller should do that.
+ *
+ * @param packet The packet to read from.
+ * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+ */
+ public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ List<MdnsRecord> records;
+ try {
+ reader.readUInt16(); // transaction ID (not used)
+ int flags = reader.readUInt16();
+ if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
+ return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
+ }
+
+ int numQuestions = reader.readUInt16();
+ int numAnswers = reader.readUInt16();
+ int numAuthority = reader.readUInt16();
+ int numRecords = reader.readUInt16();
+
+ LOGGER.log(String.format(
+ "num questions: %d, num answers: %d, num authority: %d, num records: %d",
+ numQuestions, numAnswers, numAuthority, numRecords));
+
+ if (numAnswers < 1) {
+ return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
+ }
+
+ records = new LinkedList<>();
+
+ for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
+ String[] name;
+ try {
+ name = reader.readLabels();
+ } catch (IOException e) {
+ LOGGER.e("Failed to read labels from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
+ }
+ int type = reader.readUInt16();
+
+ switch (type) {
+ case MdnsRecord.TYPE_A: {
+ try {
+ records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read A record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_AAAA: {
+ try {
+ // AAAA should only contain the IPv6 address.
+ MdnsInetAddressRecord record =
+ new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+ if (record.getInet6Address() != null) {
+ records.add(record);
+ }
+ } catch (IOException e) {
+ LOGGER.e("Failed to read AAAA record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_PTR: {
+ try {
+ records.add(new MdnsPointerRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read PTR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_SRV: {
+ if (name.length == 4) {
+ try {
+ records.add(new MdnsServiceRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read SRV record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
+ }
+ } else {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip SVR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
+ }
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_TXT: {
+ try {
+ records.add(new MdnsTextRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read TXT record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
+ }
+ break;
+ }
+
+ default: {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip mDNS record.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
+ }
+ }
+ }
+ }
+ } catch (EOFException e) {
+ LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
+ return MdnsResponseErrorCode.ERROR_END_OF_FILE;
+ }
+
+ // The response records are structured in a hierarchy, where some records reference
+ // others, as follows:
+ //
+ // PTR
+ // / \
+ // / \
+ // TXT SRV
+ // / \
+ // / \
+ // A AAAA
+ //
+ // But the order in which these records appear in the response packet is completely
+ // arbitrary. This means that we need to rescan the record list to construct each level of
+ // this hierarchy.
+ //
+ // PTR: service type -> service instance name
+ //
+ // SRV: service instance name -> host name (priority, weight)
+ //
+ // TXT: service instance name -> machine readable txt entries.
+ //
+ // A: host name -> IP address
+
+ // Loop 1: find PTR records, which identify distinct service instances.
+ long now = SystemClock.elapsedRealtime();
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsPointerRecord) {
+ String[] name = record.getName();
+ if ((serviceType == null)
+ || Arrays.equals(name, serviceType)
+ || ((name.length == (serviceType.length + 2))
+ && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+ MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
+ // Group PTR records that refer to the same service instance name into a single
+ // response.
+ MdnsResponse response = findResponseWithPointer(responses,
+ pointerRecord.getPointer());
+ if (response == null) {
+ response = new MdnsResponse(now);
+ responses.add(response);
+ }
+ response.addPointerRecord((MdnsPointerRecord) record);
+ }
+ }
+ }
+
+ // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsServiceRecord) {
+ MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
+ if (response != null) {
+ response.setServiceRecord(serviceRecord);
+ }
+ } else if (record instanceof MdnsTextRecord) {
+ MdnsTextRecord textRecord = (MdnsTextRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
+ if (response != null) {
+ response.setTextRecord(textRecord);
+ }
+ }
+ }
+
+ // Loop 3: find A and AAAA records, which reference the host name in the SRV record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsInetAddressRecord) {
+ MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
+ MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
+ if (inetRecord.getInet4Address() != null && response != null) {
+ response.setInet4AddressRecord(inetRecord);
+ } else if (inetRecord.getInet6Address() != null && response != null) {
+ response.setInet6AddressRecord(inetRecord);
+ }
+ }
+ }
+
+ return SUCCESS;
+ }
+
+ public static class Clock {
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
new file mode 100644
index 0000000..fcf9058
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+/**
+ * The list of error code for parsing mDNS response.
+ *
+ * @hide
+ */
+public class MdnsResponseErrorCode {
+ public static final int SUCCESS = 0;
+ public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
+ public static final int ERROR_NO_ANSWERS = 2;
+ public static final int ERROR_READING_RECORD_NAME = 3;
+ public static final int ERROR_READING_A_RDATA = 4;
+ public static final int ERROR_READING_AAAA_RDATA = 5;
+ public static final int ERROR_READING_PTR_RDATA = 6;
+ public static final int ERROR_SKIPPING_PTR_RDATA = 7;
+ public static final int ERROR_READING_SRV_RDATA = 8;
+ public static final int ERROR_SKIPPING_SRV_RDATA = 9;
+ public static final int ERROR_READING_TXT_RDATA = 10;
+ public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
+ public static final int ERROR_END_OF_FILE = 12;
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
new file mode 100644
index 0000000..6e90d2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * API configuration parameters for searching the mDNS service.
+ *
+ * <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
+ *
+ * @hide
+ */
+public class MdnsSearchOptions implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
+ new Parcelable.Creator<MdnsSearchOptions>() {
+ @Override
+ public MdnsSearchOptions createFromParcel(Parcel source) {
+ return new MdnsSearchOptions(source.createStringArrayList(),
+ source.readBoolean());
+ }
+
+ @Override
+ public MdnsSearchOptions[] newArray(int size) {
+ return new MdnsSearchOptions[size];
+ }
+ };
+ private static MdnsSearchOptions defaultOptions;
+ private final List<String> subtypes;
+
+ private final boolean isPassiveMode;
+
+ /** Parcelable constructs for a {@link MdnsServiceInfo}. */
+ MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.isPassiveMode = isPassiveMode;
+ }
+
+ /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Returns a default search options. */
+ public static synchronized MdnsSearchOptions getDefaultOptions() {
+ if (defaultOptions == null) {
+ defaultOptions = newBuilder().build();
+ }
+ return defaultOptions;
+ }
+
+ /** @return the list of subtypes to search. */
+ public List<String> getSubtypes() {
+ return subtypes;
+ }
+
+ /**
+ * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
+ * order to conserve battery and produce less network traffic.
+ */
+ public boolean isPassiveMode() {
+ return isPassiveMode;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStringList(subtypes);
+ out.writeBoolean(isPassiveMode);
+ }
+
+ /** A builder to create {@link MdnsSearchOptions}. */
+ public static final class Builder {
+ private final Set<String> subtypes;
+ private boolean isPassiveMode = true;
+
+ private Builder() {
+ subtypes = new ArraySet<>();
+ }
+
+ /**
+ * Adds a subtype to search.
+ *
+ * @param subtype the subtype to add.
+ */
+ public Builder addSubtype(@NonNull String subtype) {
+ if (TextUtils.isEmpty(subtype)) {
+ throw new IllegalArgumentException("Empty subtype");
+ }
+ subtypes.add(subtype);
+ return this;
+ }
+
+ /**
+ * Adds a set of subtypes to search.
+ *
+ * @param subtypes The list of subtypes to add.
+ */
+ public Builder addSubtypes(@NonNull Collection<String> subtypes) {
+ this.subtypes.addAll(Objects.requireNonNull(subtypes));
+ return this;
+ }
+
+ /**
+ * Sets if the passive mode scan should be used. The passive mode scans less frequently in
+ * order
+ * to conserve battery and produce less network traffic.
+ *
+ * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
+ * false}, active mode will be used.
+ */
+ public Builder setIsPassiveMode(boolean isPassiveMode) {
+ this.isPassiveMode = isPassiveMode;
+ return this;
+ }
+
+ /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
+ public MdnsSearchOptions build() {
+ return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
new file mode 100644
index 0000000..53e58d1
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Listener interface for mDNS service instance discovery events.
+ *
+ * @hide
+ */
+public interface MdnsServiceBrowserListener {
+
+ /**
+ * Called when an mDNS service instance is found.
+ *
+ * @param serviceInfo The found mDNS service instance.
+ */
+ void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is updated.
+ *
+ * @param serviceInfo The updated mDNS service instance.
+ */
+ void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is no longer valid and removed.
+ *
+ * @param serviceInstanceName The service instance name of the removed mDNS service.
+ */
+ void onServiceRemoved(@NonNull String serviceInstanceName);
+
+ /**
+ * Called when searching for mDNS service has stopped because of an error.
+ *
+ * TODO (changed when importing code): define error constants
+ *
+ * @param error The error code of the stop reason.
+ */
+ void onSearchStoppedWithError(int error);
+
+ /** Called when it failed to start an mDNS service discovery process. */
+ void onSearchFailedToStart();
+
+ /**
+ * Called when a mDNS service discovery query has been sent.
+ *
+ * @param subtypes The list of subtypes in the discovery query.
+ * @param transactionId The transaction ID of the query.
+ */
+ void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
+
+ /**
+ * Called when an error has happened when parsing a received mDNS response packet.
+ *
+ * @param receivedPacketNumber The packet sequence number of the received packet.
+ * @param errorCode The error code, defined in {@link MdnsResponseErrorCode}.
+ */
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
new file mode 100644
index 0000000..2e4a4e5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class representing a discovered mDNS service instance.
+ *
+ * @hide
+ */
+public class MdnsServiceInfo implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
+ new Parcelable.Creator<MdnsServiceInfo>() {
+
+ @Override
+ public MdnsServiceInfo createFromParcel(Parcel source) {
+ return new MdnsServiceInfo(
+ source.readString(),
+ source.createStringArray(),
+ source.createStringArrayList(),
+ source.createStringArray(),
+ source.readInt(),
+ source.readString(),
+ source.readString(),
+ source.createStringArrayList());
+ }
+
+ @Override
+ public MdnsServiceInfo[] newArray(int size) {
+ return new MdnsServiceInfo[size];
+ }
+ };
+
+ private final String serviceInstanceName;
+ private final String[] serviceType;
+ private final List<String> subtypes;
+ private final String[] hostName;
+ private final int port;
+ private final String ipv4Address;
+ private final String ipv6Address;
+ private final Map<String, String> attributes = new HashMap<>();
+ List<String> textStrings;
+
+ /**
+ * Constructs a {@link MdnsServiceInfo} object with default values.
+ *
+ * @hide
+ */
+ public MdnsServiceInfo(
+ String serviceInstanceName,
+ String[] serviceType,
+ List<String> subtypes,
+ String[] hostName,
+ int port,
+ String ipv4Address,
+ String ipv6Address,
+ List<String> textStrings) {
+ this.serviceInstanceName = serviceInstanceName;
+ this.serviceType = serviceType;
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.hostName = hostName;
+ this.port = port;
+ this.ipv4Address = ipv4Address;
+ this.ipv6Address = ipv6Address;
+ if (textStrings != null) {
+ for (String text : textStrings) {
+ int pos = text.indexOf('=');
+ if (pos < 1) {
+ continue;
+ }
+ attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
+ text.substring(++pos));
+ }
+ }
+ }
+
+ /** @return the name of this service instance. */
+ public String getServiceInstanceName() {
+ return serviceInstanceName;
+ }
+
+ /** @return the type of this service instance. */
+ public String[] getServiceType() {
+ return serviceType;
+ }
+
+ /** @return the list of subtypes supported by this service instance. */
+ public List<String> getSubtypes() {
+ return new ArrayList<>(subtypes);
+ }
+
+ /**
+ * @return {@code true} if this service instance supports any subtypes.
+ * @return {@code false} if this service instance does not support any subtypes.
+ */
+ public boolean hasSubtypes() {
+ return !subtypes.isEmpty();
+ }
+
+ /** @return the host name of this service instance. */
+ public String[] getHostName() {
+ return hostName;
+ }
+
+ /** @return the port number of this service instance. */
+ public int getPort() {
+ return port;
+ }
+
+ /** @return the IPV4 address of this service instance. */
+ public String getIpv4Address() {
+ return ipv4Address;
+ }
+
+ /** @return the IPV6 address of this service instance. */
+ public String getIpv6Address() {
+ return ipv6Address;
+ }
+
+ /**
+ * @return the attribute value for {@code key}.
+ * @return {@code null} if no attribute value exists for {@code key}.
+ */
+ public String getAttributeByKey(@NonNull String key) {
+ return attributes.get(key.toLowerCase(Locale.ENGLISH));
+ }
+
+ /** @return an immutable map of all attributes. */
+ public Map<String, String> getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (textStrings == null) {
+ // Lazily initialize the parcelable field mTextStrings.
+ textStrings = new ArrayList<>(attributes.size());
+ for (Map.Entry<String, String> kv : attributes.entrySet()) {
+ textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
+ }
+ }
+
+ out.writeString(serviceInstanceName);
+ out.writeStringArray(serviceType);
+ out.writeStringList(subtypes);
+ out.writeStringArray(hostName);
+ out.writeInt(port);
+ out.writeString(ipv4Address);
+ out.writeString(ipv6Address);
+ out.writeStringList(textStrings);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "Name: %s, subtypes: %s, ip: %s, port: %d",
+ serviceInstanceName,
+ TextUtils.join(",", subtypes),
+ ipv4Address,
+ port);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
new file mode 100644
index 0000000..51de3b2
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "SRV" record, which contains service information. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsServiceRecord extends MdnsRecord {
+ public static final int PROTO_NONE = 0;
+ public static final int PROTO_TCP = 1;
+ public static final int PROTO_UDP = 2;
+ private static final String PROTO_TOKEN_TCP = "_tcp";
+ private static final String PROTO_TOKEN_UDP = "_udp";
+ private int servicePriority;
+ private int serviceWeight;
+ private int servicePort;
+ private String[] serviceHost;
+
+ public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_SRV, reader);
+ }
+
+ /** Returns the service's port number. */
+ public int getServicePort() {
+ return servicePort;
+ }
+
+ /** Returns the service's host name. */
+ public String[] getServiceHost() {
+ return serviceHost;
+ }
+
+ /** Returns the service's priority. */
+ public int getServicePriority() {
+ return servicePriority;
+ }
+
+ /** Returns the service's weight. */
+ public int getServiceWeight() {
+ return serviceWeight;
+ }
+
+ // Format of name is <instance-name>.<service-name>.<protocol>.<domain>
+
+ /** Returns the service's instance name, which uniquely identifies the service instance. */
+ public String getServiceInstanceName() {
+ if (name.length < 1) {
+ return null;
+ }
+ return name[0];
+ }
+
+ /** Returns the service's name. */
+ public String getServiceName() {
+ if (name.length < 2) {
+ return null;
+ }
+ return name[1];
+ }
+
+ /** Returns the service's protocol. */
+ public int getServiceProtocol() {
+ if (name.length < 3) {
+ return PROTO_NONE;
+ }
+
+ String protocol = name[2];
+ if (protocol.equals(PROTO_TOKEN_TCP)) {
+ return PROTO_TCP;
+ }
+ if (protocol.equals(PROTO_TOKEN_UDP)) {
+ return PROTO_UDP;
+ }
+ return PROTO_NONE;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ servicePriority = reader.readUInt16();
+ serviceWeight = reader.readUInt16();
+ servicePort = reader.readUInt16();
+ serviceHost = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeUInt16(servicePriority);
+ writer.writeUInt16(serviceWeight);
+ writer.writeUInt16(servicePort);
+ writer.writeLabels(serviceHost);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "SRV: %s:%d (prio=%d, weight=%d)",
+ labelsToString(serviceHost),
+ servicePort,
+ servicePriority,
+ serviceWeight);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+ servicePort);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsServiceRecord)) {
+ return false;
+ }
+ MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
+
+ return super.equals(other)
+ && (servicePriority == otherRecord.servicePriority)
+ && (serviceWeight == otherRecord.serviceWeight)
+ && Objects.equals(serviceHost, otherRecord.serviceHost)
+ && (servicePort == otherRecord.servicePort);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
new file mode 100644
index 0000000..c3a86e3
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Instance of this class sends and receives mDNS packets of a given service type and invoke
+ * registered {@link MdnsServiceBrowserListener} instances.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsServiceTypeClient {
+
+ private static final int DEFAULT_MTU = 1500;
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsServiceTypeClient");
+
+ private final String serviceType;
+ private final String[] serviceTypeLabels;
+ private final MdnsSocketClient socketClient;
+ private final ScheduledExecutorService executor;
+ private final Object lock = new Object();
+ private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
+ private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+
+ // The session ID increases when startSendAndReceive() is called where we schedule a
+ // QueryTask for
+ // new subtypes. It stays the same between packets for same subtypes.
+ private long currentSessionId = 0;
+
+ @GuardedBy("lock")
+ private Future<?> requestTaskFuture;
+
+ /**
+ * Constructor of {@link MdnsServiceTypeClient}.
+ *
+ * @param socketClient Sends and receives mDNS packet.
+ * @param executor A {@link ScheduledExecutorService} used to schedule query tasks.
+ */
+ public MdnsServiceTypeClient(
+ @NonNull String serviceType,
+ @NonNull MdnsSocketClient socketClient,
+ @NonNull ScheduledExecutorService executor) {
+ this.serviceType = serviceType;
+ this.socketClient = socketClient;
+ this.executor = executor;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ }
+
+ private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
+ @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+ String[] hostName = response.getServiceRecord().getServiceHost();
+ int port = response.getServiceRecord().getServicePort();
+
+ String ipv4Address = null;
+ String ipv6Address = null;
+ if (response.hasInet4AddressRecord()) {
+ ipv4Address = response.getInet4AddressRecord().getInet4Address().getHostAddress();
+ }
+ if (response.hasInet6AddressRecord()) {
+ ipv6Address = response.getInet6AddressRecord().getInet6Address().getHostAddress();
+ }
+ // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+ return new MdnsServiceInfo(
+ response.getServiceInstanceName(),
+ serviceTypeLabels,
+ response.getSubtypes(),
+ hostName,
+ port,
+ ipv4Address,
+ ipv6Address,
+ response.getTextRecord().getStrings());
+ }
+
+ /**
+ * Registers {@code listener} for receiving discovery event of mDNS service instances, and
+ * starts
+ * (or continue) to send mDNS queries periodically.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to register.
+ * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
+ */
+ public void startSendAndReceive(
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ synchronized (lock) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+ if (existingResponse.isComplete()) {
+ listener.onServiceFound(
+ buildMdnsServiceInfoFromResponse(existingResponse,
+ serviceTypeLabels));
+ }
+ }
+ }
+ // Cancel the next scheduled periodical task.
+ if (requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ }
+ // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
+ // interested anymore.
+ requestTaskFuture =
+ executor.submit(
+ new QueryTask(
+ new QueryTaskConfig(
+ searchOptions.getSubtypes(),
+ searchOptions.isPassiveMode(),
+ ++currentSessionId)));
+ }
+ }
+
+ /**
+ * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to unregister.
+ * @return {@code true} if no listener is registered with this client after unregistering {@code
+ * listener}. Otherwise returns {@code false}.
+ */
+ public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
+ synchronized (lock) {
+ listeners.remove(listener);
+ if (listeners.isEmpty() && requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ requestTaskFuture = null;
+ }
+ return listeners.isEmpty();
+ }
+ }
+
+ public String[] getServiceTypeLabels() {
+ return serviceTypeLabels;
+ }
+
+ public synchronized void processResponse(@NonNull MdnsResponse response) {
+ if (response.isGoodbye()) {
+ onGoodbyeReceived(response.getServiceInstanceName());
+ } else {
+ onResponseReceived(response);
+ }
+ }
+
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ private void onResponseReceived(@NonNull MdnsResponse response) {
+ MdnsResponse currentResponse;
+ currentResponse = instanceNameToResponse.get(response.getServiceInstanceName());
+
+ boolean newServiceFound = false;
+ boolean existingServiceChanged = false;
+ if (currentResponse == null) {
+ newServiceFound = true;
+ currentResponse = response;
+ instanceNameToResponse.put(response.getServiceInstanceName(), currentResponse);
+ } else if (currentResponse.mergeRecordsFrom(response)) {
+ existingServiceChanged = true;
+ }
+ if (!currentResponse.isComplete() || (!newServiceFound && !existingServiceChanged)) {
+ return;
+ }
+ MdnsServiceInfo serviceInfo =
+ buildMdnsServiceInfoFromResponse(currentResponse, serviceTypeLabels);
+
+ for (MdnsServiceBrowserListener listener : listeners) {
+ if (newServiceFound) {
+ listener.onServiceFound(serviceInfo);
+ } else {
+ listener.onServiceUpdated(serviceInfo);
+ }
+ }
+ }
+
+ private void onGoodbyeReceived(@NonNull String serviceInstanceName) {
+ instanceNameToResponse.remove(serviceInstanceName);
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onServiceRemoved(serviceInstanceName);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return new MdnsPacketWriter(DEFAULT_MTU);
+ }
+
+ // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+ // Call to getConfigForNextRun returns a config that can be used to build the next query task.
+ @VisibleForTesting
+ static class QueryTaskConfig {
+
+ private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+ (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+ private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+ private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+ (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+ private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+ (int) MdnsConfigs.queriesPerBurstPassive();
+ private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+ // The following fields are used by QueryTask so we need to test them.
+ @VisibleForTesting
+ final List<String> subtypes;
+ private final boolean alwaysAskForUnicastResponse =
+ MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
+ private final boolean usePassiveMode;
+ private final long sessionId;
+ @VisibleForTesting
+ int transactionId;
+ @VisibleForTesting
+ boolean expectUnicastResponse;
+ private int queriesPerBurst;
+ private int timeBetweenBurstsInMs;
+ private int burstCounter;
+ private int timeToRunNextTaskInMs;
+ private boolean isFirstBurst;
+
+ QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
+ long sessionId) {
+ this.usePassiveMode = usePassiveMode;
+ this.subtypes = new ArrayList<>(subtypes);
+ this.queriesPerBurst = QUERIES_PER_BURST;
+ this.burstCounter = 0;
+ this.transactionId = 1;
+ this.expectUnicastResponse = true;
+ this.isFirstBurst = true;
+ this.sessionId = sessionId;
+ // Config the scan frequency based on the scan mode.
+ if (this.usePassiveMode) {
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
+ // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+ } else {
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+ }
+ }
+
+ QueryTaskConfig getConfigForNextRun() {
+ if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
+ transactionId = 1;
+ }
+ // Only the first query expects uni-cast response.
+ expectUnicastResponse = false;
+ if (++burstCounter == queriesPerBurst) {
+ burstCounter = 0;
+
+ if (alwaysAskForUnicastResponse) {
+ expectUnicastResponse = true;
+ }
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
+ // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ if (isFirstBurst) {
+ isFirstBurst = false;
+ if (usePassiveMode) {
+ queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+ }
+ }
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+ if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
+ timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+ TIME_BETWEEN_BURSTS_MS);
+ }
+ } else {
+ timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+ return this;
+ }
+ }
+
+ // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
+ private class QueryTask implements Runnable {
+
+ private final QueryTaskConfig config;
+
+ QueryTask(@NonNull QueryTaskConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void run() {
+ Pair<Integer, List<String>> result;
+ try {
+ result =
+ new EnqueueMdnsQueryCallable(
+ socketClient,
+ createMdnsPacketWriter(),
+ serviceType,
+ config.subtypes,
+ config.expectUnicastResponse,
+ config.transactionId)
+ .call();
+ } catch (Exception e) {
+ LOGGER.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
+ TextUtils.join(",", config.subtypes)), e);
+ result = null;
+ }
+ synchronized (lock) {
+ if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
+ // In case that the task is not canceled successfully, use session ID to check
+ // if this task should continue to schedule more.
+ if (config.sessionId != currentSessionId) {
+ return;
+ }
+ }
+
+ if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
+ if (requestTaskFuture == null) {
+ // If requestTaskFuture is set to null, the task is cancelled. We can't use
+ // isCancelled() here because this QueryTask is different from the future
+ // that is returned from executor.schedule(). See b/71646910.
+ return;
+ }
+ }
+ if ((result != null)) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onDiscoveryQuerySent(result.second, result.first);
+ }
+ }
+ QueryTaskConfig config = this.config.getConfigForNextRun();
+ requestTaskFuture =
+ executor.schedule(
+ new QueryTask(config), config.timeToRunNextTaskInMs,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
new file mode 100644
index 0000000..241a52a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.util.List;
+
+/**
+ * {@link MdnsSocket} provides a similar interface to {@link MulticastSocket} and binds to all
+ * available multi-cast network interfaces.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocket {
+ 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);
+ private static boolean isOnIPv6OnlyNetwork = false;
+ private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
+ private final MulticastSocket multicastSocket;
+
+ public MdnsSocket(
+ @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+ throws IOException {
+ this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
+ this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
+ multicastSocket = createMulticastSocket(port);
+ // RFC Spec: https://tools.ietf.org/html/rfc6762
+ // Time to live is set 255, which is similar to the jMDNS implementation.
+ multicastSocket.setTimeToLive(255);
+
+ // TODO (changed when importing code): consider tagging the socket for data usage
+ isOnIPv6OnlyNetwork = false;
+ }
+
+ public void send(DatagramPacket packet) throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.setNetworkInterface(networkInterface.getNetworkInterface());
+ multicastSocket.send(packet);
+ }
+ }
+
+ public void receive(DatagramPacket packet) throws IOException {
+ multicastSocket.receive(packet);
+ }
+
+ public void joinGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ isOnIPv6OnlyNetwork = true;
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ } else {
+ isOnIPv6OnlyNetwork = false;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void leaveGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void close() {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
+ }
+
+ @VisibleForTesting
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return new MulticastSocket(port);
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return isOnIPv6OnlyNetwork;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
new file mode 100644
index 0000000..e689d6c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link MdnsSocketClient} maintains separate threads to send and receive mDNS packets for all
+ * the requested service types.
+ *
+ * <p>See https://tools.ietf.org/html/rfc6763 (namely sections 4 and 5).
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocketClient {
+
+ private static final String TAG = "MdnsClient";
+ // TODO: The following values are copied from cast module. We need to think about the
+ // better way to share those.
+ private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
+ private static final String CAST_PREFS_NAME = "google_cast";
+ private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final String MULTICAST_TYPE = "multicast";
+ private static final String UNICAST_TYPE = "unicast";
+
+ private static final long SLEEP_TIME_FOR_SOCKET_THREAD_MS =
+ MdnsConfigs.sleepTimeForSocketThreadMs();
+ // A value of 0 leads to an infinite wait.
+ private static final long THREAD_JOIN_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
+ private static final int RECEIVER_BUFFER_SIZE = 2048;
+ @VisibleForTesting
+ final Queue<DatagramPacket> multicastPacketQueue = new ArrayDeque<>();
+ @VisibleForTesting
+ final Queue<DatagramPacket> unicastPacketQueue = new ArrayDeque<>();
+ private final Context context;
+ private final byte[] multicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ private final byte[] unicastReceiverBuffer;
+ private final MdnsResponseDecoder responseDecoder;
+ private final MulticastLock multicastLock;
+ private final boolean useSeparateSocketForUnicast =
+ MdnsConfigs.useSeparateSocketToSendUnicastQuery();
+ private final boolean checkMulticastResponse = MdnsConfigs.checkMulticastResponse();
+ private final long checkMulticastResponseIntervalMs =
+ MdnsConfigs.checkMulticastResponseIntervalMs();
+ private final Object socketLock = new Object();
+ private final Object timerObject = new Object();
+ // If multicast response was received in the current session. The value is reset in the
+ // beginning of each session.
+ @VisibleForTesting
+ boolean receivedMulticastResponse;
+ // If unicast response was received in the current session. The value is reset in the beginning
+ // of each session.
+ @VisibleForTesting
+ boolean receivedUnicastResponse;
+ // If the phone is the bad state where it can't receive any multicast response.
+ @VisibleForTesting
+ AtomicBoolean cannotReceiveMulticastResponse = new AtomicBoolean(false);
+ @VisibleForTesting
+ volatile Thread sendThread;
+ @VisibleForTesting
+ Thread multicastReceiveThread;
+ @VisibleForTesting
+ Thread unicastReceiveThread;
+ private volatile boolean shouldStopSocketLoop;
+ private Callback callback;
+ private MdnsSocket multicastSocket;
+ private MdnsSocket unicastSocket;
+ private int receivedPacketNumber = 0;
+ private Timer logMdnsPacketTimer;
+ private AtomicInteger packetsCount;
+ private Timer checkMulticastResponseTimer;
+
+ public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock) {
+ this.context = context;
+ this.multicastLock = multicastLock;
+ responseDecoder = new MdnsResponseDecoder(new MdnsResponseDecoder.Clock(), null);
+ if (useSeparateSocketForUnicast) {
+ unicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ } else {
+ unicastReceiverBuffer = null;
+ }
+ }
+
+ public synchronized void setCallback(@Nullable Callback callback) {
+ this.callback = callback;
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void startDiscovery() throws IOException {
+ if (multicastSocket != null) {
+ LOGGER.w("Discovery is already in progress.");
+ return;
+ }
+
+ receivedMulticastResponse = false;
+ receivedUnicastResponse = false;
+ cannotReceiveMulticastResponse.set(false);
+
+ shouldStopSocketLoop = false;
+ try {
+ // TODO (changed when importing code): consider setting thread stats tag
+ multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+ multicastSocket.joinGroup();
+ if (useSeparateSocketForUnicast) {
+ // For unicast, use port 0 and the system will assign it with any available port.
+ unicastSocket = createMdnsSocket(0);
+ }
+ multicastLock.acquire();
+ } catch (IOException e) {
+ multicastLock.release();
+ if (multicastSocket != null) {
+ multicastSocket.close();
+ multicastSocket = null;
+ }
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ unicastSocket = null;
+ }
+ throw e;
+ } finally {
+ // TODO (changed when importing code): consider resetting thread stats tag
+ }
+ createAndStartSendThread();
+ createAndStartReceiverThreads();
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public void stopDiscovery() {
+ LOGGER.log("Stop discovery.");
+ if (multicastSocket == null && unicastSocket == null) {
+ return;
+ }
+
+ if (MdnsConfigs.clearMdnsPacketQueueAfterDiscoveryStops()) {
+ synchronized (multicastPacketQueue) {
+ multicastPacketQueue.clear();
+ }
+ synchronized (unicastPacketQueue) {
+ unicastPacketQueue.clear();
+ }
+ }
+
+ multicastLock.release();
+
+ shouldStopSocketLoop = true;
+ waitForSendThreadToStop();
+ waitForReceiverThreadsToStop();
+
+ synchronized (socketLock) {
+ multicastSocket = null;
+ unicastSocket = null;
+ }
+
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer != null) {
+ checkMulticastResponseTimer.cancel();
+ checkMulticastResponseTimer = null;
+ }
+ }
+ }
+
+ /** Sends a mDNS request packet that asks for multicast response. */
+ public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+
+ /** Sends a mDNS request packet that asks for unicast response. */
+ public void sendUnicastPacket(DatagramPacket packet) {
+ if (useSeparateSocketForUnicast) {
+ sendMdnsPacket(packet, unicastPacketQueue);
+ } else {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+ }
+
+ private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+ if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
+ LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+ return;
+ }
+ synchronized (packetQueueToUse) {
+ while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+ packetQueueToUse.remove();
+ }
+ packetQueueToUse.add(packet);
+ }
+ triggerSendThread();
+ }
+
+ private void createAndStartSendThread() {
+ if (sendThread != null) {
+ LOGGER.w("A socket thread already exists.");
+ return;
+ }
+ sendThread = new Thread(this::sendThreadMain);
+ sendThread.setName("mdns-send");
+ sendThread.start();
+ }
+
+ private void createAndStartReceiverThreads() {
+ if (multicastReceiveThread != null) {
+ LOGGER.w("A multicast receiver thread already exists.");
+ return;
+ }
+ multicastReceiveThread =
+ new Thread(() -> receiveThreadMain(multicastReceiverBuffer, multicastSocket));
+ multicastReceiveThread.setName("mdns-multicast-receive");
+ multicastReceiveThread.start();
+
+ if (useSeparateSocketForUnicast) {
+ unicastReceiveThread =
+ new Thread(() -> receiveThreadMain(unicastReceiverBuffer, unicastSocket));
+ unicastReceiveThread.setName("mdns-unicast-receive");
+ unicastReceiveThread.start();
+ }
+ }
+
+ private void triggerSendThread() {
+ LOGGER.log("Trigger send thread.");
+ Thread sendThread = this.sendThread;
+ if (sendThread != null) {
+ sendThread.interrupt();
+ } else {
+ LOGGER.w("Socket thread is null");
+ }
+ }
+
+ private void waitForReceiverThreadsToStop() {
+ if (multicastReceiveThread != null) {
+ waitForThread(multicastReceiveThread);
+ multicastReceiveThread = null;
+ }
+
+ if (unicastReceiveThread != null) {
+ waitForThread(unicastReceiveThread);
+ unicastReceiveThread = null;
+ }
+ }
+
+ private void waitForSendThreadToStop() {
+ LOGGER.log("wait For Send Thread To Stop");
+ if (sendThread == null) {
+ LOGGER.w("socket thread is already dead.");
+ return;
+ }
+ waitForThread(sendThread);
+ sendThread = null;
+ }
+
+ private void waitForThread(Thread thread) {
+ long startMs = SystemClock.elapsedRealtime();
+ long waitMs = THREAD_JOIN_TIMEOUT_MS;
+ while (thread.isAlive() && (waitMs > 0)) {
+ try {
+ thread.interrupt();
+ thread.join(waitMs);
+ if (thread.isAlive()) {
+ LOGGER.w("Failed to join thread: " + thread);
+ }
+ break;
+ } catch (InterruptedException e) {
+ // Compute remaining time after at least a single join call, in case the clock
+ // resolution is poor.
+ waitMs = THREAD_JOIN_TIMEOUT_MS - (SystemClock.elapsedRealtime() - startMs);
+ }
+ }
+ }
+
+ private void sendThreadMain() {
+ List<DatagramPacket> multicastPacketsToSend = new ArrayList<>();
+ List<DatagramPacket> unicastPacketsToSend = new ArrayList<>();
+ boolean shouldThreadSleep;
+ try {
+ while (!shouldStopSocketLoop) {
+ try {
+ // Make a local copy of all packets, and clear the queue.
+ // Send packets that ask for multicast response.
+ multicastPacketsToSend.clear();
+ synchronized (multicastPacketQueue) {
+ multicastPacketsToSend.addAll(multicastPacketQueue);
+ multicastPacketQueue.clear();
+ }
+
+ // Send packets that ask for unicast response.
+ if (useSeparateSocketForUnicast) {
+ unicastPacketsToSend.clear();
+ synchronized (unicastPacketQueue) {
+ unicastPacketsToSend.addAll(unicastPacketQueue);
+ unicastPacketQueue.clear();
+ }
+ }
+
+ // Send all the packets.
+ sendPackets(multicastPacketsToSend, multicastSocket);
+ sendPackets(unicastPacketsToSend, unicastSocket);
+
+ // Sleep ONLY if no more packets have been added to the queue, while packets
+ // were being sent.
+ synchronized (multicastPacketQueue) {
+ synchronized (unicastPacketQueue) {
+ shouldThreadSleep =
+ multicastPacketQueue.isEmpty() && unicastPacketQueue.isEmpty();
+ }
+ }
+ if (shouldThreadSleep) {
+ Thread.sleep(SLEEP_TIME_FOR_SOCKET_THREAD_MS);
+ }
+ } catch (InterruptedException e) {
+ // Don't log the interruption as it's expected.
+ }
+ }
+ } finally {
+ LOGGER.log("Send thread stopped.");
+ try {
+ multicastSocket.leaveGroup();
+ } catch (Exception t) {
+ LOGGER.e("Failed to leave the group.", t);
+ }
+
+ // Close the socket first. This is the only way to interrupt a blocking receive.
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ }
+ } catch (Exception t) {
+ LOGGER.e("Failed to close the mdns socket.", t);
+ }
+ }
+ }
+
+ private void receiveThreadMain(byte[] receiverBuffer, MdnsSocket socket) {
+ DatagramPacket packet = new DatagramPacket(receiverBuffer, receiverBuffer.length);
+
+ while (!shouldStopSocketLoop) {
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ synchronized (socketLock) {
+ // This checks is to make sure the socket was not set to null.
+ if (socket != null && (socket == multicastSocket || socket == unicastSocket)) {
+ socket.receive(packet);
+ }
+ }
+
+ if (!shouldStopSocketLoop) {
+ String responseType = socket == multicastSocket ? MULTICAST_TYPE : UNICAST_TYPE;
+ processResponsePacket(packet, responseType);
+ }
+ } catch (IOException e) {
+ if (!shouldStopSocketLoop) {
+ LOGGER.e("Failed to receive mDNS packets.", e);
+ }
+ }
+ }
+ LOGGER.log("Receive thread stopped.");
+ }
+
+ private int processResponsePacket(@NonNull DatagramPacket packet, String responseType)
+ throws IOException {
+ int packetNumber = ++receivedPacketNumber;
+
+ List<MdnsResponse> responses = new LinkedList<>();
+ int errorCode = responseDecoder.decode(packet, responses);
+ if (errorCode == MdnsResponseDecoder.SUCCESS) {
+ if (responseType.equals(MULTICAST_TYPE)) {
+ receivedMulticastResponse = true;
+ if (cannotReceiveMulticastResponse.getAndSet(false)) {
+ // If we are already in the bad state, receiving a multicast response means
+ // we are recovered.
+ LOGGER.e(
+ "Recovered from the state where the phone can't receive any multicast"
+ + " response");
+ }
+ } else {
+ receivedUnicastResponse = true;
+ }
+ for (MdnsResponse response : responses) {
+ String serviceInstanceName = response.getServiceInstanceName();
+ LOGGER.log("mDNS %s response received: %s", responseType, serviceInstanceName);
+ if (callback != null) {
+ callback.onResponseReceived(response);
+ }
+ }
+ } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+ LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+ responseType, packetNumber, errorCode));
+ if (callback != null) {
+ callback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+ }
+ }
+ return errorCode;
+ }
+
+ @VisibleForTesting
+ MdnsSocket createMdnsSocket(int port) throws IOException {
+ return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+ }
+
+ private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
+ String requestType = socket == multicastSocket ? "multicast" : "unicast";
+ for (DatagramPacket packet : packets) {
+ if (shouldStopSocketLoop) {
+ break;
+ }
+ try {
+ LOGGER.log("Sending a %s mDNS packet...", requestType);
+ socket.send(packet);
+
+ // Start the timer task to monitor the response.
+ synchronized (timerObject) {
+ if (socket == multicastSocket) {
+ if (cannotReceiveMulticastResponse.get()) {
+ // Don't schedule the timer task if we are already in the bad state.
+ return;
+ }
+ if (checkMulticastResponseTimer != null) {
+ // Don't schedule the timer task if it's already scheduled.
+ return;
+ }
+ if (checkMulticastResponse && useSeparateSocketForUnicast) {
+ // Only when useSeparateSocketForUnicast is true, we can tell if we
+ // received a multicast or unicast response.
+ checkMulticastResponseTimer = new Timer();
+ checkMulticastResponseTimer.schedule(
+ new TimerTask() {
+ @Override
+ public void run() {
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer == null) {
+ // Discovery already stopped.
+ return;
+ }
+ if ((!receivedMulticastResponse)
+ && receivedUnicastResponse) {
+ LOGGER.e(String.format(
+ "Haven't received multicast response"
+ + " in the last %d ms.",
+ checkMulticastResponseIntervalMs));
+ cannotReceiveMulticastResponse.set(true);
+ }
+ checkMulticastResponseTimer = null;
+ }
+ }
+ },
+ checkMulticastResponseIntervalMs);
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+ }
+ }
+ packets.clear();
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return multicastSocket.isOnIPv6OnlyNetwork();
+ }
+
+ /** Callback for {@link MdnsSocketClient}. */
+ public interface Callback {
+ void onResponseReceived(@NonNull MdnsResponse response);
+
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
new file mode 100644
index 0000000..a5b5595
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** An mDNS "TXT" record, which contains a list of text strings. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsTextRecord extends MdnsRecord {
+ private List<String> strings;
+
+ public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_TXT, reader);
+ }
+
+ /** Returns the list of strings. */
+ public List<String> getStrings() {
+ return Collections.unmodifiableList(strings);
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ strings = new ArrayList<>();
+ while (reader.getRemaining() > 0) {
+ strings.add(reader.readString());
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ if (strings != null) {
+ for (String string : strings) {
+ writer.writeString(string);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("TXT: {");
+ if (strings != null) {
+ for (String string : strings) {
+ sb.append(' ').append(string);
+ }
+ }
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Objects.hash(strings);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsTextRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Objects.equals(strings, ((MdnsTextRecord) other).strings);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
new file mode 100644
index 0000000..e0d8fa6
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class is used by the {@link MdnsSocket} to monitor the list of {@link NetworkInterface}
+ * instances that are currently available for multi-cast messaging.
+ */
+public class MulticastNetworkInterfaceProvider {
+
+ private static final String TAG = "MdnsNIProvider";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
+
+ private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
+ // Only modifiable from tests.
+ @VisibleForTesting
+ ConnectivityMonitor connectivityMonitor;
+ private volatile boolean connectivityChanged = true;
+
+ @SuppressWarnings("nullness:methodref.receiver.bound")
+ public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+ // IMPORT CHANGED
+ this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
+ context, this::onConnectivityChanged);
+ }
+
+ private void onConnectivityChanged() {
+ connectivityChanged = true;
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ public void startWatchingConnectivityChanges() {
+ connectivityMonitor.startWatchingConnectivityChanges();
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ public void stopWatchingConnectivityChanges() {
+ connectivityMonitor.stopWatchingConnectivityChanges();
+ }
+
+ /**
+ * Returns the list of {@link NetworkInterfaceWrapper} instances available for multi-cast
+ * messaging.
+ */
+ public synchronized List<NetworkInterfaceWrapper> getMulticastNetworkInterfaces() {
+ if (connectivityChanged) {
+ connectivityChanged = false;
+ updateMulticastNetworkInterfaces();
+ if (multicastNetworkInterfaces.isEmpty()) {
+ LOGGER.log("No network interface available for mDNS scanning.");
+ }
+ }
+ return new ArrayList<>(multicastNetworkInterfaces);
+ }
+
+ private void updateMulticastNetworkInterfaces() {
+ multicastNetworkInterfaces.clear();
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
+ if (canScanOnInterface(interfaceWrapper)) {
+ multicastNetworkInterfaces.add(interfaceWrapper);
+ }
+ }
+ }
+
+ public boolean isOnIpV6OnlyNetwork(List<NetworkInterfaceWrapper> networkInterfaces) {
+ if (networkInterfaces.isEmpty()) {
+ return false;
+ }
+
+ // TODO(b/79866499): Remove this when the bug is resolved.
+ if (PREFER_IPV6) {
+ return true;
+ }
+ boolean hasAtleastOneIPv6Address = false;
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaces) {
+ for (InterfaceAddress ifAddr : interfaceWrapper.getInterfaceAddresses()) {
+ if (!(ifAddr.getAddress() instanceof Inet6Address)) {
+ return false;
+ } else {
+ hasAtleastOneIPv6Address = true;
+ }
+ }
+ }
+ return hasAtleastOneIPv6Address;
+ }
+
+ @VisibleForTesting
+ List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = new ArrayList<>();
+ try {
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ if (interfaces != null) {
+ while (interfaces.hasMoreElements()) {
+ networkInterfaceWrappers.add(
+ new NetworkInterfaceWrapper(interfaces.nextElement()));
+ }
+ }
+ } catch (SocketException e) {
+ LOGGER.e("Failed to get network interfaces.", e);
+ } catch (NullPointerException e) {
+ // Android R has a bug that could lead to a NPE. See b/159277702.
+ LOGGER.e("Failed to call getNetworkInterfaces API", e);
+ }
+
+ return networkInterfaceWrappers;
+ }
+
+ private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+ try {
+ if ((networkInterface == null)
+ || networkInterface.isLoopback()
+ || networkInterface.isPointToPoint()
+ || networkInterface.isVirtual()
+ || !networkInterface.isUp()
+ || !networkInterface.supportsMulticast()) {
+ return false;
+ }
+ return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to check interface %s.",
+ networkInterface.getNetworkInterface().getDisplayName()), e);
+ }
+
+ return false;
+ }
+
+ private boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet4Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet6Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
new file mode 100644
index 0000000..0ecae48
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.List;
+
+/** A wrapper class of {@link NetworkInterface} to be mocked in unit tests. */
+public class NetworkInterfaceWrapper {
+ private final NetworkInterface networkInterface;
+
+ public NetworkInterfaceWrapper(NetworkInterface networkInterface) {
+ this.networkInterface = networkInterface;
+ }
+
+ public NetworkInterface getNetworkInterface() {
+ return networkInterface;
+ }
+
+ public boolean isUp() throws SocketException {
+ return networkInterface.isUp();
+ }
+
+ public boolean isLoopback() throws SocketException {
+ return networkInterface.isLoopback();
+ }
+
+ public boolean isPointToPoint() throws SocketException {
+ return networkInterface.isPointToPoint();
+ }
+
+ public boolean isVirtual() {
+ return networkInterface.isVirtual();
+ }
+
+ public boolean supportsMulticast() throws SocketException {
+ return networkInterface.supportsMulticast();
+ }
+
+ public List<InterfaceAddress> getInterfaceAddresses() {
+ return networkInterface.getInterfaceAddresses();
+ }
+
+ @Override
+ public String toString() {
+ return networkInterface.toString();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
new file mode 100644
index 0000000..31c62f5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns.util;
+
+import android.net.util.SharedLog;
+import android.text.TextUtils;
+
+/**
+ * The logger used in mDNS.
+ */
+public class MdnsLogger {
+ // Make this logger public for other level logging than dogfood.
+ public final SharedLog mLog;
+
+ /**
+ * Constructs a new {@link MdnsLogger} with the given logging tag.
+ *
+ * @param tag The log tag that will be used by this logger
+ */
+ public MdnsLogger(String tag) {
+ mLog = new SharedLog(tag);
+ }
+
+ public void log(String message) {
+ mLog.log(message);
+ }
+
+ public void log(String message, Object... args) {
+ mLog.log(message + " ; " + TextUtils.join(" ; ", args));
+ }
+
+ public void d(String message) {
+ mLog.log(message);
+ }
+
+ public void e(String message) {
+ mLog.e(message);
+ }
+
+ public void e(String message, Throwable e) {
+ mLog.e(message, e);
+ }
+
+ public void w(String message) {
+ mLog.w(message);
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 68fa38d..7d1e13f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -113,7 +113,7 @@
private static final int UNKNOWN_DETECTION_METHOD = 4;
private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
- private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+ private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 5000;
private static final Executor INLINE_EXECUTOR = x -> x.run();
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index cc64239..458d225 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -57,7 +57,7 @@
import com.android.net.module.util.TrackRecord
import com.android.testutils.anyNetwork
import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DeviceInfoUtils
+import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
@@ -145,6 +145,8 @@
raResponder.start()
}
+ // WARNING: this function requires kernel support. Call assumeChangingCarrierSupported() at
+ // the top of your test.
fun setCarrierEnabled(enabled: Boolean) {
runAsShell(MANAGE_TEST_NETWORKS) {
tnm.setCarrierEnabled(tapInterface, enabled)
@@ -295,6 +297,9 @@
releaseTetheredInterface()
}
+ // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
+ private fun assumeChangingCarrierSupported() = assumeTrue(isKernelVersionAtLeast("5.0.0"))
+
private fun addInterfaceStateListener(listener: EthernetStateListener) {
runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
em.addInterfaceStateListener(handler::post, listener)
@@ -302,6 +307,8 @@
addedListeners.add(listener)
}
+ // WARNING: setting hasCarrier to false requires kernel support. Call
+ // assumeChangingCarrierSupported() at the top of your test.
private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
val iface = EthernetTestInterface(
context,
@@ -631,9 +638,7 @@
@Test
fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
- // Notice this test case fails on devices running on an older kernel version(e.g. 4.14)
- // that might not support ioctl new argument. Only run this test on 4.19 kernel or above.
- assumeTrue(DeviceInfoUtils.isKernelVersionAtLeast("4.19.0"))
+ assumeChangingCarrierSupported()
val iface = createInterface(false /* hasCarrier */)
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 3ea27f7..0908ad2 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -63,12 +63,14 @@
"java/com/android/internal/net/NetworkUtilsInternalTest.java",
"java/com/android/internal/net/VpnProfileTest.java",
"java/com/android/server/NetworkManagementServiceTest.java",
+ "java/com/android/server/VpnManagerServiceTest.java",
"java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
"java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
"java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
"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 +145,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/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 900ee5a..0919dfc 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -10633,19 +10633,6 @@
}
@Test
- public void testStartVpnProfileFromDiffPackage() throws Exception {
- final String notMyVpnPkg = "com.not.my.vpn";
- assertThrows(
- SecurityException.class, () -> mVpnManagerService.startVpnProfile(notMyVpnPkg));
- }
-
- @Test
- public void testStopVpnProfileFromDiffPackage() throws Exception {
- final String notMyVpnPkg = "com.not.my.vpn";
- assertThrows(SecurityException.class, () -> mVpnManagerService.stopVpnProfile(notMyVpnPkg));
- }
-
- @Test
public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
new file mode 100644
index 0000000..ece13b3
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import static com.android.testutils.ContextUtils.mockService;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.UserManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.connectivity.Vpn;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(R) // VpnManagerService is not available before R
+@SmallTest
+public class VpnManagerServiceTest extends VpnTestBase {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ @Spy Context mContext;
+ private HandlerThread mHandlerThread;
+ @Mock private Handler mHandler;
+ @Mock private Vpn mVpn;
+ @Mock private INetworkManagementService mNms;
+ @Mock private ConnectivityManager mCm;
+ @Mock private UserManager mUserManager;
+ @Mock private INetd mNetd;
+ @Mock private PackageManager mPackageManager;
+ private VpnManagerServiceDependencies mDeps;
+ private VpnManagerService mService;
+
+ private final String mNotMyVpnPkg = "com.not.my.vpn";
+
+ class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
+
+ @Override
+ public INetworkManagementService getINetworkManagementService() {
+ return mNms;
+ }
+
+ @Override
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ @Override
+ public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
+ INetd netd, @UserIdInt int userId) {
+ return mVpn;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread("TestVpnManagerService");
+ mDeps = new VpnManagerServiceDependencies();
+ doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt());
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ setMockedPackages(mPackageManager, sPackages);
+
+ mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
+ mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
+
+ doReturn(new Intent()).when(mContext).registerReceiver(
+ any() /* receiver */,
+ any() /* intentFilter */,
+ any() /* broadcastPermission */,
+ eq(mHandler) /* scheduler */);
+ doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
+ mService = new VpnManagerService(mContext, mDeps);
+ }
+
+ @Test
+ public void testUpdateAppExclusionList() {
+ // Add user to create vpn in mVpn
+ mService.onUserStarted(SYSTEM_USER_ID);
+ assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
+
+ // Start vpn
+ mService.startVpnProfile(TEST_VPN_PKG);
+ verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
+
+ // Remove package due to package replaced.
+ mService.onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+ verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+ // Add package due to package replaced.
+ mService.onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+ verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+ // Remove package
+ mService.onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+ verify(mVpn).refreshPlatformVpnAppExclusionList();
+
+ // Add the package back
+ mService.onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+ verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
+ }
+
+ @Test
+ public void testStartVpnProfileFromDiffPackage() {
+ assertThrows(
+ SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
+ }
+
+ @Test
+ public void testStopVpnProfileFromDiffPackage() {
+ assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
+ }
+}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
new file mode 100644
index 0000000..6113872
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnTestBase.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
+public class VpnTestBase {
+ protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
+ /**
+ * Names and UIDs for some fake packages. Important points:
+ * - UID is ordered increasing.
+ * - One pair of packages have consecutive UIDs.
+ */
+ protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+ protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
+ // Mock packages
+ protected static final Map<String, Integer> sPackages = new ArrayMap<>();
+ static {
+ for (int i = 0; i < PKGS.length; i++) {
+ sPackages.put(PKGS[i], PKG_UIDS[i]);
+ }
+ sPackages.put(TEST_VPN_PKG, Process.myUid());
+ }
+
+ // Mock users
+ protected static final int SYSTEM_USER_ID = 0;
+ protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
+ protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
+ FLAG_ADMIN | FLAG_PRIMARY);
+ protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
+ protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
+ FLAG_RESTRICTED);
+ protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
+ FLAG_RESTRICTED);
+ protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
+ FLAG_MANAGED_PROFILE);
+ static {
+ RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
+ RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
+ MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
+ }
+
+ // Populate a fake packageName-to-UID mapping.
+ protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
+ try {
+ doAnswer(invocation -> {
+ final String appName = (String) invocation.getArguments()[0];
+ final int userId = (int) invocation.getArguments()[1];
+
+ final Integer appId = packages.get(appName);
+ if (appId == null) {
+ throw new PackageManager.NameNotFoundException(appName);
+ }
+
+ return UserHandle.getUid(userId, appId);
+ }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
+ } catch (Exception e) {
+ }
+ }
+
+ protected List<Integer> toList(int[] arr) {
+ return Arrays.stream(arr).boxed().collect(Collectors.toList());
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 8f1d3b8..0891ee3 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -20,10 +20,6 @@
import static android.Manifest.permission.CONTROL_VPN;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
import static android.net.ConnectivityManager.NetworkCallback;
import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
@@ -34,6 +30,7 @@
import static android.os.UserHandle.PER_USER_RANGE;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -96,10 +93,8 @@
import android.net.LocalSocket;
import android.net.Network;
import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkProvider;
import android.net.RouteInfo;
import android.net.UidRangeParcel;
import android.net.VpnManager;
@@ -121,7 +116,6 @@
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.INetworkManagementService;
-import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.PowerWhitelistManager;
import android.os.Process;
@@ -145,6 +139,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.server.DeviceIdleInternal;
import com.android.server.IpSecService;
+import com.android.server.VpnTestBase;
import com.android.server.vcn.util.PersistableBundleUtils;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -177,6 +172,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -191,28 +188,15 @@
*/
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@IgnoreUpTo(VERSION_CODES.S_V2)
-public class VpnTest {
+@IgnoreUpTo(S_V2)
+public class VpnTest extends VpnTestBase {
private static final String TAG = "VpnTest";
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
- // Mock users
- static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
- static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
- static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED);
- static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED);
- static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE);
- static {
- restrictedProfileA.restrictedProfileParentId = primaryUser.id;
- restrictedProfileB.restrictedProfileParentId = secondaryUser.id;
- managedProfileA.profileGroupId = primaryUser.id;
- }
-
static final Network EGRESS_NETWORK = new Network(101);
static final String EGRESS_IFACE = "wlan0";
- static final String TEST_VPN_PKG = "com.testvpn.vpn";
private static final String TEST_VPN_CLIENT = "2.4.6.8";
private static final String TEST_VPN_SERVER = "1.2.3.4";
private static final String TEST_VPN_IDENTITY = "identity";
@@ -249,23 +233,8 @@
private static final long TEST_TIMEOUT_MS = 500L;
private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
"VPN_APP_EXCLUDED_27_com.testvpn.vpn";
- /**
- * Names and UIDs for some fake packages. Important points:
- * - UID is ordered increasing.
- * - One pair of packages have consecutive UIDs.
- */
- static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
- static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
-
- // Mock packages
- static final Map<String, Integer> mPackages = new ArrayMap<>();
- static {
- for (int i = 0; i < PKGS.length; i++) {
- mPackages.put(PKGS[i], PKG_UIDS[i]);
- }
- }
- private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id);
+ private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
@Mock private UserManager mUserManager;
@@ -307,7 +276,7 @@
mTestDeps = spy(new TestDeps());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
- setMockedPackages(mPackages);
+ setMockedPackages(sPackages);
when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
@@ -412,50 +381,51 @@
@Test
public void testRestrictedProfilesAreAddedToVpn() {
- setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
+ setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
// Assume the user can have restricted profiles.
doReturn(true).when(mUserManager).canHaveRestrictedProfile();
final Set<Range<Integer>> ranges =
- vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null);
+ vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
+ ranges);
}
@Test
public void testManagedProfilesAreNotAddedToVpn() {
- setMockedUsers(primaryUser, managedProfileA);
+ setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
- final Vpn vpn = createVpn(primaryUser.id);
- final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
- null, null);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
}
@Test
public void testAddUserToVpnOnlyAddsOneUser() {
- setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final Set<Range<Integer>> ranges = new ArraySet<>();
- vpn.addUserToRanges(ranges, primaryUser.id, null, null);
+ vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
}
@Test
public void testUidAllowAndDenylist() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
// Allowed list
- final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+ final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
Arrays.asList(packages), null /* disallowedApplications */);
assertEquals(rangeSet(
uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
@@ -468,7 +438,7 @@
// Denied list
final Set<Range<Integer>> disallow =
- vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+ vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
null /* allowedApplications */, Arrays.asList(packages));
assertEquals(rangeSet(
uidRange(userStart, userStart + PKG_UIDS[0] - 1),
@@ -490,7 +460,7 @@
@Test
public void testGetAlwaysAndOnGetLockDown() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
// Default state.
assertFalse(vpn.getAlwaysOn());
@@ -514,8 +484,8 @@
@Test
public void testLockdownChangingPackage() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
// Set always-on without lockdown.
@@ -548,8 +518,8 @@
@Test
public void testLockdownAllowlist() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
// Set always-on with lockdown and allow app PKGS[2] from lockdown.
@@ -659,9 +629,9 @@
@Test
public void testLockdownRuleRepeatability() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())};
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
// Given legacy lockdown is already enabled,
vpn.setLockdown(true);
verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
@@ -692,9 +662,9 @@
@Test
public void testLockdownRuleReversibility() throws Exception {
doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] entireUser = {
- new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
};
final UidRangeParcel[] exceptPkg0 = {
new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
@@ -744,17 +714,17 @@
@Test
public void testIsAlwaysOnPackageSupported() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
ApplicationInfo appInfo = new ApplicationInfo();
- when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id)))
+ when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
.thenReturn(appInfo);
ServiceInfo svcInfo = new ServiceInfo();
ResolveInfo resInfo = new ResolveInfo();
resInfo.serviceInfo = svcInfo;
when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
- eq(primaryUser.id)))
+ eq(PRIMARY_USER.id)))
.thenReturn(Collections.singletonList(resInfo));
// null package name should return false
@@ -778,9 +748,9 @@
@Test
public void testNotificationShownForAlwaysOnApp() throws Exception {
- final UserHandle userHandle = UserHandle.of(primaryUser.id);
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
final InOrder order = inOrder(mNotificationManager);
@@ -813,15 +783,15 @@
*/
@Test
public void testGetProfileNameForPackage() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
- final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+ final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
}
private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception {
- return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+ return createVpnAndSetupUidChecks(PRIMARY_USER, grantedOps);
}
private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception {
@@ -878,14 +848,11 @@
vpn.startVpnProfile(TEST_VPN_PKG);
verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- vpn.mNetworkAgent = new NetworkAgent(mContext, Looper.getMainLooper(), TAG,
- new NetworkCapabilities.Builder().build(), new LinkProperties(), 10 /* score */,
- new NetworkAgentConfig.Builder().build(),
- new NetworkProvider(mContext, Looper.getMainLooper(), TAG)) {};
+ vpn.mNetworkAgent = mMockNetworkAgent;
return vpn;
}
- @Test @IgnoreUpTo(S_V2)
+ @Test
public void testSetAndGetAppExclusionList() throws Exception {
final Vpn vpn = prepareVpnForVerifyAppExclusionList();
verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
@@ -894,16 +861,90 @@
.put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
assertEquals(vpn.createUserAndRestrictedProfilesRanges(
- primaryUser.id, null, Arrays.asList(PKGS)),
+ PRIMARY_USER.id, null, Arrays.asList(PKGS)),
vpn.mNetworkCapabilities.getUids());
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
}
- @Test @IgnoreUpTo(S_V2)
+ @Test
+ public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
+ final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+ verify(mMockNetworkAgent).sendNetworkCapabilities(any());
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+
+ reset(mMockNetworkAgent);
+
+ // Remove one of the package
+ List<Integer> newExcludedUids = toList(PKG_UIDS);
+ newExcludedUids.remove((Integer) PKG_UIDS[0]);
+ sPackages.remove(PKGS[0]);
+ vpn.refreshPlatformVpnAppExclusionList();
+
+ // List in keystore is not changed, but UID for the removed packages is no longer exempted.
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ vpn.mNetworkCapabilities.getUids());
+ ArgumentCaptor<NetworkCapabilities> ncCaptor =
+ ArgumentCaptor.forClass(NetworkCapabilities.class);
+ verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ ncCaptor.getValue().getUids());
+
+ reset(mMockNetworkAgent);
+
+ // Add the package back
+ newExcludedUids.add(PKG_UIDS[0]);
+ sPackages.put(PKGS[0], PKG_UIDS[0]);
+ vpn.refreshPlatformVpnAppExclusionList();
+
+ // List in keystore is not changed and the uid list should be updated in the net cap.
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ vpn.mNetworkCapabilities.getUids());
+ verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ ncCaptor.getValue().getUids());
+ }
+
+ private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+ final SortedSet<Integer> list = new TreeSet<>();
+
+ final int userBase = userId * UserHandle.PER_USER_RANGE;
+ for (int uid : excludedList) {
+ final int applicationUid = UserHandle.getUid(userId, uid);
+ list.add(applicationUid);
+ list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+ }
+
+ final int minUid = userBase;
+ final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+ final Set<Range<Integer>> ranges = new ArraySet<>();
+
+ // Iterate the list to create the ranges between each uid.
+ int start = minUid;
+ for (int uid : list) {
+ if (uid == start) {
+ start++;
+ } else {
+ ranges.add(new Range<>(start, uid - 1));
+ start = uid + 1;
+ }
+ }
+
+ // Create the range between last uid and max uid.
+ if (start <= maxUid) {
+ ranges.add(new Range<>(start, maxUid));
+ }
+
+ return ranges;
+ }
+
+ @Test
public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
final Vpn vpn = prepareVpnForVerifyAppExclusionList();
// Mock it to restricted profile
- when(mUserManager.getUserInfo(anyInt())).thenReturn(restrictedProfileA);
+ when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
// Restricted users cannot configure VPNs
assertThrows(SecurityException.class,
() -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
@@ -953,7 +994,7 @@
public void testProvisionVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
@@ -976,7 +1017,7 @@
public void testDeleteVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.deleteVpnProfile(TEST_VPN_PKG);
@@ -1099,7 +1140,7 @@
public void testStartVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.startVpnProfile(TEST_VPN_PKG);
@@ -1112,7 +1153,7 @@
public void testStopVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.stopVpnProfile(TEST_VPN_PKG);
@@ -1183,7 +1224,7 @@
private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
int errorCode, VpnProfileState... profileState) {
final Context userContext =
- mContext.createContextAsUser(UserHandle.of(primaryUser.id), 0 /* flags */);
+ mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
final int verifyTimes = (profileState == null) ? 1 : profileState.length;
@@ -1250,7 +1291,7 @@
assumeTrue(SdkLevel.isAtLeastT());
// Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
// Enable VPN always-on for PKGS[1].
assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
null /* lockdownAllowlist */));
@@ -1512,7 +1553,7 @@
public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
.thenThrow(new IllegalArgumentException());
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
final NetworkCallback cb = triggerOnAvailableAndGetCallback();
verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1532,18 +1573,18 @@
eq(AppOpsManager.MODE_ALLOWED));
verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id));
+ eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
verify(mSystemServices).settingsSecurePutIntForUser(
eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
- eq(primaryUser.id));
+ eq(PRIMARY_USER.id));
verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id));
+ eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
}
@Test
public void testSetAndStartAlwaysOnVpn() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
// UID checks must return a different UID; otherwise it'll be treated as already prepared.
final int uid = Process.myUid() + 1;
@@ -1560,7 +1601,7 @@
}
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
- setMockedUsers(primaryUser);
+ setMockedUsers(PRIMARY_USER);
// Dummy egress interface
final LinkProperties lp = new LinkProperties();
@@ -1876,11 +1917,10 @@
doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
any(), any(), any(), any(), anyInt());
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
final TestDeps deps = (TestDeps) vpn.mDeps;
- // TODO: use import when this is merged in all branches and there's no merge conflict
- com.android.testutils.Cleanup.testAndCleanup(() -> {
+ testAndCleanup(() -> {
final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
final String[] argsPrefix = new String[]{
EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
@@ -1928,7 +1968,7 @@
legacyRunnerReady.open();
return new Network(102);
});
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
final TestDeps deps = (TestDeps) vpn.mDeps;
try {
// udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
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