441 lines
18 KiB
Java
441 lines
18 KiB
Java
/*
|
|
* 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 libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
|
|
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
|
|
|
|
import static org.junit.Assert.assertEquals;
|
|
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.Mockito.any;
|
|
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.when;
|
|
|
|
import android.compat.testing.PlatformCompatChangeRule;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.net.INetd;
|
|
import android.net.InetAddresses;
|
|
import android.net.mdns.aidl.DiscoveryInfo;
|
|
import android.net.mdns.aidl.GetAddressInfo;
|
|
import android.net.mdns.aidl.IMDnsEventListener;
|
|
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.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 androidx.annotation.NonNull;
|
|
import androidx.test.filters.SmallTest;
|
|
|
|
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.util.LinkedList;
|
|
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.R)
|
|
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;
|
|
|
|
// 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;
|
|
HandlerThread mThread;
|
|
TestHandler mHandler;
|
|
|
|
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);
|
|
doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
|
|
.getSystemServiceName(MDnsManager.class);
|
|
doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
|
|
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());
|
|
}
|
|
|
|
@After
|
|
public void tearDown() throws Exception {
|
|
if (mThread != null) {
|
|
mThread.quit();
|
|
mThread = null;
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
|
|
public void testPreSClients() throws Exception {
|
|
NsdService service = makeService();
|
|
|
|
// Pre S client connected, the daemon should be started.
|
|
connectClient(service);
|
|
waitForIdle();
|
|
final INsdManagerCallback cb1 = getCallback();
|
|
final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
|
|
verify(mMockMDnsM, times(1)).registerEventListener(any());
|
|
verify(mMockMDnsM, times(1)).startDaemon();
|
|
|
|
connectClient(service);
|
|
waitForIdle();
|
|
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(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
|
|
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
|
|
final NsdService service = makeService();
|
|
|
|
// Creating an NsdManager will not cause daemon startup.
|
|
connectClient(service);
|
|
waitForIdle();
|
|
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(service);
|
|
waitForIdle();
|
|
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(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
|
|
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
|
|
NsdService service = makeService();
|
|
|
|
NsdManager client = connectClient(service);
|
|
waitForIdle();
|
|
final INsdManagerCallback cb1 = getCallback();
|
|
final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
|
|
verify(mMockMDnsM, never()).registerEventListener(any());
|
|
verify(mMockMDnsM, never()).startDaemon();
|
|
|
|
NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
|
|
request.setPort(2201);
|
|
|
|
// Client registration request
|
|
NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
|
|
client.registerService(request, PROTOCOL, listener1);
|
|
waitForIdle();
|
|
verify(mMockMDnsM, times(1)).registerEventListener(any());
|
|
verify(mMockMDnsM, times(1)).startDaemon();
|
|
verify(mMockMDnsM, times(1)).registerService(
|
|
eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
|
|
|
|
// Client discovery request
|
|
NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
|
|
client.discoverServices("a_type", PROTOCOL, listener2);
|
|
waitForIdle();
|
|
verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0));
|
|
|
|
// Client resolve request
|
|
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
|
|
client.resolveService(request, listener3);
|
|
waitForIdle();
|
|
verify(mMockMDnsM, times(1)).resolve(
|
|
eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0));
|
|
|
|
// Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
|
|
deathRecipient.binderDied();
|
|
verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
|
|
// checks that request are cleaned
|
|
verify(mMockMDnsM, times(1)).stopOperation(eq(2));
|
|
verify(mMockMDnsM, times(1)).stopOperation(eq(3));
|
|
verify(mMockMDnsM, times(1)).stopOperation(eq(4));
|
|
}
|
|
|
|
@Test
|
|
@EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
|
|
public void testCleanupDelayNoRequestActive() throws Exception {
|
|
NsdService service = makeService();
|
|
NsdManager client = connectClient(service);
|
|
|
|
NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
|
|
request.setPort(2201);
|
|
NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
|
|
client.registerService(request, PROTOCOL, listener1);
|
|
waitForIdle();
|
|
verify(mMockMDnsM, times(1)).registerEventListener(any());
|
|
verify(mMockMDnsM, times(1)).startDaemon();
|
|
final INsdManagerCallback cb1 = getCallback();
|
|
final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
|
|
verify(mMockMDnsM, times(1)).registerService(
|
|
eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
|
|
|
|
client.unregisterService(listener1);
|
|
waitForIdle();
|
|
verify(mMockMDnsM, times(1)).stopOperation(eq(2));
|
|
|
|
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();
|
|
}
|
|
|
|
@Test
|
|
public void testDiscoverOnTetheringDownstream() throws Exception {
|
|
NsdService service = makeService();
|
|
NsdManager client = connectClient(service);
|
|
|
|
final String serviceType = "a_type";
|
|
final String serviceName = "a_name";
|
|
final String domainName = "mytestdevice.local";
|
|
final int interfaceIdx = 123;
|
|
final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
|
|
client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
|
|
waitForIdle();
|
|
|
|
final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
|
|
ArgumentCaptor.forClass(IMDnsEventListener.class);
|
|
verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
|
|
final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
|
|
verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
|
|
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(serviceType);
|
|
|
|
final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
|
|
discIdCaptor.getValue(),
|
|
IMDnsEventListener.SERVICE_FOUND,
|
|
serviceName,
|
|
serviceType,
|
|
domainName,
|
|
interfaceIdx,
|
|
INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
|
|
final IMDnsEventListener eventListener = listenerCaptor.getValue();
|
|
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(serviceName, foundInfo.getServiceName());
|
|
assertEquals(serviceType, foundInfo.getServiceType());
|
|
assertNull(foundInfo.getHost());
|
|
assertNull(foundInfo.getNetwork());
|
|
assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
|
|
|
|
// After discovering the service, verify resolving it
|
|
final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
|
|
client.resolveService(foundInfo, resolveListener);
|
|
waitForIdle();
|
|
|
|
final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
|
|
verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
|
|
eq("local.") /* domain */, eq(interfaceIdx));
|
|
|
|
final int servicePort = 10123;
|
|
final String serviceFullName = serviceName + "." + serviceType;
|
|
final ResolutionInfo resolutionInfo = new ResolutionInfo(
|
|
resolvIdCaptor.getValue(),
|
|
IMDnsEventListener.SERVICE_RESOLVED,
|
|
null /* serviceName */,
|
|
null /* serviceType */,
|
|
null /* domain */,
|
|
serviceFullName,
|
|
domainName,
|
|
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(domainName),
|
|
eq(interfaceIdx));
|
|
|
|
final String serviceAddress = "192.0.2.123";
|
|
final GetAddressInfo addressInfo = new GetAddressInfo(
|
|
getAddrIdCaptor.getValue(),
|
|
IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
|
|
serviceFullName,
|
|
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(serviceName, resolvedService.getServiceName());
|
|
assertEquals("." + serviceType, resolvedService.getServiceType());
|
|
assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
|
|
assertEquals(servicePort, resolvedService.getPort());
|
|
assertNull(resolvedService.getNetwork());
|
|
assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
|
|
}
|
|
|
|
private void waitForIdle() {
|
|
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
|
|
}
|
|
|
|
NsdService makeService() {
|
|
final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS) {
|
|
@Override
|
|
public INsdServiceConnector connect(INsdManagerCallback baseCb) {
|
|
// 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);
|
|
}
|
|
};
|
|
return service;
|
|
}
|
|
|
|
private INsdManagerCallback getCallback() {
|
|
return mCreatedCallbacks.remove();
|
|
}
|
|
|
|
NsdManager connectClient(NsdService service) {
|
|
return new NsdManager(mContext, service);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|