diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java index baf716f3c5..353db10b62 100644 --- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java +++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java @@ -17,10 +17,7 @@ package android.net.thread; import static android.Manifest.permission.MANAGE_TEST_NETWORKS; -import static android.Manifest.permission.NETWORK_SETTINGS; -import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT; -import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet; import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source; import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup; @@ -36,13 +33,14 @@ import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork; import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.io.BaseEncoding.base16; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static java.util.Objects.requireNonNull; + import android.content.Context; import android.net.InetAddresses; import android.net.LinkProperties; @@ -54,6 +52,7 @@ import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; +import android.net.thread.utils.ThreadNetworkControllerWrapper; import android.os.Handler; import android.os.HandlerThread; @@ -74,9 +73,6 @@ import java.net.Inet6Address; import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.function.Predicate; /** Integration test cases for Thread Border Routing feature. */ @@ -108,7 +104,8 @@ public class BorderRoutingTest { @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule(); private final Context mContext = ApplicationProvider.getApplicationContext(); - private ThreadNetworkController mController; + private final ThreadNetworkControllerWrapper mController = + ThreadNetworkControllerWrapper.newInstance(mContext); private OtDaemonController mOtCtl; private HandlerThread mHandlerThread; private Handler mHandler; @@ -119,11 +116,6 @@ public class BorderRoutingTest { @Before public void setUp() throws Exception { - final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class); - if (manager != null) { - mController = manager.getAllThreadNetworkControllers().get(0); - } - // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network mOtCtl = new OtDaemonController(); mOtCtl.factoryReset(); @@ -134,9 +126,7 @@ public class BorderRoutingTest { mFtds = new ArrayList<>(); setUpInfraNetwork(); - - // BR forms a network. - startBrLeader(); + mController.joinAndWait(DEFAULT_DATASET); // Creates a infra network device. mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler); @@ -150,16 +140,8 @@ public class BorderRoutingTest { @After public void tearDown() throws Exception { - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - NETWORK_SETTINGS, - () -> { - CountDownLatch latch = new CountDownLatch(2); - mController.setTestNetworkAsUpstream( - null, directExecutor(), v -> latch.countDown()); - mController.leave(directExecutor(), v -> latch.countDown()); - latch.await(10, TimeUnit.SECONDS); - }); + mController.setTestNetworkAsUpstreamAndWait(null); + mController.leaveAndWait(); tearDownInfraNetwork(); mHandlerThread.quitSafely(); @@ -205,9 +187,6 @@ public class BorderRoutingTest { * */ - // Form the network. - mOtCtl.factoryReset(); - startBrLeader(); startInfraDevice(); FullThreadDevice ftd = mFtds.get(0); startFtdChild(ftd); @@ -229,12 +208,10 @@ public class BorderRoutingTest { * */ - // Creates a Full Thread Device (FTD) and lets it join the network. FullThreadDevice ftd = mFtds.get(0); startFtdChild(ftd); - Inet6Address ftdOmr = ftd.getOmrAddress(); - Inet6Address ftdMlEid = ftd.getMlEid(); - assertNotNull(ftdMlEid); + Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress()); + Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid()); ftd.udpBind(ftdOmr, 12345); sendUdpMessage(ftdOmr, 12345, "aaaaaaaa"); @@ -588,38 +565,21 @@ public class BorderRoutingTest { pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4)); } - private void setUpInfraNetwork() { + private void setUpInfraNetwork() throws Exception { mInfraNetworkTracker = runAsShell( MANAGE_TEST_NETWORKS, () -> initTestNetwork( mContext, new LinkProperties(), 5000 /* timeoutMs */)); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - NETWORK_SETTINGS, - () -> { - CompletableFuture future = new CompletableFuture<>(); - mController.setTestNetworkAsUpstream( - mInfraNetworkTracker.getTestIface().getInterfaceName(), - directExecutor(), - future::complete); - future.get(5, TimeUnit.SECONDS); - }); + mController.setTestNetworkAsUpstreamAndWait( + mInfraNetworkTracker.getTestIface().getInterfaceName()); } private void tearDownInfraNetwork() { runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown()); } - private void startBrLeader() throws Exception { - CompletableFuture joinFuture = new CompletableFuture<>(); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete)); - joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS); - } - private void startFtdChild(FullThreadDevice ftd) throws Exception { ftd.factoryReset(); ftd.joinNetwork(DEFAULT_DATASET); diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java index 4e6ee5f777..56b658d09f 100644 --- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java +++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java @@ -16,11 +16,8 @@ package android.net.thread; -import static android.Manifest.permission.NETWORK_SETTINGS; import static android.net.InetAddresses.parseNumericAddress; -import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT; -import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost; import static android.net.thread.utils.IntegrationTestUtils.discoverService; @@ -28,16 +25,12 @@ import static android.net.thread.utils.IntegrationTestUtils.resolveService; import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil; import static android.net.thread.utils.IntegrationTestUtils.waitFor; -import static com.android.testutils.TestPermissionUtil.runAsShell; - import static com.google.common.io.BaseEncoding.base16; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.junit.Assert.assertThrows; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; import android.net.nsd.NsdManager; @@ -47,6 +40,7 @@ import android.net.thread.utils.TapTestNetworkTracker; import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; +import android.net.thread.utils.ThreadNetworkControllerWrapper; import android.os.HandlerThread; import androidx.test.core.app.ApplicationProvider; @@ -99,27 +93,18 @@ public class ServiceDiscoveryTest { @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule(); private final Context mContext = ApplicationProvider.getApplicationContext(); + private final ThreadNetworkControllerWrapper mController = + ThreadNetworkControllerWrapper.newInstance(mContext); private HandlerThread mHandlerThread; - private ThreadNetworkController mController; private NsdManager mNsdManager; private TapTestNetworkTracker mTestNetworkTracker; private List mFtds; @Before public void setUp() throws Exception { - final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class); - if (manager != null) { - mController = manager.getAllThreadNetworkControllers().get(0); - } - - // BR forms a network. - CompletableFuture joinFuture = new CompletableFuture<>(); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete)); - joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS); + mController.joinAndWait(DEFAULT_DATASET); mNsdManager = mContext.getSystemService(NsdManager.class); mHandlerThread = new HandlerThread(TAG); @@ -127,17 +112,8 @@ public class ServiceDiscoveryTest { mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper()); assertThat(mTestNetworkTracker).isNotNull(); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - NETWORK_SETTINGS, - () -> { - CompletableFuture future = new CompletableFuture<>(); - mController.setTestNetworkAsUpstream( - mTestNetworkTracker.getInterfaceName(), - directExecutor(), - v -> future.complete(null)); - future.get(5, SECONDS); - }); + mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName()); + // Create the FTDs in setUp() so that the FTDs can be safely released in tearDown(). // Don't create new FTDs in test cases. mFtds = new ArrayList<>(); @@ -164,18 +140,8 @@ public class ServiceDiscoveryTest { mHandlerThread.quitSafely(); mHandlerThread.join(); } - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - NETWORK_SETTINGS, - () -> { - CompletableFuture setUpstreamFuture = new CompletableFuture<>(); - CompletableFuture leaveFuture = new CompletableFuture<>(); - mController.setTestNetworkAsUpstream( - null, directExecutor(), v -> setUpstreamFuture.complete(null)); - mController.leave(directExecutor(), v -> leaveFuture.complete(null)); - setUpstreamFuture.get(5, SECONDS); - leaveFuture.get(5, SECONDS); - }); + mController.setTestNetworkAsUpstreamAndWait(null); + mController.leaveAndWait(); } @Test diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java index 9585d7d46d..4a006cf729 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java @@ -16,31 +16,22 @@ package android.net.thread; -import static android.Manifest.permission.NETWORK_SETTINGS; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED; -import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT; -import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT; -import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf; import static com.android.compatibility.common.util.SystemUtil.runShellCommand; -import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -import android.annotation.Nullable; import android.content.Context; -import android.net.thread.ThreadNetworkController.StateCallback; import android.net.thread.utils.OtDaemonController; import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; +import android.net.thread.utils.ThreadNetworkControllerWrapper; import android.os.SystemClock; import androidx.test.core.app.ApplicationProvider; @@ -55,7 +46,6 @@ import org.junit.runner.RunWith; import java.net.Inet6Address; import java.util.List; -import java.util.concurrent.CompletableFuture; /** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */ @LargeTest @@ -76,17 +66,14 @@ public class ThreadIntegrationTest { @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule(); private final Context mContext = ApplicationProvider.getApplicationContext(); - private ThreadNetworkController mController; + private final ThreadNetworkControllerWrapper mController = + ThreadNetworkControllerWrapper.newInstance(mContext); private OtDaemonController mOtCtl; @Before public void setUp() throws Exception { - mController = - mContext.getSystemService(ThreadNetworkManager.class) - .getAllThreadNetworkControllers() - .get(0); mOtCtl = new OtDaemonController(); - leaveAndWait(mController); + mController.leaveAndWait(); // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network mOtCtl.factoryReset(); @@ -94,43 +81,43 @@ public class ThreadIntegrationTest { @After public void tearDown() throws Exception { - setTestUpStreamNetworkAndWait(mController, null); - leaveAndWait(mController); + mController.setTestNetworkAsUpstreamAndWait(null); + mController.leaveAndWait(); } @Test public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception { - leaveAndWait(mController); + mController.leaveAndWait(); runShellCommand("stop ot-daemon"); // TODO(b/323331973): the sleep is needed to workaround the race conditions SystemClock.sleep(200); - waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT); + mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT); } @Test public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception { - joinAndWait(mController, DEFAULT_DATASET); + mController.joinAndWait(DEFAULT_DATASET); runShellCommand("stop ot-daemon"); - waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT); - waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT); + mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT); + mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT); } @Test public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception { - joinAndWait(mController, DEFAULT_DATASET); + mController.joinAndWait(DEFAULT_DATASET); mOtCtl.factoryReset(); - assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED); + assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED); } @Test public void otDaemonFactoryReset_addressesRemoved() throws Exception { - joinAndWait(mController, DEFAULT_DATASET); + mController.joinAndWait(DEFAULT_DATASET); mOtCtl.factoryReset(); String ifconfig = runShellCommand("ifconfig thread-wpan"); @@ -140,7 +127,7 @@ public class ThreadIntegrationTest { @Test public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception { - joinAndWait(mController, DEFAULT_DATASET); + mController.joinAndWait(DEFAULT_DATASET); String ifconfig = runShellCommand("ifconfig thread-wpan"); List otAddresses = mOtCtl.getAddresses(); @@ -152,46 +139,4 @@ public class ThreadIntegrationTest { // TODO (b/323300829): add more tests for integration with linux platform and // ConnectivityService - - private static int getDeviceRole(ThreadNetworkController controller) throws Exception { - CompletableFuture future = new CompletableFuture<>(); - StateCallback callback = future::complete; - controller.registerStateCallback(directExecutor(), callback); - try { - return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS); - } finally { - controller.unregisterStateCallback(callback); - } - } - - private static void joinAndWait( - ThreadNetworkController controller, ActiveOperationalDataset activeDataset) - throws Exception { - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - () -> controller.join(activeDataset, directExecutor(), result -> {})); - waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT); - } - - private static void leaveAndWait(ThreadNetworkController controller) throws Exception { - CompletableFuture future = new CompletableFuture<>(); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - () -> controller.leave(directExecutor(), future::complete)); - future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS); - } - - private static void setTestUpStreamNetworkAndWait( - ThreadNetworkController controller, @Nullable String networkInterfaceName) - throws Exception { - CompletableFuture future = new CompletableFuture<>(); - runAsShell( - PERMISSION_THREAD_NETWORK_PRIVILEGED, - NETWORK_SETTINGS, - () -> { - controller.setTestNetworkAsUpstream( - networkInterfaceName, directExecutor(), future::complete); - }); - future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS); - } } diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java new file mode 100644 index 0000000000..e7b4cd9104 --- /dev/null +++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java @@ -0,0 +1,195 @@ +/* + * 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 android.net.thread.utils; + +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; +import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT; + +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.annotation.Nullable; +import android.content.Context; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.ThreadNetworkController; +import android.net.thread.ThreadNetworkController.StateCallback; +import android.net.thread.ThreadNetworkException; +import android.net.thread.ThreadNetworkManager; +import android.os.OutcomeReceiver; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */ +public final class ThreadNetworkControllerWrapper { + public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10); + public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2); + private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1); + + private final ThreadNetworkController mController; + + /** + * Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread + * feature is not supported on this device. + */ + @Nullable + public static ThreadNetworkControllerWrapper newInstance(Context context) { + final ThreadNetworkManager manager = context.getSystemService(ThreadNetworkManager.class); + if (manager == null) { + return null; + } + return new ThreadNetworkControllerWrapper(manager.getAllThreadNetworkControllers().get(0)); + } + + private ThreadNetworkControllerWrapper(ThreadNetworkController controller) { + mController = controller; + } + + /** + * Returns the Thread enabled state. + * + *

The value can be one of {@code ThreadNetworkController#STATE_*}. + */ + public final int getEnabledState() + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + StateCallback callback = + new StateCallback() { + @Override + public void onThreadEnableStateChanged(int enabledState) { + future.complete(enabledState); + } + + @Override + public void onDeviceRoleChanged(int deviceRole) {} + }; + + runAsShell( + ACCESS_NETWORK_STATE, + () -> mController.registerStateCallback(directExecutor(), callback)); + try { + return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS); + } finally { + runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback)); + } + } + + /** + * Returns the Thread device role. + * + *

The value can be one of {@code ThreadNetworkController#DEVICE_ROLE_*}. + */ + public final int getDeviceRole() + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + StateCallback callback = future::complete; + + runAsShell( + ACCESS_NETWORK_STATE, + () -> mController.registerStateCallback(directExecutor(), callback)); + try { + return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS); + } finally { + runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback)); + } + } + + /** Joins the given network and wait for this device to become attached. */ + public void joinAndWait(ActiveOperationalDataset activeDataset) + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> + mController.join( + activeDataset, directExecutor(), newOutcomeReceiver(future))); + future.get(JOIN_TIMEOUT.toSeconds(), SECONDS); + } + + /** An synchronous variant of {@link ThreadNetworkController#leave}. */ + public void leaveAndWait() throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> mController.leave(directExecutor(), future::complete)); + future.get(LEAVE_TIMEOUT.toSeconds(), SECONDS); + } + + /** Waits for the device role to become {@code deviceRole}. */ + public int waitForRole(int deviceRole, Duration timeout) + throws InterruptedException, ExecutionException, TimeoutException { + return waitForRoleAnyOf(List.of(deviceRole), timeout); + } + + /** Waits for the device role to become one of the values specified in {@code deviceRoles}. */ + public int waitForRoleAnyOf(List deviceRoles, Duration timeout) + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + ThreadNetworkController.StateCallback callback = + newRole -> { + if (deviceRoles.contains(newRole)) { + future.complete(newRole); + } + }; + + runAsShell( + ACCESS_NETWORK_STATE, + () -> mController.registerStateCallback(directExecutor(), callback)); + + try { + return future.get(timeout.toSeconds(), SECONDS); + } finally { + mController.unregisterStateCallback(callback); + } + } + + /** An synchronous variant of {@link ThreadNetworkController#setTestNetworkAsUpstream}. */ + public void setTestNetworkAsUpstreamAndWait(@Nullable String networkInterfaceName) + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + NETWORK_SETTINGS, + () -> { + mController.setTestNetworkAsUpstream( + networkInterfaceName, directExecutor(), future::complete); + }); + future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS); + } + + private static OutcomeReceiver newOutcomeReceiver( + CompletableFuture future) { + return new OutcomeReceiver() { + @Override + public void onResult(V result) { + future.complete(result); + } + + @Override + public void onError(ThreadNetworkException e) { + future.completeExceptionally(e); + } + }; + } +}