blob: 2ed989eff029d6638202a2be68abeb5ef9a83f56 [file] [log] [blame]
/*
* Copyright (C) 2017 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.net.InetAddresses.parseNumericAddress;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND;
import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
import static com.android.server.NsdService.constructServiceType;
import static com.android.testutils.ContextUtils.mockService;
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
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;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ContentResolver;
import android.content.Context;
import android.net.INetd;
import android.net.Network;
import android.net.mdns.aidl.DiscoveryInfo;
import android.net.mdns.aidl.GetAddressInfo;
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdManager.DiscoveryListener;
import android.net.nsd.NsdManager.RegistrationListener;
import android.net.nsd.NsdManager.ResolveListener;
import android.net.nsd.NsdServiceInfo;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
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;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
// TODOs:
// - test client can send requests and receive replies
// - test NSD_ON ENABLE/DISABLED listening
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
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 = "_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;
private static final int IFACE_IDX_ANY = 0;
private static final String IPV4_ADDRESS = "192.0.2.0";
private static final String IPV6_ADDRESS = "2001:db8::";
// Records INsdManagerCallback created when NsdService#connect is called.
// Only accessed on the test thread, since NsdService#connect is called by the NsdManager
// constructor called on the test thread.
private final Queue<INsdManagerCallback> mCreatedCallbacks = new LinkedList<>();
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
@Mock Context mContext;
@Mock ContentResolver mResolver;
@Mock MDnsManager mMockMDnsM;
@Mock Dependencies mDeps;
@Mock MdnsDiscoveryManager mDiscoveryManager;
@Mock MdnsAdvertiser mAdvertiser;
@Mock MdnsSocketProvider mSocketProvider;
HandlerThread mThread;
TestHandler mHandler;
NsdService mService;
private static class LinkToDeathRecorder extends Binder {
IBinder.DeathRecipient mDr;
@Override
public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
super.linkToDeath(recipient, flags);
mDr = recipient;
}
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mThread = new HandlerThread("mock-service-handler");
mThread.start();
mHandler = new TestHandler(mThread.getLooper());
when(mContext.getContentResolver()).thenReturn(mResolver);
mockService(mContext, MDnsManager.class, MDnsManager.MDNS_SERVICE, mMockMDnsM);
if (mContext.getSystemService(MDnsManager.class) == null) {
// Test is using mockito-extended
doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
}
doReturn(true).when(mMockMDnsM).registerService(
anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt());
doReturn(true).when(mMockMDnsM).resolve(
anyInt(), anyString(), anyString(), anyString(), anyInt());
doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any());
doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any());
mService = makeService();
}
@After
public void tearDown() throws Exception {
if (mThread != null) {
mThread.quit();
mThread = null;
}
}
@Test
@DisableCompatChanges({
RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
ENABLE_PLATFORM_MDNS_BACKEND})
public void testPreSClients() throws Exception {
// Pre S client connected, the daemon should be started.
connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
verify(mMockMDnsM, times(1)).registerEventListener(any());
verify(mMockMDnsM, times(1)).startDaemon();
connectClient(mService);
final INsdManagerCallback cb2 = getCallback();
final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
// Daemon has been started, it should not try to start it again.
verify(mMockMDnsM, times(1)).registerEventListener(any());
verify(mMockMDnsM, times(1)).startDaemon();
deathRecipient1.binderDied();
// Still 1 client remains, daemon shouldn't be stopped.
waitForIdle();
verify(mMockMDnsM, never()).stopDaemon();
deathRecipient2.binderDied();
// All clients are disconnected, the daemon should be stopped.
verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
}
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
// Creating an NsdManager will not cause daemon startup.
connectClient(mService);
verify(mMockMDnsM, never()).registerEventListener(any());
verify(mMockMDnsM, never()).startDaemon();
final INsdManagerCallback cb1 = getCallback();
final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
// Creating another NsdManager will not cause daemon startup either.
connectClient(mService);
verify(mMockMDnsM, never()).registerEventListener(any());
verify(mMockMDnsM, never()).startDaemon();
final INsdManagerCallback cb2 = getCallback();
final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
// If there is no active request, try to clean up the daemon but should not do it because
// daemon has not been started.
deathRecipient1.binderDied();
verify(mMockMDnsM, never()).unregisterEventListener(any());
verify(mMockMDnsM, never()).stopDaemon();
deathRecipient2.binderDied();
verify(mMockMDnsM, never()).unregisterEventListener(any());
verify(mMockMDnsM, never()).stopDaemon();
}
private IBinder.DeathRecipient verifyLinkToDeath(INsdManagerCallback cb)
throws Exception {
final IBinder.DeathRecipient dr = ((LinkToDeathRecorder) cb.asBinder()).mDr;
assertNotNull(dr);
return dr;
}
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
verify(mMockMDnsM, never()).registerEventListener(any());
verify(mMockMDnsM, never()).startDaemon();
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
request.setPort(PORT);
// Client registration request
final RegistrationListener listener1 = mock(RegistrationListener.class);
client.registerService(request, PROTOCOL, listener1);
waitForIdle();
verify(mMockMDnsM).registerEventListener(any());
verify(mMockMDnsM).startDaemon();
verify(mMockMDnsM).registerService(
eq(2), eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
// Client discovery request
final DiscoveryListener listener2 = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, listener2);
waitForIdle();
verify(mMockMDnsM).discover(3 /* id */, SERVICE_TYPE, IFACE_IDX_ANY);
// Client resolve request
final ResolveListener listener3 = mock(ResolveListener.class);
client.resolveService(request, listener3);
waitForIdle();
verify(mMockMDnsM).resolve(
4 /* id */, SERVICE_NAME, SERVICE_TYPE, "local." /* domain */, IFACE_IDX_ANY);
// Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
deathRecipient.binderDied();
verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
// checks that request are cleaned
verify(mMockMDnsM).stopOperation(2 /* id */);
verify(mMockMDnsM).stopOperation(3 /* id */);
verify(mMockMDnsM).stopOperation(4 /* id */);
}
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testCleanupDelayNoRequestActive() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
request.setPort(PORT);
final RegistrationListener listener1 = mock(RegistrationListener.class);
client.registerService(request, PROTOCOL, listener1);
waitForIdle();
verify(mMockMDnsM).registerEventListener(any());
verify(mMockMDnsM).startDaemon();
final INsdManagerCallback cb1 = getCallback();
final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
verify(mMockMDnsM).registerService(
eq(2), eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
client.unregisterService(listener1);
waitForIdle();
verify(mMockMDnsM).stopOperation(2 /* id */);
verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
reset(mMockMDnsM);
deathRecipient.binderDied();
// Client disconnects, daemon should not be stopped after CLEANUP_DELAY_MS.
verify(mMockMDnsM, never()).unregisterEventListener(any());
verify(mMockMDnsM, never()).stopDaemon();
}
private IMDnsEventListener getEventListener() {
final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
ArgumentCaptor.forClass(IMDnsEventListener.class);
verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
return listenerCaptor.getValue();
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testDiscoverOnTetheringDownstream() throws Exception {
final NsdManager client = connectClient(mService);
final int interfaceIdx = 123;
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE),
eq(0) /* interfaceIdx */);
// NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
// this needs to use a timeout
verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
discIdCaptor.getValue(),
IMDnsEventListener.SERVICE_FOUND,
SERVICE_NAME,
SERVICE_TYPE,
DOMAIN_NAME,
interfaceIdx,
INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
eventListener.onServiceDiscoveryStatus(discoveryInfo);
waitForIdle();
final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
assertEquals(SERVICE_NAME, foundInfo.getServiceName());
assertEquals(SERVICE_TYPE, foundInfo.getServiceType());
assertNull(foundInfo.getHost());
assertNull(foundInfo.getNetwork());
assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
// After discovering the service, verify resolving it
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(foundInfo, resolveListener);
waitForIdle();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(interfaceIdx));
final int servicePort = 10123;
final ResolutionInfo resolutionInfo = new ResolutionInfo(
resolvIdCaptor.getValue(),
IMDnsEventListener.SERVICE_RESOLVED,
null /* serviceName */,
null /* serviceType */,
null /* domain */,
SERVICE_FULL_NAME,
DOMAIN_NAME,
servicePort,
new byte[0] /* txtRecord */,
interfaceIdx);
doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
eventListener.onServiceResolutionStatus(resolutionInfo);
waitForIdle();
final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
eq(interfaceIdx));
final String serviceAddress = "192.0.2.123";
final GetAddressInfo addressInfo = new GetAddressInfo(
getAddrIdCaptor.getValue(),
IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
SERVICE_FULL_NAME,
serviceAddress,
interfaceIdx,
INetd.LOCAL_NET_ID);
eventListener.onGettingServiceAddressStatus(addressInfo);
waitForIdle();
final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
assertEquals(SERVICE_NAME, resolvedService.getServiceName());
assertEquals("." + SERVICE_TYPE, resolvedService.getServiceType());
assertEquals(parseNumericAddress(serviceAddress), resolvedService.getHost());
assertEquals(servicePort, resolvedService.getPort());
assertNull(resolvedService.getNetwork());
assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testDiscoverOnBlackholeNetwork() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE),
eq(0) /* interfaceIdx */);
// NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
// this needs to use a timeout
verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
discIdCaptor.getValue(),
IMDnsEventListener.SERVICE_FOUND,
SERVICE_NAME,
SERVICE_TYPE,
DOMAIN_NAME,
123 /* interfaceIdx */,
INetd.DUMMY_NET_ID); // netId of the blackhole network
eventListener.onServiceDiscoveryStatus(discoveryInfo);
waitForIdle();
verify(discListener, never()).onServiceFound(any());
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
request.setPort(PORT);
final RegistrationListener regListener = mock(RegistrationListener.class);
client.registerService(request, PROTOCOL, regListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> regIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).registerService(regIdCaptor.capture(),
eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
// Register service successfully.
final RegistrationInfo registrationInfo = new RegistrationInfo(
regIdCaptor.getValue(),
IMDnsEventListener.SERVICE_REGISTERED,
SERVICE_NAME,
SERVICE_TYPE,
PORT,
new byte[0] /* txtRecord */,
IFACE_IDX_ANY);
eventListener.onServiceRegistrationStatus(registrationInfo);
final ArgumentCaptor<NsdServiceInfo> registeredInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
verify(regListener, timeout(TIMEOUT_MS))
.onServiceRegistered(registeredInfoCaptor.capture());
final NsdServiceInfo registeredInfo = registeredInfoCaptor.getValue();
assertEquals(SERVICE_NAME, registeredInfo.getServiceName());
// Fail to register service.
final RegistrationInfo registrationFailedInfo = new RegistrationInfo(
regIdCaptor.getValue(),
IMDnsEventListener.SERVICE_REGISTRATION_FAILED,
null /* serviceName */,
null /* registrationType */,
0 /* port */,
new byte[0] /* txtRecord */,
IFACE_IDX_ANY);
eventListener.onServiceRegistrationStatus(registrationFailedInfo);
verify(regListener, timeout(TIMEOUT_MS))
.onRegistrationFailed(any(), eq(FAILURE_INTERNAL_ERROR));
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testServiceDiscoveryFailed() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE), eq(IFACE_IDX_ANY));
verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
// Fail to discover service.
final DiscoveryInfo discoveryFailedInfo = new DiscoveryInfo(
discIdCaptor.getValue(),
IMDnsEventListener.SERVICE_DISCOVERY_FAILED,
null /* serviceName */,
null /* registrationType */,
null /* domainName */,
IFACE_IDX_ANY,
0 /* netId */);
eventListener.onServiceDiscoveryStatus(discoveryFailedInfo);
verify(discListener, timeout(TIMEOUT_MS))
.onStartDiscoveryFailed(SERVICE_TYPE, FAILURE_INTERNAL_ERROR);
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testServiceResolutionFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(IFACE_IDX_ANY));
// Fail to resolve service.
final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
resolvIdCaptor.getValue(),
IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
null /* serviceName */,
null /* serviceType */,
null /* domain */,
null /* serviceFullName */,
null /* domainName */,
0 /* port */,
new byte[0] /* txtRecord */,
IFACE_IDX_ANY);
eventListener.onServiceResolutionStatus(resolutionFailedInfo);
verify(resolveListener, timeout(TIMEOUT_MS))
.onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testGettingAddressFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(IFACE_IDX_ANY));
// Resolve service successfully.
final ResolutionInfo resolutionInfo = new ResolutionInfo(
resolvIdCaptor.getValue(),
IMDnsEventListener.SERVICE_RESOLVED,
null /* serviceName */,
null /* serviceType */,
null /* domain */,
SERVICE_FULL_NAME,
DOMAIN_NAME,
PORT,
new byte[0] /* txtRecord */,
IFACE_IDX_ANY);
doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
eventListener.onServiceResolutionStatus(resolutionInfo);
waitForIdle();
final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
eq(IFACE_IDX_ANY));
// Fail to get service address.
final GetAddressInfo gettingAddrFailedInfo = new GetAddressInfo(
getAddrIdCaptor.getValue(),
IMDnsEventListener.SERVICE_GET_ADDR_FAILED,
null /* hostname */,
null /* address */,
IFACE_IDX_ANY,
0 /* netId */);
eventListener.onGettingServiceAddressStatus(gettingAddrFailedInfo);
verify(resolveListener, timeout(TIMEOUT_MS))
.onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb = getCallback();
final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb);
deathRecipient.binderDied();
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
verify(mMockMDnsM, never()).registerEventListener(any());
verify(mMockMDnsM, never()).startDaemon();
verify(mMockMDnsM, never()).resolve(anyInt() /* id */, anyString() /* serviceName */,
anyString() /* registrationType */, anyString() /* domain */,
anyInt()/* interfaceIdx */);
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testStopServiceResolution() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(IFACE_IDX_ANY));
final int resolveId = resolvIdCaptor.getValue();
client.stopServiceResolution(resolveListener);
waitForIdle();
verify(mMockMDnsM).stopOperation(resolveId);
verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
request.getServiceName().equals(ns.getServiceName())
&& request.getServiceType().equals(ns.getServiceType())));
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testStopResolutionFailed() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(IFACE_IDX_ANY));
final int resolveId = resolvIdCaptor.getValue();
doReturn(false).when(mMockMDnsM).stopOperation(anyInt());
client.stopServiceResolution(resolveListener);
waitForIdle();
verify(mMockMDnsM).stopOperation(resolveId);
verify(resolveListener, timeout(TIMEOUT_MS)).onStopResolutionFailed(argThat(ns ->
request.getServiceName().equals(ns.getServiceName())
&& request.getServiceType().equals(ns.getServiceType())),
eq(FAILURE_OPERATION_NOT_RUNNING));
}
@Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testStopResolutionDuringGettingAddress() throws RemoteException {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(request, resolveListener);
waitForIdle();
final IMDnsEventListener eventListener = getEventListener();
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
eq("local.") /* domain */, eq(IFACE_IDX_ANY));
// Resolve service successfully.
final ResolutionInfo resolutionInfo = new ResolutionInfo(
resolvIdCaptor.getValue(),
IMDnsEventListener.SERVICE_RESOLVED,
null /* serviceName */,
null /* serviceType */,
null /* domain */,
SERVICE_FULL_NAME,
DOMAIN_NAME,
PORT,
new byte[0] /* txtRecord */,
IFACE_IDX_ANY);
doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
eventListener.onServiceResolutionStatus(resolutionInfo);
waitForIdle();
final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
eq(IFACE_IDX_ANY));
final int getAddrId = getAddrIdCaptor.getValue();
client.stopServiceResolution(resolveListener);
waitForIdle();
verify(mMockMDnsM).stopOperation(getAddrId);
verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
request.getServiceName().equals(ns.getServiceName())
&& request.getServiceType().equals(ns.getServiceType())));
}
private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
String serviceType, List<InetAddress> address, int port, int interfaceIndex,
Network network) {
assertEquals(serviceName, info.getServiceName());
assertEquals(serviceType, info.getServiceType());
assertEquals(address, info.getHostAddresses());
assertEquals(port, info.getPort());
assertEquals(network, info.getNetwork());
assertEquals(interfaceIndex, info.getInterfaceIndex());
}
@Test
public void testRegisterAndUnregisterServiceInfoCallback() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
NsdManager.ServiceInfoCallback.class);
final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final Network network = new Network(999);
request.setNetwork(network);
client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
waitForIdle();
// Verify the registration callback start.
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
verify(mSocketProvider).startMonitoringSockets();
verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain),
listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
SERVICE_NAME,
serviceTypeWithLocalDomain.split("\\."),
List.of(), /* subtypes */
new String[]{"android", "local"}, /* hostName */
PORT,
List.of(IPV4_ADDRESS),
List.of(IPV6_ADDRESS),
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
network);
// Verify onServiceFound callback
listener.onServiceFound(mdnsServiceInfo);
final ArgumentCaptor<NsdServiceInfo> updateInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(1))
.onServiceUpdated(updateInfoCaptor.capture());
verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(0) /* info */, SERVICE_NAME,
SERVICE_TYPE,
List.of(parseNumericAddress(IPV4_ADDRESS), parseNumericAddress(IPV6_ADDRESS)),
PORT, IFACE_IDX_ANY, new Network(999));
// Service addresses changed.
final String v4Address = "192.0.2.1";
final String v6Address = "2001:db8::1";
final MdnsServiceInfo updatedServiceInfo = new MdnsServiceInfo(
SERVICE_NAME,
serviceTypeWithLocalDomain.split("\\."),
List.of(), /* subtypes */
new String[]{"android", "local"}, /* hostName */
PORT,
List.of(v4Address),
List.of(v6Address),
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
network);
// Verify onServiceUpdated callback.
listener.onServiceUpdated(updatedServiceInfo);
verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(2))
.onServiceUpdated(updateInfoCaptor.capture());
verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(2) /* info */, SERVICE_NAME,
SERVICE_TYPE,
List.of(parseNumericAddress(v4Address), parseNumericAddress(v6Address)),
PORT, IFACE_IDX_ANY, new Network(999));
// Verify service callback unregistration.
client.unregisterServiceInfoCallback(serviceInfoCallback);
waitForIdle();
verify(serviceInfoCallback, timeout(TIMEOUT_MS)).onServiceInfoCallbackUnregistered();
}
@Test
public void testRegisterServiceCallbackFailed() {
final NsdManager client = connectClient(mService);
final String invalidServiceType = "a_service";
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, invalidServiceType);
final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
NsdManager.ServiceInfoCallback.class);
client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
waitForIdle();
// Fail to register service callback.
verify(serviceInfoCallback, timeout(TIMEOUT_MS))
.onServiceInfoCallbackRegistrationFailed(eq(FAILURE_BAD_PARAMETERS));
}
@Test
public void testUnregisterNotRegisteredCallback() {
final NsdManager client = connectClient(mService);
final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
NsdManager.ServiceInfoCallback.class);
assertThrows(IllegalArgumentException.class, () ->
client.unregisterServiceInfoCallback(serviceInfoCallback));
}
private void setMdnsDiscoveryManagerEnabled() {
doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
}
private void setMdnsAdvertiserEnabled() {
doReturn(true).when(mDeps).isMdnsAdvertiserEnabled(any(Context.class));
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
final DiscoveryListener discListenerWithoutFeature = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, discListenerWithoutFeature);
waitForIdle();
final ArgumentCaptor<Integer> legacyIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).discover(legacyIdCaptor.capture(), any(), anyInt());
verifyNoMoreInteractions(mDiscoveryManager);
setMdnsDiscoveryManagerEnabled();
final DiscoveryListener discListenerWithFeature = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, discListenerWithFeature);
waitForIdle();
final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain),
listenerCaptor.capture(), any());
client.stopServiceDiscovery(discListenerWithoutFeature);
waitForIdle();
verify(mMockMDnsM).stopOperation(legacyIdCaptor.getValue());
client.stopServiceDiscovery(discListenerWithFeature);
waitForIdle();
verify(mDiscoveryManager).unregisterListener(serviceTypeWithLocalDomain,
listenerCaptor.getValue());
}
@Test
public void testDiscoveryWithMdnsDiscoveryManager() {
setMdnsDiscoveryManagerEnabled();
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<MdnsServiceBrowserListener> 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 */
List.of(IPV4_ADDRESS),
List.of(IPV6_ADDRESS),
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 */
List.of(), /* ipv4Address */
List.of(), /* 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)).requestStopWhenInactive();
}
@Test
public void testDiscoveryWithMdnsDiscoveryManager_FailedWithInvalidServiceType() {
setMdnsDiscoveryManagerEnabled();
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);
}
@Test
public void testResolutionWithMdnsDiscoveryManager() throws UnknownHostException {
setMdnsDiscoveryManagerEnabled();
final NsdManager client = connectClient(mService);
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
final String constructedServiceType = "_nsd._sub._service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
request.setNetwork(network);
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
listenerCaptor.capture(), argThat(options ->
network.equals(options.getNetwork())
&& SERVICE_NAME.equals(options.getResolveInstanceName())));
final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
SERVICE_NAME,
constructedServiceType.split("\\."),
List.of(), /* subtypes */
new String[]{"android", "local"}, /* hostName */
PORT,
List.of(IPV4_ADDRESS),
List.of("2001:db8::1", "2001:db8::2"),
List.of() /* textStrings */,
List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
1234,
network);
// Verify onServiceFound callback
listener.onServiceFound(mdnsServiceInfo);
final ArgumentCaptor<NsdServiceInfo> infoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(infoCaptor.capture());
final NsdServiceInfo info = infoCaptor.getValue();
assertEquals(SERVICE_NAME, info.getServiceName());
assertEquals("." + serviceType, info.getServiceType());
assertEquals(PORT, info.getPort());
assertTrue(info.getAttributes().containsKey("key"));
assertEquals(1, info.getAttributes().size());
assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE}, info.getAttributes().get("key"));
assertEquals(parseNumericAddress(IPV4_ADDRESS), info.getHost());
assertEquals(3, info.getHostAddresses().size());
assertTrue(info.getHostAddresses().stream().anyMatch(
address -> address.equals(parseNumericAddress("2001:db8::1"))));
assertTrue(info.getHostAddresses().stream().anyMatch(
address -> address.equals(parseNumericAddress("2001:db8::2"))));
assertEquals(network, info.getNetwork());
// Verify the listener has been unregistered.
verify(mDiscoveryManager, timeout(TIMEOUT_MS))
.unregisterListener(eq(constructedServiceType), any());
verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testMdnsAdvertiserFeatureFlagging() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
regInfo.setPort(12345);
final RegistrationListener regListenerWithoutFeature = mock(RegistrationListener.class);
client.registerService(regInfo, PROTOCOL, regListenerWithoutFeature);
waitForIdle();
final ArgumentCaptor<Integer> legacyIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMockMDnsM).registerService(legacyIdCaptor.capture(), any(), any(), anyInt(),
any(), anyInt());
verifyNoMoreInteractions(mAdvertiser);
setMdnsAdvertiserEnabled();
final RegistrationListener regListenerWithFeature = mock(RegistrationListener.class);
client.registerService(regInfo, PROTOCOL, regListenerWithFeature);
waitForIdle();
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(serviceIdCaptor.capture(),
argThat(info -> matches(info, regInfo)));
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
verify(mMockMDnsM).stopOperation(legacyIdCaptor.getValue());
verify(mAdvertiser, never()).removeService(anyInt());
client.unregisterService(regListenerWithFeature);
waitForIdle();
verify(mAdvertiser).removeService(serviceIdCaptor.getValue());
}
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testTypeSpecificFeatureFlagging() {
doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
doReturn(true).when(mDeps).isFeatureEnabled(any(),
eq("mdns_discovery_manager_allowlist_flag1_version"));
doReturn(true).when(mDeps).isFeatureEnabled(any(),
eq("mdns_advertiser_allowlist_flag2_version"));
final NsdManager client = connectClient(mService);
final NsdServiceInfo service1 = new NsdServiceInfo(SERVICE_NAME, "_type1._tcp");
service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service1.setPort(1234);
final NsdServiceInfo service2 = new NsdServiceInfo(SERVICE_NAME, "_type2._tcp");
service2.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service2.setPort(1234);
client.discoverServices(service1.getServiceType(),
NsdManager.PROTOCOL_DNS_SD, mock(DiscoveryListener.class));
client.discoverServices(service2.getServiceType(),
NsdManager.PROTOCOL_DNS_SD, mock(DiscoveryListener.class));
waitForIdle();
// The DiscoveryManager is enabled for _type1 but not _type2
verify(mDiscoveryManager).registerListener(eq("_type1._tcp.local"), any(), any());
verify(mDiscoveryManager, never()).registerListener(
eq("_type2._tcp.local"), any(), any());
client.resolveService(service1, mock(ResolveListener.class));
client.resolveService(service2, mock(ResolveListener.class));
waitForIdle();
// Same behavior for resolve
verify(mDiscoveryManager, times(2)).registerListener(
eq("_type1._tcp.local"), any(), any());
verify(mDiscoveryManager, never()).registerListener(
eq("_type2._tcp.local"), any(), any());
client.registerService(service1, NsdManager.PROTOCOL_DNS_SD,
mock(RegistrationListener.class));
client.registerService(service2, NsdManager.PROTOCOL_DNS_SD,
mock(RegistrationListener.class));
waitForIdle();
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addService(anyInt(), argThat(info -> matches(info, service1)));
verify(mAdvertiser).addService(anyInt(), argThat(info -> matches(info, service2)));
}
@Test
public void testAdvertiseWithMdnsAdvertiser() {
setMdnsAdvertiserEnabled();
final NsdManager client = connectClient(mService);
final RegistrationListener regListener = mock(RegistrationListener.class);
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
regInfo.setPort(12345);
regInfo.setAttribute("testattr", "testvalue");
regInfo.setNetwork(new Network(999));
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
matches(info, regInfo)));
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(argThat(info -> matches(info,
new NsdServiceInfo(regInfo.getServiceName(), null))));
client.unregisterService(regListener);
waitForIdle();
verify(mAdvertiser).removeService(idCaptor.getValue());
verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
argThat(info -> matches(info, regInfo)));
verify(mSocketProvider, timeout(TIMEOUT_MS)).requestStopWhenInactive();
}
@Test
public void testAdvertiseWithMdnsAdvertiser_FailedWithInvalidServiceType() {
setMdnsAdvertiserEnabled();
final NsdManager client = connectClient(mService);
final RegistrationListener regListener = mock(RegistrationListener.class);
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
regInfo.setHost(parseNumericAddress("192.0.2.123"));
regInfo.setPort(12345);
regInfo.setAttribute("testattr", "testvalue");
regInfo.setNetwork(new Network(999));
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mAdvertiser, never()).addService(anyInt(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
}
@Test
public void testAdvertiseWithMdnsAdvertiser_LongServiceName() {
setMdnsAdvertiserEnabled();
final NsdManager client = connectClient(mService);
final RegistrationListener regListener = mock(RegistrationListener.class);
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
regInfo.setPort(12345);
regInfo.setAttribute("testattr", "testvalue");
regInfo.setNetwork(new Network(999));
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addService(idCaptor.capture(),
argThat(info -> info.getServiceName().equals("a".repeat(63))));
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
}
@Test
public void testStopServiceResolutionWithMdnsDiscoveryManager() {
setMdnsDiscoveryManagerEnabled();
final NsdManager client = connectClient(mService);
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
final String constructedServiceType = "_nsd._sub._service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
request.setNetwork(network);
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
client.stopServiceResolution(resolveListener);
waitForIdle();
// Verify the listener has been unregistered.
verify(mDiscoveryManager, timeout(TIMEOUT_MS))
.unregisterListener(eq(constructedServiceType), eq(listenerCaptor.getValue()));
verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
request.getServiceName().equals(ns.getServiceName())
&& request.getServiceType().equals(ns.getServiceType())));
verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
}
@Test
public void testConstructServiceType() {
final String serviceType1 = "test._tcp";
final String serviceType2 = "_test._quic";
final String serviceType3 = "_123._udp.";
final String serviceType4 = "_TEST._999._tcp.";
assertEquals(null, constructServiceType(serviceType1));
assertEquals(null, constructServiceType(serviceType2));
assertEquals("_123._udp", constructServiceType(serviceType3));
assertEquals("_TEST._sub._999._tcp", constructServiceType(serviceType4));
}
@Test
@EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
public void testEnablePlatformMdnsBackend() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
final Network network = new Network(999);
regInfo.setHostAddresses(List.of(parseNumericAddress("192.0.2.123")));
regInfo.setPort(12345);
regInfo.setAttribute("testattr", "testvalue");
regInfo.setNetwork(network);
// Verify the registration uses MdnsAdvertiser
final RegistrationListener regListener = mock(RegistrationListener.class);
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
verify(mAdvertiser).addService(anyInt(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(SERVICE_TYPE, PROTOCOL, network, r -> r.run(), discListener);
waitForIdle();
verify(mDiscoveryManager).registerListener(anyString(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final ResolveListener resolveListener = mock(ResolveListener.class);
client.resolveService(regInfo, r -> r.run(), resolveListener);
waitForIdle();
verify(mDiscoveryManager, times(2)).registerListener(anyString(), any(), any());
}
private void waitForIdle() {
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
}
NsdService makeService() {
final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS, mDeps) {
@Override
public INsdServiceConnector connect(INsdManagerCallback baseCb,
boolean runNewMdnsBackend) {
// Wrap the callback in a transparent mock, to mock asBinder returning a
// LinkToDeathRecorder. This will allow recording the binder death recipient
// registered on the callback. Use a transparent mock and not a spy as the actual
// implementation class is not public and cannot be spied on by Mockito.
final INsdManagerCallback cb = mock(INsdManagerCallback.class,
AdditionalAnswers.delegatesTo(baseCb));
doReturn(new LinkToDeathRecorder()).when(cb).asBinder();
mCreatedCallbacks.add(cb);
return super.connect(cb, runNewMdnsBackend);
}
};
return service;
}
private INsdManagerCallback getCallback() {
return mCreatedCallbacks.remove();
}
NsdManager connectClient(NsdService service) {
final NsdManager nsdManager = new NsdManager(mContext, service);
// Wait for client registration done.
waitForIdle();
return nsdManager;
}
void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception {
waitForIdle();
// Stop daemon shouldn't be called immediately.
verify(mMockMDnsM, never()).unregisterEventListener(any());
verify(mMockMDnsM, never()).stopDaemon();
// Clean up the daemon after CLEANUP_DELAY_MS.
verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).unregisterEventListener(any());
verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
}
/**
* Return true if two service info are the same.
*
* Useful for argument matchers as {@link NsdServiceInfo} does not implement equals.
*/
private boolean matches(NsdServiceInfo a, NsdServiceInfo b) {
return Objects.equals(a.getServiceName(), b.getServiceName())
&& Objects.equals(a.getServiceType(), b.getServiceType())
&& Objects.equals(a.getHost(), b.getHost())
&& Objects.equals(a.getNetwork(), b.getNetwork())
&& Objects.equals(a.getAttributes(), b.getAttributes());
}
public static class TestHandler extends Handler {
public Message lastMessage;
TestHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
lastMessage = obtainMessage();
lastMessage.copyFrom(msg);
}
}
}