Merge "[mdns] implement NsdPublisher AIDL service" into main

This commit is contained in:
Handa Wang 2024-02-04 05:23:14 +00:00 • committed by Gerrit Code Review
commit addabc4729
7 changed files with 922 additions and 11 deletions

View file

@ -0,0 +1,335 @@
/*
* Copyright (C) 2024 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.thread;
import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
import android.annotation.NonNull;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdPublisher;
import com.android.server.thread.openthread.INsdStatusReceiver;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Implementation of {@link INsdPublisher}.
*
* <p>This class provides API for service registration and discovery over mDNS. This class is a
* proxy between ot-daemon and NsdManager.
*
* <p>All the data members of this class MUST be accessed in the {@code mHandler}'s Thread except
* {@code mHandler} itself.
*
* <p>TODO: b/323300118 - Remove the following mechanism when the race condition in NsdManager is
* fixed.
*
* <p>There's always only one running registration job at any timepoint. All other pending jobs are
* queued in {@code mRegistrationJobs}. When a registration job is complete (i.e. the according
* method in {@link NsdManager.RegistrationListener} is called), it will start the next registration
* job in the queue.
*/
public final class NsdPublisher extends INsdPublisher.Stub {
// TODO: b/321883491 - specify network for mDNS operations
private static final String TAG = NsdPublisher.class.getSimpleName();
private final NsdManager mNsdManager;
private final Handler mHandler;
private final Executor mExecutor;
private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
@VisibleForTesting
public NsdPublisher(NsdManager nsdManager, Handler handler) {
mNsdManager = nsdManager;
mHandler = handler;
mExecutor = runnable -> mHandler.post(runnable);
}
public static NsdPublisher newInstance(Context context, Handler handler) {
return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
}
@Override
public void registerService(
String hostname,
String name,
String type,
List<String> subTypeList,
int port,
List<DnsTxtAttribute> txt,
INsdStatusReceiver receiver,
int listenerId) {
postRegistrationJob(
() -> {
NsdServiceInfo serviceInfo =
buildServiceInfoForService(
hostname, name, type, subTypeList, port, txt);
registerInternal(serviceInfo, receiver, listenerId, "service");
});
}
private static NsdServiceInfo buildServiceInfoForService(
String hostname,
String name,
String type,
List<String> subTypeList,
int port,
List<DnsTxtAttribute> txt) {
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(name);
if (!TextUtils.isEmpty(hostname)) {
serviceInfo.setHostname(hostname);
}
serviceInfo.setServiceType(type);
serviceInfo.setPort(port);
serviceInfo.setSubtypes(new HashSet<>(subTypeList));
for (DnsTxtAttribute attribute : txt) {
serviceInfo.setAttribute(attribute.name, attribute.value);
}
return serviceInfo;
}
private void registerInternal(
NsdServiceInfo serviceInfo,
INsdStatusReceiver receiver,
int listenerId,
String registrationType) {
checkOnHandlerThread();
Log.i(
TAG,
"Registering "
+ registrationType
+ ". Listener ID: "
+ listenerId
+ ", serviceInfo: "
+ serviceInfo);
RegistrationListener listener = new RegistrationListener(serviceInfo, listenerId, receiver);
mRegistrationListeners.append(listenerId, listener);
try {
mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
} catch (IllegalArgumentException e) {
Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
}
}
public void unregister(INsdStatusReceiver receiver, int listenerId) {
postRegistrationJob(() -> unregisterInternal(receiver, listenerId));
}
public void unregisterInternal(INsdStatusReceiver receiver, int listenerId) {
checkOnHandlerThread();
RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
if (registrationListener == null) {
Log.w(
TAG,
"Failed to unregister service."
+ " Listener ID: "
+ listenerId
+ " The registrationListener is empty.");
return;
}
Log.i(
TAG,
"Unregistering service."
+ " Listener ID: "
+ listenerId
+ " serviceInfo: "
+ registrationListener.mServiceInfo);
registrationListener.addUnregistrationReceiver(receiver);
mNsdManager.unregisterService(registrationListener);
}
private void checkOnHandlerThread() {
if (mHandler.getLooper().getThread() != Thread.currentThread()) {
throw new IllegalStateException(
"Not running on handler Thread: " + Thread.currentThread().getName());
}
}
/** On ot-daemon died, unregister all registrations. */
public void onOtDaemonDied() {
checkOnHandlerThread();
for (int i = 0; i < mRegistrationListeners.size(); ++i) {
try {
mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
} catch (IllegalArgumentException e) {
Log.i(
TAG,
"Failed to unregister."
+ " Listener ID: "
+ mRegistrationListeners.keyAt(i)
+ " serviceInfo: "
+ mRegistrationListeners.valueAt(i).mServiceInfo,
e);
}
}
mRegistrationListeners.clear();
}
// TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
/** Fetch the first job from the queue and run it. See the class doc for more details. */
private void peekAndRun() {
if (mRegistrationJobs.isEmpty()) {
return;
}
Runnable job = mRegistrationJobs.getFirst();
job.run();
}
// TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
/**
* Pop the first job from the queue and run the next job. See the class doc for more details.
*/
private void popAndRunNext() {
if (mRegistrationJobs.isEmpty()) {
Log.i(TAG, "No registration jobs when trying to pop and run next.");
return;
}
mRegistrationJobs.removeFirst();
peekAndRun();
}
private void postRegistrationJob(Runnable registrationJob) {
mHandler.post(
() -> {
mRegistrationJobs.addLast(registrationJob);
if (mRegistrationJobs.size() == 1) {
peekAndRun();
}
});
}
private final class RegistrationListener implements NsdManager.RegistrationListener {
private final NsdServiceInfo mServiceInfo;
private final int mListenerId;
private final INsdStatusReceiver mRegistrationReceiver;
private final List<INsdStatusReceiver> mUnregistrationReceivers;
RegistrationListener(
@NonNull NsdServiceInfo serviceInfo,
int listenerId,
@NonNull INsdStatusReceiver registrationReceiver) {
mServiceInfo = serviceInfo;
mListenerId = listenerId;
mRegistrationReceiver = registrationReceiver;
mUnregistrationReceivers = new ArrayList<>();
}
void addUnregistrationReceiver(@NonNull INsdStatusReceiver unregistrationReceiver) {
mUnregistrationReceivers.add(unregistrationReceiver);
}
@Override
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
mRegistrationListeners.remove(mListenerId);
Log.i(
TAG,
"Failed to register listener ID: "
+ mListenerId
+ " error code: "
+ errorCode
+ " serviceInfo: "
+ serviceInfo);
try {
mRegistrationReceiver.onError(errorCode);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
popAndRunNext();
}
@Override
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
Log.i(
TAG,
"Failed to unregister."
+ "Listener ID: "
+ mListenerId
+ ", error code: "
+ errorCode
+ ", serviceInfo: "
+ serviceInfo);
try {
receiver.onError(errorCode);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
popAndRunNext();
}
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
Log.i(
TAG,
"Registered successfully. "
+ "Listener ID: "
+ mListenerId
+ ", serviceInfo: "
+ serviceInfo);
try {
mRegistrationReceiver.onSuccess();
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
popAndRunNext();
}
@Override
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
Log.i(
TAG,
"Unregistered successfully. "
+ "Listener ID: "
+ mListenerId
+ ", serviceInfo: "
+ serviceInfo);
try {
receiver.onSuccess();
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
mRegistrationListeners.remove(mListenerId);
popAndRunNext();
}
}
}

View file

@ -149,6 +149,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
private final InfraInterfaceController mInfraIfController;
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
// TODO(b/308310823): read supported channel from Thread dameon
@ -178,7 +179,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings) {
ThreadPersistentSettings persistentSettings,
NsdPublisher nsdPublisher) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@ -190,24 +192,27 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
}
public static ThreadNetworkControllerService newInstance(
Context context, ThreadPersistentSettings persistentSettings) {
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
return new ThreadNetworkControllerService(
context,
new Handler(handlerThread.getLooper()),
handler,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings);
persistentSettings,
NsdPublisher.newInstance(context, handler));
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@ -285,7 +290,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
otDaemon.initialize(
mTunIfController.getTunFd(),
mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED));
mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED),
mNsdPublisher);
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
@ -299,7 +305,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mTunIfController.onOtDaemonDied();
mNsdPublisher.onOtDaemonDied();
mOtDaemon = null;
initializeOtDaemon();
}

View file

@ -51,4 +51,5 @@ android_test {
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
platform_apis: true,
}

View file

@ -19,6 +19,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.cts">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>

View file

@ -17,6 +17,7 @@
package android.net.thread.cts;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
@ -32,6 +33,7 @@ import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@ -46,9 +48,12 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
@ -60,6 +65,7 @@ import android.net.thread.ThreadNetworkManager;
import android.os.Build;
import android.os.OutcomeReceiver;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
@ -68,6 +74,7 @@ import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import com.android.testutils.TestNetworkTracker;
import org.junit.After;
import org.junit.Before;
@ -75,16 +82,22 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
@ -97,6 +110,8 @@ public class ThreadNetworkControllerTest {
private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 10 * 1000;
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@ -105,6 +120,7 @@ public class ThreadNetworkControllerTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
@ -123,6 +139,8 @@ public class ThreadNetworkControllerTest {
assumeNotNull(mController);
setEnabledAndWait(mController, true);
mNsdManager = mContext.getSystemService(NsdManager.class);
}
@After
@ -809,6 +827,74 @@ public class ThreadNetworkControllerTest {
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
@Test
public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
TestNetworkTracker testNetwork = setUpTestNetwork();
setEnabledAndWait(mController, true);
leaveAndWait(mController);
NsdServiceInfo serviceInfo =
expectServiceResolved(
MESHCOP_SERVICE_TYPE,
SERVICE_DISCOVERY_TIMEOUT_MILLIS,
s -> s.getAttributes().get("at") == null);
Map<String, byte[]> txtMap = serviceInfo.getAttributes();
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
TestNetworkTracker testNetwork = setUpTestNetwork();
String networkName = "TestNet" + new Random().nextInt(10_000);
joinRandomizedDatasetAndWait(mController, networkName);
Predicate<NsdServiceInfo> predicate =
serviceInfo ->
serviceInfo.getAttributes().get("at") != null
&& Arrays.equals(
serviceInfo.getAttributes().get("nn"),
networkName.getBytes(StandardCharsets.UTF_8));
NsdServiceInfo resolvedService =
expectServiceResolved(
MESHCOP_SERVICE_TYPE, SERVICE_DISCOVERY_TIMEOUT_MILLIS, predicate);
Map<String, byte[]> txtMap = resolvedService.getAttributes();
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(txtMap.get("id").length).isEqualTo(16);
tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_threadDisabled_notDiscovered() throws Exception {
TestNetworkTracker testNetwork = setUpTestNetwork();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
setEnabledAndWait(mController, false);
try {
serviceLostFuture.get(10_000, MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
tearDownTestNetwork(testNetwork);
}
private static void dropAllPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
@ -888,6 +974,12 @@ public class ThreadNetworkControllerTest {
runAsShell(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
}
private void leaveAndWait(ThreadNetworkController controller) throws Exception {
CompletableFuture<Void> future = new CompletableFuture<>();
leave(controller, future::complete);
future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
}
private void scheduleMigration(
ThreadNetworkController controller,
PendingOperationalDataset pendingDataset,
@ -942,9 +1034,9 @@ public class ThreadNetworkControllerTest {
waitForEnabledState(controller, booleanToEnabledState(enabled));
}
private CompletableFuture joinRandomizedDataset(ThreadNetworkController controller)
throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
private CompletableFuture joinRandomizedDataset(
ThreadNetworkController controller, String networkName) throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
runAsShell(
THREAD_NETWORK_PRIVILEGED,
@ -953,7 +1045,12 @@ public class ThreadNetworkControllerTest {
}
private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception {
CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller);
joinRandomizedDatasetAndWait(controller, "TestNet");
}
private void joinRandomizedDatasetAndWait(
ThreadNetworkController controller, String networkName) throws Exception {
CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller, networkName);
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
assertThat(isAttached(controller)).isTrue();
}
@ -1010,4 +1107,103 @@ public class ThreadNetworkControllerTest {
fail("Should not have thrown " + e);
}
}
// Return the first discovered service instance.
private NsdServiceInfo discoverService(String serviceType) throws Exception {
CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
new DefaultDiscoveryListener() {
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
serviceInfoFuture.complete(serviceInfo);
}
};
mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
try {
serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
return serviceInfoFuture.get();
}
private NsdManager.DiscoveryListener discoverForServiceLost(
String serviceType, CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
NsdManager.DiscoveryListener listener =
new DefaultDiscoveryListener() {
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
serviceInfoFuture.complete(serviceInfo);
}
};
mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
return listener;
}
private NsdServiceInfo expectServiceResolved(
String serviceType, int timeoutMilliseconds, Predicate<NsdServiceInfo> predicate)
throws Exception {
NsdServiceInfo discoveredServiceInfo = discoverService(serviceType);
CompletableFuture<NsdServiceInfo> future = new CompletableFuture<>();
NsdManager.ServiceInfoCallback callback =
new DefaultServiceInfoCallback() {
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
if (predicate.test(serviceInfo)) {
future.complete(serviceInfo);
}
}
};
mNsdManager.registerServiceInfoCallback(discoveredServiceInfo, mExecutor, callback);
try {
return future.get(timeoutMilliseconds, MILLISECONDS);
} finally {
mNsdManager.unregisterServiceInfoCallback(callback);
}
}
TestNetworkTracker setUpTestNetwork() {
return runAsShell(
MANAGE_TEST_NETWORKS,
() -> initTestNetwork(mContext, new LinkAddress("2001:db8:123::/64"), 10_000));
}
void tearDownTestNetwork(TestNetworkTracker testNetwork) {
runAsShell(MANAGE_TEST_NETWORKS, () -> testNetwork.teardown());
}
private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
@Override
public void onDiscoveryStarted(String serviceType) {}
@Override
public void onDiscoveryStopped(String serviceType) {}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {}
}
private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
@Override
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
@Override
public void onServiceLost() {}
@Override
public void onServiceInfoCallbackUnregistered() {}
}
}

View file

@ -0,0 +1,367 @@
/*
* Copyright (C) 2024 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.thread;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.os.test.TestLooper;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdStatusReceiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/** Unit tests for {@link NsdPublisher}. */
public final class NsdPublisherTest {
@Mock private NsdManager mMockNsdManager;
@Mock private INsdStatusReceiver mRegistrationReceiver;
@Mock private INsdStatusReceiver mUnregistrationReceiver;
private TestLooper mTestLooper;
private NsdPublisher mNsdPublisher;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mTestLooper.dispatchAll();
assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
assertThat(actualServiceInfo.getAttributes().get("key1"))
.isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
assertThat(actualServiceInfo.getAttributes().get("key2"))
.isEqualTo(new byte[] {(byte) 0x03});
verify(mRegistrationReceiver, times(1)).onSuccess();
}
@Test
public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
mTestLooper.dispatchAll();
assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
assertThat(actualServiceInfo.getAttributes().get("key1"))
.isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
assertThat(actualServiceInfo.getAttributes().get("key2"))
.isEqualTo(new byte[] {(byte) 0x03});
verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
}
@Test
public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
doThrow(new IllegalArgumentException("NsdManager fails"))
.when(mMockNsdManager)
.registerService(any(), anyInt(), any(Executor.class), any());
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
}
@Test
public void unregisterService_nsdManagerSucceeds_serviceUnregistrationSucceeds()
throws Exception {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
mTestLooper.dispatchAll();
verify(mUnregistrationReceiver, times(1)).onSuccess();
}
@Test
public void unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
actualRegistrationListener.onUnregistrationFailed(
actualServiceInfo, FAILURE_INTERNAL_ERROR);
mTestLooper.dispatchAll();
verify(mUnregistrationReceiver, times(1)).onError(0);
}
@Test
public void onOtDaemonDied_unregisterAll() {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
mNsdPublisher.registerService(
null,
"MyService",
"_test._tcp",
List.of("_subtype1", "_subtype2"),
12345,
List.of(txt1, txt2),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdManager.RegistrationListener actualListener1 =
actualRegistrationListenerCaptor.getValue();
actualListener1.onServiceRegistered(actualServiceInfoCaptor.getValue());
mNsdPublisher.registerService(
null,
"MyService2",
"_test._udp",
Collections.emptyList(),
11111,
Collections.emptyList(),
mRegistrationReceiver,
17 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(2))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdManager.RegistrationListener actualListener2 =
actualRegistrationListenerCaptor.getAllValues().get(1);
actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
mNsdPublisher.onOtDaemonDied();
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
}
private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
txtAttribute.name = name;
txtAttribute.value = new byte[value.size()];
for (int i = 0; i < value.size(); ++i) {
txtAttribute.value[i] = value.get(i).byteValue();
}
return txtAttribute;
}
// @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
// thread looper, so TestLooper needs to be created inside each test case to install the
// correct looper.
private void prepareTest() {
mTestLooper = new TestLooper();
Handler handler = new Handler(mTestLooper.getLooper());
mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
}
}

View file

@ -87,6 +87,7 @@ public final class ThreadNetworkControllerServiceTest {
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
@Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
@ -117,12 +118,13 @@ public final class ThreadNetworkControllerServiceTest {
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController,
mMockPersistentSettings);
mMockPersistentSettings,
mMockNsdPublisher);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@Test
public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
mService.initialize();
@ -130,6 +132,7 @@ public final class ThreadNetworkControllerServiceTest {
verify(mMockTunIfController, times(1)).createTunInterface();
assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
}
@Test