Merge "[Thread] add CTS tests for ThreadNetworkException" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index fba7d69..e4e6c70 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -83,7 +83,6 @@
],
manifest: "AndroidManifestBase.xml",
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
},
}
@@ -103,9 +102,7 @@
],
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
-
},
}
@@ -122,9 +119,7 @@
],
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
-
},
}
@@ -198,9 +193,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: {
- strict_updatability_linting: true,
- },
}
// Updatable tethering packaged for finalized API
@@ -216,10 +208,6 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: {
- strict_updatability_linting: true,
-
- },
}
android_app {
@@ -236,9 +224,7 @@
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
-
},
}
diff --git a/framework/Android.bp b/framework/Android.bp
index 31ddb53..1356eea 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -107,9 +107,6 @@
apex_available: [
"com.android.tethering",
],
- lint: {
- strict_updatability_linting: true,
- },
}
java_library {
diff --git a/service/Android.bp b/service/Android.bp
index 4394f03..c6ecadd 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -202,7 +202,6 @@
"com.android.tethering",
],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
},
@@ -274,9 +273,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: {
- strict_updatability_linting: true,
- },
}
// A special library created strictly for use by the tests as they need the
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
new file mode 100644
index 0000000..c74c023
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -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();
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index b5f7230..21e3927 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -149,6 +149,7 @@
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 @@
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
- ThreadPersistentSettings persistentSettings) {
+ ThreadPersistentSettings persistentSettings,
+ NsdPublisher nsdPublisher) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -190,24 +192,27 @@
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 @@
}
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 @@
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mTunIfController.onOtDaemonDied();
-
+ mNsdPublisher.onOtDaemonDied();
mOtDaemon = null;
initializeOtDaemon();
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 81e24da..522120c 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -51,4 +51,5 @@
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
+ platform_apis: true,
}
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
index 4370fe3..1541bf5 100644
--- a/thread/tests/cts/AndroidManifest.xml
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -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>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index aab4b2e..3bec36b 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -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 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 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.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.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.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 @@
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 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
+ private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
@@ -123,6 +139,8 @@
assumeNotNull(mController);
setEnabledAndWait(mController, true);
+
+ mNsdManager = mContext.getSystemService(NsdManager.class);
}
@After
@@ -809,6 +827,74 @@
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 @@
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 @@
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 @@
}
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 @@
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() {}
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
new file mode 100644
index 0000000..8aea0a3
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -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);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 1d83abc..f626edf 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -87,6 +87,7 @@
@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 @@
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 @@
verify(mMockTunIfController, times(1)).createTunInterface();
assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+ assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
}
@Test