diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index fb3b1d67b1..d34038459f 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -242,6 +242,11 @@ public final class NsdManager { /** @hide */ public static final int UNREGISTER_CLIENT = 22; + /** @hide */ + public static final int MDNS_MONITORING_SOCKETS_CLEANUP = 23; + /** @hide */ + public static final int MDNS_DISCOVERY_MANAGER_EVENT = 24; + /** Dns based service discovery protocol */ public static final int PROTOCOL_DNS_SD = 0x0001; diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index b5a10e29c4..36c3cd48e9 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -17,9 +17,11 @@ package com.android.server; import static android.net.ConnectivityManager.NETID_UNSET; +import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT; import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; @@ -45,6 +47,7 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -58,6 +61,9 @@ import com.android.net.module.util.PermissionUtils; import com.android.server.connectivity.mdns.ExecutorProvider; import com.android.server.connectivity.mdns.MdnsDiscoveryManager; import com.android.server.connectivity.mdns.MdnsMultinetworkSocketClient; +import com.android.server.connectivity.mdns.MdnsSearchOptions; +import com.android.server.connectivity.mdns.MdnsServiceBrowserListener; +import com.android.server.connectivity.mdns.MdnsServiceInfo; import com.android.server.connectivity.mdns.MdnsSocketClientBase; import com.android.server.connectivity.mdns.MdnsSocketProvider; @@ -68,6 +74,9 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.HashMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Network Service Discovery Service handles remote service discovery operation requests by @@ -79,6 +88,7 @@ public class NsdService extends INsdManager.Stub { private static final String TAG = "NsdService"; private static final String MDNS_TAG = "mDnsConnector"; private static final String MDNS_DISCOVERY_MANAGER_VERSION = "mdns_discovery_manager_version"; + private static final String LOCAL_DOMAIN_NAME = "local"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final long CLEANUP_DELAY_MS = 10000; @@ -94,10 +104,11 @@ public class NsdService extends INsdManager.Stub { private final MdnsDiscoveryManager mMdnsDiscoveryManager; @Nullable private final MdnsSocketProvider mMdnsSocketProvider; - // WARNING : Accessing this value in any thread is not safe, it must only be changed in the + // WARNING : Accessing these values in any thread is not safe, it must only be changed in the // state machine thread. If change this outside state machine, it will need to introduce // synchronization. private boolean mIsDaemonStarted = false; + private boolean mIsMonitoringSocketsStarted = false; /** * Clients receiving asynchronous messages @@ -114,6 +125,95 @@ public class NsdService extends INsdManager.Stub { // The count of the connected legacy clients. private int mLegacyClientCount = 0; + private static class MdnsListener implements MdnsServiceBrowserListener { + protected final int mClientId; + protected final int mTransactionId; + @NonNull + protected final NsdServiceInfo mReqServiceInfo; + @NonNull + protected final String mListenedServiceType; + + MdnsListener(int clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo, + @NonNull String listenedServiceType) { + mClientId = clientId; + mTransactionId = transactionId; + mReqServiceInfo = reqServiceInfo; + mListenedServiceType = listenedServiceType; + } + + @NonNull + public String getListenedServiceType() { + return mListenedServiceType; + } + + @Override + public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo) { } + + @Override + public void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo) { } + + @Override + public void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo) { } + + @Override + public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) { } + + @Override + public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) { } + + @Override + public void onSearchStoppedWithError(int error) { } + + @Override + public void onSearchFailedToStart() { } + + @Override + public void onDiscoveryQuerySent(@NonNull List subtypes, int transactionId) { } + + @Override + public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { } + } + + private class DiscoveryListener extends MdnsListener { + + DiscoveryListener(int clientId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo, + @NonNull String listenServiceType) { + super(clientId, transactionId, reqServiceInfo, listenServiceType); + } + + @Override + public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) { + mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId, + NsdManager.SERVICE_FOUND, + new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo)); + } + + @Override + public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) { + mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId, + NsdManager.SERVICE_LOST, + new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo)); + } + } + + /** + * Data class of mdns service callback information. + */ + private static class MdnsEvent { + final int mClientId; + @NonNull + final String mRequestedServiceType; + @NonNull + final MdnsServiceInfo mMdnsServiceInfo; + + MdnsEvent(int clientId, @NonNull String requestedServiceType, + @NonNull MdnsServiceInfo mdnsServiceInfo) { + mClientId = clientId; + mRequestedServiceType = requestedServiceType; + mMdnsServiceInfo = mdnsServiceInfo; + } + } + private class NsdStateMachine extends StateMachine { private final DefaultState mDefaultState = new DefaultState(); @@ -164,6 +264,31 @@ public class NsdService extends INsdManager.Stub { this.removeMessages(NsdManager.DAEMON_CLEANUP); } + private void maybeStartMonitoringSockets() { + if (mIsMonitoringSocketsStarted) { + if (DBG) Log.d(TAG, "Socket monitoring is already started."); + return; + } + + mMdnsSocketProvider.startMonitoringSockets(); + mIsMonitoringSocketsStarted = true; + } + + private void maybeStopMonitoringSockets() { + if (!mIsMonitoringSocketsStarted) { + if (DBG) Log.d(TAG, "Socket monitoring has not been started."); + return; + } + mMdnsSocketProvider.stopMonitoringSockets(); + mIsMonitoringSocketsStarted = false; + } + + private void maybeStopMonitoringSocketsIfNoActiveRequest() { + if (!isAnyRequestActive()) { + maybeStopMonitoringSockets(); + } + } + NsdStateMachine(String name, Handler handler) { super(name, handler); addState(mDefaultState); @@ -195,11 +320,17 @@ public class NsdService extends INsdManager.Stub { final NsdServiceConnector connector = (NsdServiceConnector) msg.obj; cInfo = mClients.remove(connector); if (cInfo != null) { + if (mMdnsDiscoveryManager != null) { + cInfo.unregisterAllListeners(); + } cInfo.expungeAllRequests(); if (cInfo.isLegacy()) { mLegacyClientCount -= 1; } } + if (mMdnsDiscoveryManager != null) { + maybeStopMonitoringSocketsIfNoActiveRequest(); + } maybeScheduleStop(); break; case NsdManager.DISCOVER_SERVICES: @@ -251,6 +382,9 @@ public class NsdService extends INsdManager.Stub { maybeStartDaemon(); } break; + case NsdManager.MDNS_MONITORING_SOCKETS_CLEANUP: + maybeStopMonitoringSockets(); + break; default: Log.e(TAG, "Unhandled " + msg); return NOT_HANDLED; @@ -300,6 +434,47 @@ public class NsdService extends INsdManager.Stub { maybeScheduleStop(); } + private void storeListenerMap(int clientId, int transactionId, MdnsListener listener, + ClientInfo clientInfo) { + clientInfo.mClientIds.put(clientId, transactionId); + clientInfo.mListeners.put(clientId, listener); + mIdToClientInfoMap.put(transactionId, clientInfo); + removeMessages(NsdManager.MDNS_MONITORING_SOCKETS_CLEANUP); + } + + private void removeListenerMap(int clientId, int transactionId, ClientInfo clientInfo) { + clientInfo.mClientIds.delete(clientId); + clientInfo.mListeners.delete(clientId); + mIdToClientInfoMap.remove(transactionId); + maybeStopMonitoringSocketsIfNoActiveRequest(); + } + + /** + * Check the given service type is valid and construct it to a service type + * which can use for discovery / resolution service. + * + *

The valid service type should be 2 labels, or 3 labels if the query is for a + * subtype (see RFC6763 7.1). Each label is up to 63 characters and must start with an + * underscore; they are alphanumerical characters or dashes or underscore, except the + * last one that is just alphanumerical. The last label must be _tcp or _udp. + * + * @param serviceType the request service type for discovery / resolution service + * @return constructed service type or null if the given service type is invalid. + */ + @Nullable + private String constructServiceType(String serviceType) { + if (TextUtils.isEmpty(serviceType)) return null; + + final Pattern serviceTypePattern = Pattern.compile( + "^(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\.)?" + + "(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\._(?:tcp|udp))$"); + final Matcher matcher = serviceTypePattern.matcher(serviceType); + if (!matcher.matches()) return null; + return matcher.group(1) == null + ? serviceType + ".local" + : matcher.group(1) + "._sub" + matcher.group(2) + ".local"; + } + @Override public boolean processMessage(Message msg) { final ClientInfo clientInfo; @@ -325,19 +500,40 @@ public class NsdService extends INsdManager.Stub { break; } - maybeStartDaemon(); + final NsdServiceInfo info = args.serviceInfo; id = getUniqueId(); - if (discoverServices(id, args.serviceInfo)) { - if (DBG) { - Log.d(TAG, "Discover " + msg.arg2 + " " + id - + args.serviceInfo.getServiceType()); + if (mMdnsDiscoveryManager != null) { + final String serviceType = constructServiceType(info.getServiceType()); + if (serviceType == null) { + clientInfo.onDiscoverServicesFailed(clientId, + NsdManager.FAILURE_INTERNAL_ERROR); + break; } - storeRequestMap(clientId, id, clientInfo, msg.what); - clientInfo.onDiscoverServicesStarted(clientId, args.serviceInfo); + + maybeStartMonitoringSockets(); + final MdnsListener listener = + new DiscoveryListener(clientId, id, info, serviceType); + final MdnsSearchOptions options = MdnsSearchOptions.newBuilder() + .setNetwork(info.getNetwork()) + .setIsPassiveMode(true) + .build(); + mMdnsDiscoveryManager.registerListener(serviceType, listener, options); + storeListenerMap(clientId, id, listener, clientInfo); + clientInfo.onDiscoverServicesStarted(clientId, info); } else { - stopServiceDiscovery(id); - clientInfo.onDiscoverServicesFailed(clientId, - NsdManager.FAILURE_INTERNAL_ERROR); + maybeStartDaemon(); + if (discoverServices(id, info)) { + if (DBG) { + Log.d(TAG, "Discover " + msg.arg2 + " " + id + + info.getServiceType()); + } + storeRequestMap(clientId, id, clientInfo, msg.what); + clientInfo.onDiscoverServicesStarted(clientId, info); + } else { + stopServiceDiscovery(id); + clientInfo.onDiscoverServicesFailed(clientId, + NsdManager.FAILURE_INTERNAL_ERROR); + } } break; case NsdManager.STOP_DISCOVERY: @@ -359,12 +555,25 @@ public class NsdService extends INsdManager.Stub { clientId, NsdManager.FAILURE_INTERNAL_ERROR); break; } - removeRequestMap(clientId, id, clientInfo); - if (stopServiceDiscovery(id)) { + if (mMdnsDiscoveryManager != null) { + final MdnsListener listener = clientInfo.mListeners.get(clientId); + if (listener == null) { + clientInfo.onStopDiscoveryFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + } + mMdnsDiscoveryManager.unregisterListener( + listener.getListenedServiceType(), listener); + removeListenerMap(clientId, id, clientInfo); clientInfo.onStopDiscoverySucceeded(clientId); } else { - clientInfo.onStopDiscoveryFailed( - clientId, NsdManager.FAILURE_INTERNAL_ERROR); + removeRequestMap(clientId, id, clientInfo); + if (stopServiceDiscovery(id)) { + clientInfo.onStopDiscoverySucceeded(clientId); + } else { + clientInfo.onStopDiscoveryFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } } break; case NsdManager.REGISTER_SERVICE: @@ -450,6 +659,11 @@ public class NsdService extends INsdManager.Stub { return NOT_HANDLED; } break; + case MDNS_DISCOVERY_MANAGER_EVENT: + if (!handleMdnsDiscoveryManagerEvent(msg.arg1, msg.arg2, msg.obj)) { + return NOT_HANDLED; + } + break; default: return NOT_HANDLED; } @@ -612,6 +826,48 @@ public class NsdService extends INsdManager.Stub { } return true; } + + private NsdServiceInfo buildNsdServiceInfoFromMdnsEvent(final MdnsEvent event) { + final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo; + final String serviceType = event.mRequestedServiceType; + final String serviceName = serviceInfo.getServiceInstanceName(); + final NsdServiceInfo servInfo = new NsdServiceInfo(serviceName, serviceType); + final Network network = serviceInfo.getNetwork(); + setServiceNetworkForCallback( + servInfo, + network == null ? NETID_UNSET : network.netId, + serviceInfo.getInterfaceIndex()); + return servInfo; + } + + private boolean handleMdnsDiscoveryManagerEvent( + int transactionId, int code, Object obj) { + final ClientInfo clientInfo = mIdToClientInfoMap.get(transactionId); + if (clientInfo == null) { + Log.e(TAG, String.format( + "id %d for %d has no client mapping", transactionId, code)); + return false; + } + + final MdnsEvent event = (MdnsEvent) obj; + final int clientId = event.mClientId; + final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event); + if (DBG) { + Log.d(TAG, String.format("MdnsDiscoveryManager event code=%s transactionId=%d", + NsdManager.nameOf(code), transactionId)); + } + switch (code) { + case NsdManager.SERVICE_FOUND: + clientInfo.onServiceFound(clientId, info); + break; + case NsdManager.SERVICE_LOST: + clientInfo.onServiceLost(clientId, info); + break; + default: + return false; + } + return true; + } } } @@ -982,6 +1238,9 @@ public class NsdService extends INsdManager.Stub { /* A map from client id to the type of the request we had received */ private final SparseIntArray mClientRequests = new SparseIntArray(); + /* A map from client id to the MdnsListener */ + private final SparseArray mListeners = new SparseArray<>(); + // The target SDK of this client < Build.VERSION_CODES.S private boolean mIsLegacy = false; @@ -1043,6 +1302,15 @@ public class NsdService extends INsdManager.Stub { mClientRequests.clear(); } + void unregisterAllListeners() { + for (int i = 0; i < mListeners.size(); i++) { + final MdnsListener listener = mListeners.valueAt(i); + mMdnsDiscoveryManager.unregisterListener( + listener.getListenedServiceType(), listener); + } + mListeners.clear(); + } + // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id, // return the corresponding listener id. mDnsClient id is also called a global id. private int getClientId(final int globalId) { diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java index 58a1a4fe98..a1c865f5be 100644 --- a/tests/unit/java/com/android/server/NsdServiceTest.java +++ b/tests/unit/java/com/android/server/NsdServiceTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; @@ -45,6 +46,7 @@ import android.content.ContentResolver; import android.content.Context; import android.net.INetd; import android.net.InetAddresses; +import android.net.Network; import android.net.mdns.aidl.DiscoveryInfo; import android.net.mdns.aidl.GetAddressInfo; import android.net.mdns.aidl.IMDnsEventListener; @@ -70,6 +72,10 @@ import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.server.NsdService.Dependencies; +import com.android.server.connectivity.mdns.MdnsDiscoveryManager; +import com.android.server.connectivity.mdns.MdnsServiceBrowserListener; +import com.android.server.connectivity.mdns.MdnsServiceInfo; +import com.android.server.connectivity.mdns.MdnsSocketProvider; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.HandlerUtils; @@ -86,6 +92,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.LinkedList; +import java.util.List; import java.util.Queue; // TODOs: @@ -99,7 +106,7 @@ public class NsdServiceTest { private static final long CLEANUP_DELAY_MS = 500; private static final long TIMEOUT_MS = 500; private static final String SERVICE_NAME = "a_name"; - private static final String SERVICE_TYPE = "a_type"; + private static final String SERVICE_TYPE = "_test._tcp"; private static final String SERVICE_FULL_NAME = SERVICE_NAME + "." + SERVICE_TYPE; private static final String DOMAIN_NAME = "mytestdevice.local"; private static final int PORT = 2201; @@ -116,6 +123,8 @@ public class NsdServiceTest { @Mock ContentResolver mResolver; @Mock MDnsManager mMockMDnsM; @Mock Dependencies mDeps; + @Mock MdnsDiscoveryManager mDiscoveryManager; + @Mock MdnsSocketProvider mSocketProvider; HandlerThread mThread; TestHandler mHandler; NsdService mService; @@ -558,6 +567,16 @@ public class NsdServiceTest { anyInt()/* interfaceIdx */); } + private void makeServiceWithMdnsDiscoveryManagerEnabled() { + doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class)); + doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any()); + doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any()); + + mService = makeService(); + verify(mDeps).makeMdnsDiscoveryManager(any(), any()); + verify(mDeps).makeMdnsSocketProvider(any(), any()); + } + @Test public void testMdnsDiscoveryManagerFeature() { // Create NsdService w/o feature enabled. @@ -566,12 +585,102 @@ public class NsdServiceTest { verify(mDeps, never()).makeMdnsSocketProvider(any(), any()); // Create NsdService again w/ feature enabled. - doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class)); - makeService(); - verify(mDeps).makeMdnsDiscoveryManager(any(), any()); - verify(mDeps).makeMdnsSocketProvider(any(), any()); + makeServiceWithMdnsDiscoveryManagerEnabled(); } + @Test + public void testDiscoveryWithMdnsDiscoveryManager() { + makeServiceWithMdnsDiscoveryManagerEnabled(); + + final NsdManager client = connectClient(mService); + final DiscoveryListener discListener = mock(DiscoveryListener.class); + final Network network = new Network(999); + final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local"; + // Verify the discovery start / stop. + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(MdnsServiceBrowserListener.class); + client.discoverServices(SERVICE_TYPE, PROTOCOL, network, r -> r.run(), discListener); + waitForIdle(); + verify(mSocketProvider).startMonitoringSockets(); + verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain), + listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork()))); + verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE); + + final MdnsServiceBrowserListener listener = listenerCaptor.getValue(); + final MdnsServiceInfo foundInfo = new MdnsServiceInfo( + SERVICE_NAME, /* serviceInstanceName */ + serviceTypeWithLocalDomain.split("\\."), /* serviceType */ + List.of(), /* subtypes */ + new String[] {"android", "local"}, /* hostName */ + 12345, /* port */ + "192.0.2.0", /* ipv4Address */ + "2001:db8::", /* ipv6Address */ + List.of(), /* textStrings */ + List.of(), /* textEntries */ + 1234, /* interfaceIndex */ + network); + + // Verify onServiceNameDiscovered callback + listener.onServiceNameDiscovered(foundInfo); + verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(argThat(info -> + info.getServiceName().equals(SERVICE_NAME) + && info.getServiceType().equals(SERVICE_TYPE) + && info.getNetwork().equals(network))); + + final MdnsServiceInfo removedInfo = new MdnsServiceInfo( + SERVICE_NAME, /* serviceInstanceName */ + serviceTypeWithLocalDomain.split("\\."), /* serviceType */ + null, /* subtypes */ + null, /* hostName */ + 0, /* port */ + null, /* ipv4Address */ + null, /* ipv6Address */ + null, /* textStrings */ + null, /* textEntries */ + 1234, /* interfaceIndex */ + network); + // Verify onServiceNameRemoved callback + listener.onServiceNameRemoved(removedInfo); + verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info -> + info.getServiceName().equals(SERVICE_NAME) + && info.getServiceType().equals(SERVICE_TYPE) + && info.getNetwork().equals(network))); + + client.stopServiceDiscovery(discListener); + waitForIdle(); + verify(mDiscoveryManager).unregisterListener(eq(serviceTypeWithLocalDomain), any()); + verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStopped(SERVICE_TYPE); + verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).stopMonitoringSockets(); + } + + @Test + public void testDiscoveryWithMdnsDiscoveryManager_FailedWithInvalidServiceType() { + makeServiceWithMdnsDiscoveryManagerEnabled(); + + final NsdManager client = connectClient(mService); + final DiscoveryListener discListener = mock(DiscoveryListener.class); + final Network network = new Network(999); + final String invalidServiceType = "a_service"; + client.discoverServices( + invalidServiceType, PROTOCOL, network, r -> r.run(), discListener); + waitForIdle(); + verify(discListener, timeout(TIMEOUT_MS)) + .onStartDiscoveryFailed(invalidServiceType, FAILURE_INTERNAL_ERROR); + + final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local"; + client.discoverServices( + serviceTypeWithLocalDomain, PROTOCOL, network, r -> r.run(), discListener); + waitForIdle(); + verify(discListener, timeout(TIMEOUT_MS)) + .onStartDiscoveryFailed(serviceTypeWithLocalDomain, FAILURE_INTERNAL_ERROR); + + final String serviceTypeWithoutTcpOrUdpEnding = "_test._com"; + client.discoverServices( + serviceTypeWithoutTcpOrUdpEnding, PROTOCOL, network, r -> r.run(), discListener); + waitForIdle(); + verify(discListener, timeout(TIMEOUT_MS)) + .onStartDiscoveryFailed(serviceTypeWithoutTcpOrUdpEnding, FAILURE_INTERNAL_ERROR); + } private void waitForIdle() { HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);