From 0797d9d5017a4e43d99c5e17d3a23d739decc871 Mon Sep 17 00:00:00 2001 From: Yang Sun <sunytt@google.com> Date: Sat, 6 Jan 2024 17:41:46 +0800 Subject: [PATCH] Add setEnabled API setEnabled API is added to ThreadNetworkController, this can be used by applications with privileged permission to set Thread to enable/disabled state. The Thread enabled state can be subscribed with ThreadNetworkController#registerStateCallback. When Thread is disabled, the join/scheduleMigration APIs will fail with ERROR_THREAD_DISABLED. When Thread is disabling, all commands will fail with ERROR_BUSY. Bug: 299243765 Test: atest CtsThreadNetworkTestCases:android.net.thread.cts.ThreadNetworkControllerTest Change-Id: Ifa7845bf1d5664ecd31ce74e24b3a839f92bba13 --- framework-t/api/system-current.txt | 6 + .../android/net/thread/IStateCallback.aidl | 1 + .../net/thread/IThreadNetworkController.aidl | 2 + .../net/thread/ThreadNetworkController.java | 70 +++ .../net/thread/ThreadNetworkException.java | 8 + .../cts/ThreadNetworkControllerTest.java | 544 ++++++++++++++---- 6 files changed, 505 insertions(+), 126 deletions(-) diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt index d346af3855..8251f85623 100644 --- a/framework-t/api/system-current.txt +++ b/framework-t/api/system-current.txt @@ -500,6 +500,7 @@ package android.net.thread { method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback); method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback); method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>); + method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>); method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback); method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback); field public static final int DEVICE_ROLE_CHILD = 2; // 0x2 @@ -507,6 +508,9 @@ package android.net.thread { field public static final int DEVICE_ROLE_LEADER = 4; // 0x4 field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3 field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0 + field public static final int STATE_DISABLED = 0; // 0x0 + field public static final int STATE_DISABLING = 2; // 0x2 + field public static final int STATE_ENABLED = 1; // 0x1 field public static final int THREAD_VERSION_1_3 = 4; // 0x4 } @@ -518,6 +522,7 @@ package android.net.thread { public static interface ThreadNetworkController.StateCallback { method public void onDeviceRoleChanged(int); method public default void onPartitionIdChanged(long); + method public default void onThreadEnableStateChanged(int); } @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception { @@ -530,6 +535,7 @@ package android.net.thread { field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8 field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9 + field public static final int ERROR_THREAD_DISABLED = 12; // 0xc field public static final int ERROR_TIMEOUT = 3; // 0x3 field public static final int ERROR_UNAVAILABLE = 4; // 0x4 field public static final int ERROR_UNKNOWN = 11; // 0xb diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl index d7cbda9f70..9d0a57151b 100644 --- a/thread/framework/java/android/net/thread/IStateCallback.aidl +++ b/thread/framework/java/android/net/thread/IStateCallback.aidl @@ -22,4 +22,5 @@ package android.net.thread; oneway interface IStateCallback { void onDeviceRoleChanged(int deviceRole); void onPartitionIdChanged(long partitionId); + void onThreadEnableStateChanged(int enabledState); } diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl index a9da8d6549..485e25d41c 100644 --- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl +++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl @@ -42,4 +42,6 @@ interface IThreadNetworkController { int getThreadVersion(); void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver); + + void setEnabled(boolean enabled, in IOperationReceiver receiver); } diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java index 7242ed78d5..db761a3cbb 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkController.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java @@ -68,6 +68,15 @@ public final class ThreadNetworkController { /** The device is a Thread Leader. */ public static final int DEVICE_ROLE_LEADER = 4; + /** The Thread radio is disabled. */ + public static final int STATE_DISABLED = 0; + + /** The Thread radio is enabled. */ + public static final int STATE_ENABLED = 1; + + /** The Thread radio is being disabled. */ + public static final int STATE_DISABLING = 2; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -79,6 +88,13 @@ public final class ThreadNetworkController { }) public @interface DeviceRole {} + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"STATE_"}, + value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING}) + public @interface EnabledState {} + /** Thread standard version 1.3. */ public static final int THREAD_VERSION_1_3 = 4; @@ -106,6 +122,40 @@ public final class ThreadNetworkController { mControllerService = controllerService; } + /** + * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will + * be persistent and survives device reboots. + * + * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which + * require the Thread radio will fail with error code {@link + * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING}, + * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail + * with error code {@link ThreadNetworkException#ERROR_BUSY}. + * + * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates + * the operation has completed. But there maybe subsequent calls to update the enabled state, + * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread + * enabled state changes. + * + * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a + * specific error in {@link ThreadNetworkException#ERROR_}. + * + * @param enabled {@code true} for enabling Thread + * @param executor the executor to execute {@code receiver} + * @param receiver the receiver to receive result of this operation + */ + @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") + public void setEnabled( + boolean enabled, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) { + try { + mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Returns the Thread version this device is operating on. */ @ThreadVersion public int getThreadVersion() { @@ -170,6 +220,16 @@ public final class ThreadNetworkController { * @param partitionId the new Thread partition ID */ default void onPartitionIdChanged(long partitionId) {} + + /** + * The Thread enabled state has changed. + * + * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by + * airplane mode or admin control. + * + * @param enabledState the new Thread enabled state + */ + default void onThreadEnableStateChanged(@EnabledState int enabledState) {} } private static final class StateCallbackProxy extends IStateCallback.Stub { @@ -200,6 +260,16 @@ public final class ThreadNetworkController { Binder.restoreCallingIdentity(identity); } } + + @Override + public void onThreadEnableStateChanged(@EnabledState int enabled) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } } /** diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java index af0a84bf53..23ed53e1cc 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkException.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java @@ -48,6 +48,7 @@ public class ThreadNetworkException extends Exception { ERROR_RESPONSE_BAD_FORMAT, ERROR_RESOURCE_EXHAUSTED, ERROR_UNKNOWN, + ERROR_THREAD_DISABLED, }) public @interface ErrorCode {} @@ -129,6 +130,13 @@ public class ThreadNetworkException extends Exception { */ public static final int ERROR_UNKNOWN = 11; + /** + * The operation failed because the Thread radio is disabled by {@link + * ThreadNetworkController#setEnabled}, airplane mode or device admin. The caller should retry + * only after Thread is enabled. + */ + public static final int ERROR_THREAD_DISABLED = 12; + private final int mErrorCode; /** Creates a new {@link ThreadNetworkException} object with given error code and message. */ 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 7a6c9aa558..7a129dca1b 100644 --- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java @@ -16,11 +16,19 @@ package android.net.thread.cts; +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +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; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED; +import static android.net.thread.ThreadNetworkController.STATE_DISABLED; +import static android.net.thread.ThreadNetworkController.STATE_DISABLING; +import static android.net.thread.ThreadNetworkController.STATE_ENABLED; import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3; import static android.net.thread.ThreadNetworkException.ERROR_ABORTED; import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION; import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER; +import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -29,12 +37,12 @@ import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeNotNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.Manifest.permission; import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; @@ -54,12 +62,11 @@ import android.os.OutcomeReceiver; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.LargeTest; +import com.android.net.module.util.ArrayTrackRecord; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; -import com.google.common.util.concurrent.SettableFuture; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -68,9 +75,11 @@ import org.junit.runner.RunWith; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -81,8 +90,11 @@ import java.util.concurrent.Executors; @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+ public class ThreadNetworkControllerTest { private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000; + private static final int LEAVE_TIMEOUT_MILLIS = 2_000; + private static final int MIGRATION_TIMEOUT_MILLIS = 40 * 1_000; private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000; - private static final int CALLBACK_TIMEOUT_MILLIS = 1000; + private static final int CALLBACK_TIMEOUT_MILLIS = 1_000; + private static final int ENABLED_TIMEOUT_MILLIS = 2_000; private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED = "android.permission.THREAD_NETWORK_PRIVILEGED"; @@ -95,7 +107,7 @@ public class ThreadNetworkControllerTest { private Set<String> mGrantedPermissions; @Before - public void setUp() { + public void setUp() throws Exception { mExecutor = Executors.newSingleThreadExecutor(); mManager = mContext.getSystemService(ThreadNetworkManager.class); mGrantedPermissions = new HashSet<String>(); @@ -103,13 +115,17 @@ public class ThreadNetworkControllerTest { // TODO: we will also need it in tearDown(), it's better to have a Rule to skip // tests if a feature is not available. assumeNotNull(mManager); + + for (ThreadNetworkController controller : getAllControllers()) { + setEnabledAndWait(controller, true); + } } @After public void tearDown() throws Exception { if (mManager != null) { - leaveAndWait(); dropAllPermissions(); + leaveAndWait(); } } @@ -118,12 +134,10 @@ public class ThreadNetworkControllerTest { } private void leaveAndWait() throws Exception { - grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED); - for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Void> future = SettableFuture.create(); - controller.leave(mExecutor, future::set); - future.get(); + CompletableFuture<Void> future = new CompletableFuture<>(); + leave(controller, future::complete); + future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); } } @@ -142,8 +156,8 @@ public class ThreadNetworkControllerTest { private static ActiveOperationalDataset newRandomizedDataset( String networkName, ThreadNetworkController controller) throws Exception { - SettableFuture<ActiveOperationalDataset> future = SettableFuture.create(); - controller.createRandomizedDataset(networkName, directExecutor(), future::set); + CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>(); + controller.createRandomizedDataset(networkName, directExecutor(), future::complete); return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); } @@ -152,33 +166,140 @@ public class ThreadNetworkControllerTest { } private static int getDeviceRole(ThreadNetworkController controller) throws Exception { - SettableFuture<Integer> future = SettableFuture.create(); - StateCallback callback = future::set; + CompletableFuture<Integer> future = new CompletableFuture<>(); + StateCallback callback = future::complete; controller.registerStateCallback(directExecutor(), callback); int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); controller.unregisterStateCallback(callback); return role; } + private static int waitForAttachedState(ThreadNetworkController controller) throws Exception { + List<Integer> attachedRoles = new ArrayList<>(); + attachedRoles.add(DEVICE_ROLE_CHILD); + attachedRoles.add(DEVICE_ROLE_ROUTER); + attachedRoles.add(DEVICE_ROLE_LEADER); + return waitForStateAnyOf(controller, attachedRoles); + } + private static int waitForStateAnyOf( ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception { - SettableFuture<Integer> future = SettableFuture.create(); + CompletableFuture<Integer> future = new CompletableFuture<>(); StateCallback callback = newRole -> { if (deviceRoles.contains(newRole)) { - future.set(newRole); + future.complete(newRole); } }; controller.registerStateCallback(directExecutor(), callback); - int role = future.get(); + int role = future.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); controller.unregisterStateCallback(callback); return role; } + private static void waitForEnabledState(ThreadNetworkController controller, int state) + throws Exception { + CompletableFuture<Integer> future = new CompletableFuture<>(); + StateCallback callback = + new ThreadNetworkController.StateCallback() { + @Override + public void onDeviceRoleChanged(int r) {} + + @Override + public void onThreadEnableStateChanged(int enabled) { + if (enabled == state) { + future.complete(enabled); + } + } + }; + runAsShell( + ACCESS_NETWORK_STATE, + () -> controller.registerStateCallback(directExecutor(), callback)); + future.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback)); + } + + private void leave( + ThreadNetworkController controller, + OutcomeReceiver<Void, ThreadNetworkException> receiver) { + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver)); + } + + private void scheduleMigration( + ThreadNetworkController controller, + PendingOperationalDataset pendingDataset, + OutcomeReceiver<Void, ThreadNetworkException> receiver) { + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> controller.scheduleMigration(pendingDataset, mExecutor, receiver)); + } + + private class EnabledStateListener { + private ArrayTrackRecord<Integer> mEnabledStates = new ArrayTrackRecord<>(); + private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mEnabledStates.newReadHead(); + ThreadNetworkController mController; + StateCallback mCallback = + new ThreadNetworkController.StateCallback() { + @Override + public void onDeviceRoleChanged(int r) {} + + @Override + public void onThreadEnableStateChanged(int enabled) { + mEnabledStates.add(enabled); + } + }; + + EnabledStateListener(ThreadNetworkController controller) { + this.mController = controller; + runAsShell( + ACCESS_NETWORK_STATE, + () -> controller.registerStateCallback(mExecutor, mCallback)); + } + + public void expectThreadEnabledState(int enabled) { + assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled))); + } + + public void unregisterStateCallback() { + runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback)); + } + } + + private int booleanToEnabledState(boolean enabled) { + return enabled ? STATE_ENABLED : STATE_DISABLED; + } + + private void setEnabledAndWait(ThreadNetworkController controller, boolean enabled) + throws Exception { + CompletableFuture<Void> setFuture = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture))); + setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + waitForEnabledState(controller, booleanToEnabledState(enabled)); + } + + private CompletableFuture joinRandomizedDataset(ThreadNetworkController controller) + throws Exception { + ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture))); + return joinFuture; + } + + private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception { + CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); + runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue()); + } + private static ActiveOperationalDataset getActiveOperationalDataset( ThreadNetworkController controller) throws Exception { - SettableFuture<ActiveOperationalDataset> future = SettableFuture.create(); - OperationalDatasetCallback callback = future::set; + CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>(); + OperationalDatasetCallback callback = future::complete; controller.registerOperationalDatasetCallback(directExecutor(), callback); ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); controller.unregisterOperationalDatasetCallback(callback); @@ -187,27 +308,27 @@ public class ThreadNetworkControllerTest { private static PendingOperationalDataset getPendingOperationalDataset( ThreadNetworkController controller) throws Exception { - SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create(); - SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create(); + CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>(); + CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>(); controller.registerOperationalDatasetCallback( directExecutor(), newDatasetCallback(activeFuture, pendingFuture)); - return pendingFuture.get(); + return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); } private static OperationalDatasetCallback newDatasetCallback( - SettableFuture<ActiveOperationalDataset> activeFuture, - SettableFuture<PendingOperationalDataset> pendingFuture) { + CompletableFuture<ActiveOperationalDataset> activeFuture, + CompletableFuture<PendingOperationalDataset> pendingFuture) { return new OperationalDatasetCallback() { @Override public void onActiveOperationalDatasetChanged( ActiveOperationalDataset activeOpDataset) { - activeFuture.set(activeOpDataset); + activeFuture.complete(activeOpDataset); } @Override public void onPendingOperationalDatasetChanged( PendingOperationalDataset pendingOpDataset) { - pendingFuture.set(pendingOpDataset); + pendingFuture.complete(pendingOpDataset); } }; } @@ -221,22 +342,53 @@ public class ThreadNetworkControllerTest { @Test public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = deviceRole::set; + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = deviceRole::complete; try { controller.registerStateCallback(mExecutor, callback); - assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED); + assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)) + .isEqualTo(DEVICE_ROLE_STOPPED); } finally { controller.unregisterStateCallback(callback); } } } + @Test + public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + CompletableFuture<Void> setFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> setFuture2 = new CompletableFuture<>(); + EnabledStateListener listener = new EnabledStateListener(controller); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> { + controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)); + }); + setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> { + controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)); + }); + setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + + listener.expectThreadEnabledState(STATE_ENABLED); + listener.expectThreadEnabledState(STATE_DISABLING); + listener.expectThreadEnabledState(STATE_DISABLED); + listener.expectThreadEnabledState(STATE_ENABLED); + + listener.unregisterStateCallback(); + } + } + @Test public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception { dropAllPermissions(); @@ -251,11 +403,11 @@ public class ThreadNetworkControllerTest { @Test public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = role -> deviceRole.set(role); + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = role -> deviceRole.complete(role); controller.registerStateCallback(mExecutor, callback); assertThrows( @@ -267,9 +419,9 @@ public class ThreadNetworkControllerTest { @Test public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception { for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = role -> deviceRole.set(role); - grantPermissions(permission.ACCESS_NETWORK_STATE); + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = role -> deviceRole.complete(role); + grantPermissions(ACCESS_NETWORK_STATE); controller.registerStateCallback(mExecutor, callback); try { @@ -278,7 +430,7 @@ public class ThreadNetworkControllerTest { SecurityException.class, () -> controller.unregisterStateCallback(callback)); } finally { - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); controller.unregisterStateCallback(callback); } } @@ -286,10 +438,10 @@ public class ThreadNetworkControllerTest { @Test public void unregisterStateCallback_callbackRegistered_success() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = role -> deviceRole.set(role); + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = role -> deviceRole.complete(role); controller.registerStateCallback(mExecutor, callback); controller.unregisterStateCallback(callback); @@ -300,8 +452,8 @@ public class ThreadNetworkControllerTest { public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException() throws Exception { for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = role -> deviceRole.set(role); + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = role -> deviceRole.complete(role); assertThrows( IllegalArgumentException.class, @@ -312,10 +464,10 @@ public class ThreadNetworkControllerTest { @Test public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<Integer> deviceRole = SettableFuture.create(); - StateCallback callback = deviceRole::set; + CompletableFuture<Integer> deviceRole = new CompletableFuture<>(); + StateCallback callback = deviceRole::complete; controller.registerStateCallback(mExecutor, callback); controller.unregisterStateCallback(callback); @@ -328,18 +480,18 @@ public class ThreadNetworkControllerTest { @Test public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create(); - SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create(); + CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>(); + CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>(); var callback = newDatasetCallback(activeFuture, pendingFuture); try { controller.registerOperationalDatasetCallback(mExecutor, callback); - assertThat(activeFuture.get()).isNull(); - assertThat(pendingFuture.get()).isNull(); + assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull(); + assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull(); } finally { controller.unregisterOperationalDatasetCallback(callback); } @@ -352,8 +504,8 @@ public class ThreadNetworkControllerTest { dropAllPermissions(); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create(); - SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create(); + CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>(); + CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>(); var callback = newDatasetCallback(activeFuture, pendingFuture); assertThrows( @@ -364,10 +516,10 @@ public class ThreadNetworkControllerTest { @Test public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create(); - SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create(); + CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>(); + CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>(); var callback = newDatasetCallback(activeFuture, pendingFuture); controller.registerOperationalDatasetCallback(mExecutor, callback); @@ -381,10 +533,10 @@ public class ThreadNetworkControllerTest { dropAllPermissions(); for (ThreadNetworkController controller : getAllControllers()) { - SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create(); - SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create(); + CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>(); + CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>(); var callback = newDatasetCallback(activeFuture, pendingFuture); - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); controller.registerOperationalDatasetCallback(mExecutor, callback); try { @@ -393,24 +545,23 @@ public class ThreadNetworkControllerTest { SecurityException.class, () -> controller.unregisterOperationalDatasetCallback(callback)); } finally { - grantPermissions( - permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); controller.unregisterOperationalDatasetCallback(callback); } } } private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver( - SettableFuture<V> future) { + CompletableFuture<V> future) { return new OutcomeReceiver<V, ThreadNetworkException>() { @Override public void onResult(V result) { - future.set(result); + future.complete(result); } @Override public void onError(ThreadNetworkException e) { - future.setException(e); + future.completeExceptionally(e); } }; } @@ -421,12 +572,12 @@ public class ThreadNetworkControllerTest { for (ThreadNetworkController controller : getAllControllers()) { ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); - SettableFuture<Void> joinFuture = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); assertThat(isAttached(controller)).isTrue(); assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset); } @@ -445,6 +596,20 @@ public class ThreadNetworkControllerTest { } } + @Test + public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + setEnabledAndWait(controller, false); + + CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller); + + ThreadNetworkException thrown = + (ThreadNetworkException) + assertThrows(ExecutionException.class, joinFuture::get).getCause(); + assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED); + } + } + @Test public void join_concurrentRequests_firstOneIsAborted() throws Exception { grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED); @@ -461,8 +626,8 @@ public class ThreadNetworkControllerTest { new ActiveOperationalDataset.Builder(activeDataset1) .setNetworkKey(KEY_2) .build(); - SettableFuture<Void> joinFuture1 = SettableFuture.create(); - SettableFuture<Void> joinFuture2 = SettableFuture.create(); + CompletableFuture<Void> joinFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> joinFuture2 = new CompletableFuture<>(); controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1)); controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2)); @@ -471,8 +636,8 @@ public class ThreadNetworkControllerTest { (ThreadNetworkException) assertThrows(ExecutionException.class, joinFuture1::get).getCause(); assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED); - joinFuture2.get(); - grantPermissions(permission.ACCESS_NETWORK_STATE); + joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); + grantPermissions(ACCESS_NETWORK_STATE); assertThat(isAttached(controller)).isTrue(); assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2); } @@ -480,19 +645,14 @@ public class ThreadNetworkControllerTest { @Test public void leave_withPrivilegedPermission_success() throws Exception { - grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED); - for (ThreadNetworkController controller : getAllControllers()) { - ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Void> leaveFuture = SettableFuture.create(); - controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinRandomizedDatasetAndWait(controller); - controller.leave(mExecutor, newOutcomeReceiver(leaveFuture)); - leaveFuture.get(); + CompletableFuture<Void> leaveFuture = new CompletableFuture<>(); + leave(controller, newOutcomeReceiver(leaveFuture)); + leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); - grantPermissions(permission.ACCESS_NETWORK_STATE); + grantPermissions(ACCESS_NETWORK_STATE); assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED); } } @@ -506,31 +666,47 @@ public class ThreadNetworkControllerTest { } } + @Test + public void leave_threadDisabled_success() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + joinRandomizedDatasetAndWait(controller); + + CompletableFuture<Void> leaveFuture = new CompletableFuture<>(); + setEnabledAndWait(controller, false); + leave(controller, newOutcomeReceiver(leaveFuture)); + + leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); + runAsShell( + ACCESS_NETWORK_STATE, + () -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED)); + } + } + @Test public void leave_concurrentRequests_bothSuccess() throws Exception { grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Void> leaveFuture1 = SettableFuture.create(); - SettableFuture<Void> leaveFuture2 = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>(); controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1)); controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2)); - leaveFuture1.get(); - leaveFuture2.get(); - grantPermissions(permission.ACCESS_NETWORK_STATE); + leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); + leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); + grantPermissions(ACCESS_NETWORK_STATE); assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED); } } @Test public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { ActiveOperationalDataset activeDataset1 = @@ -549,24 +725,24 @@ public class ThreadNetworkControllerTest { activeDataset2, OperationalDatasetTimestamp.fromInstant(Instant.now()), Duration.ofSeconds(30)); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Void> migrateFuture = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + CompletableFuture<Void> migrateFuture = new CompletableFuture<>(); controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); controller.scheduleMigration( pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture)); - migrateFuture.get(); + migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); - SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create(); - SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create(); + CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>(); + CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>(); OperationalDatasetCallback datasetCallback = new OperationalDatasetCallback() { @Override public void onActiveOperationalDatasetChanged( ActiveOperationalDataset activeDataset) { if (activeDataset.equals(activeDataset2)) { - dataset2IsApplied.set(true); + dataset2IsApplied.complete(true); } } @@ -574,20 +750,20 @@ public class ThreadNetworkControllerTest { public void onPendingOperationalDatasetChanged( PendingOperationalDataset pendingDataset) { if (pendingDataset == null) { - pendingDatasetIsRemoved.set(true); + pendingDatasetIsRemoved.complete(true); } } }; controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback); - assertThat(dataset2IsApplied.get()).isTrue(); - assertThat(pendingDatasetIsRemoved.get()).isTrue(); + assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue(); + assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue(); controller.unregisterOperationalDatasetCallback(datasetCallback); } } @Test public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { PendingOperationalDataset pendingDataset = @@ -595,7 +771,7 @@ public class ThreadNetworkControllerTest { newRandomizedDataset("TestNet", controller), OperationalDatasetTimestamp.fromInstant(Instant.now()), Duration.ofSeconds(30)); - SettableFuture<Void> migrateFuture = SettableFuture.create(); + CompletableFuture<Void> migrateFuture = new CompletableFuture<>(); controller.scheduleMigration( pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture)); @@ -610,7 +786,7 @@ public class ThreadNetworkControllerTest { @Test public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { final ActiveOperationalDataset activeDataset = @@ -638,15 +814,15 @@ public class ThreadNetworkControllerTest { activeDataset2, new OperationalDatasetTimestamp(20, 0, false), Duration.ofSeconds(30)); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Void> migrateFuture1 = SettableFuture.create(); - SettableFuture<Void> migrateFuture2 = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>(); controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); controller.scheduleMigration( pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1)); - migrateFuture1.get(); + migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); controller.scheduleMigration( pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2)); @@ -660,7 +836,7 @@ public class ThreadNetworkControllerTest { @Test public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied() throws Exception { - grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); + grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { final ActiveOperationalDataset activeDataset = @@ -688,28 +864,28 @@ public class ThreadNetworkControllerTest { activeDataset2, new OperationalDatasetTimestamp(200, 0, false), Duration.ofSeconds(30)); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Void> migrateFuture1 = SettableFuture.create(); - SettableFuture<Void> migrateFuture2 = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>(); controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)); - joinFuture.get(); + joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); controller.scheduleMigration( pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1)); - migrateFuture1.get(); + migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); controller.scheduleMigration( pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2)); - migrateFuture2.get(); + migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS); - SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create(); - SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create(); + CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>(); + CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>(); OperationalDatasetCallback datasetCallback = new OperationalDatasetCallback() { @Override public void onActiveOperationalDatasetChanged( ActiveOperationalDataset activeDataset) { if (activeDataset.equals(activeDataset2)) { - dataset2IsApplied.set(true); + dataset2IsApplied.complete(true); } } @@ -717,17 +893,40 @@ public class ThreadNetworkControllerTest { public void onPendingOperationalDatasetChanged( PendingOperationalDataset pendingDataset) { if (pendingDataset == null) { - pendingDatasetIsRemoved.set(true); + pendingDatasetIsRemoved.complete(true); } } }; controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback); - assertThat(dataset2IsApplied.get()).isTrue(); - assertThat(pendingDatasetIsRemoved.get()).isTrue(); + assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue(); + assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue(); controller.unregisterOperationalDatasetCallback(datasetCallback); } } + @Test + public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); + PendingOperationalDataset pendingDataset = + new PendingOperationalDataset( + activeDataset, + OperationalDatasetTimestamp.fromInstant(Instant.now()), + Duration.ofSeconds(30)); + joinRandomizedDatasetAndWait(controller); + CompletableFuture<Void> migrationFuture = new CompletableFuture<>(); + + setEnabledAndWait(controller, false); + + scheduleMigration(controller, pendingDataset, newOutcomeReceiver(migrationFuture)); + + ThreadNetworkException thrown = + (ThreadNetworkException) + assertThrows(ExecutionException.class, migrationFuture::get).getCause(); + assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED); + } + } + @Test public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() { for (ThreadNetworkController controller : getAllControllers()) { @@ -759,12 +958,106 @@ public class ThreadNetworkControllerTest { } } + @Test + public void setEnabled_permissionsGranted_succeeds() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + CompletableFuture<Void> setFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> setFuture2 = new CompletableFuture<>(); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1))); + setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + waitForEnabledState(controller, booleanToEnabledState(false)); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2))); + setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + waitForEnabledState(controller, booleanToEnabledState(true)); + } + } + + @Test + public void setEnabled_noPermissions_throwsSecurityException() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + CompletableFuture<Void> setFuture = new CompletableFuture<>(); + assertThrows( + SecurityException.class, + () -> controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture))); + } + } + + @Test + public void setEnabled_disable_leavesThreadNetwork() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + joinRandomizedDatasetAndWait(controller); + + setEnabledAndWait(controller, false); + + runAsShell( + ACCESS_NETWORK_STATE, + () -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED)); + } + } + + @Test + public void setEnabled_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + joinRandomizedDatasetAndWait(controller); + + setEnabledAndWait(controller, false); + + runAsShell( + ACCESS_NETWORK_STATE, + () -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED)); + + setEnabledAndWait(controller, true); + + runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(controller)); + } + } + + @Test + public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception { + for (ThreadNetworkController controller : getAllControllers()) { + joinRandomizedDatasetAndWait(controller); + CompletableFuture<Void> setFuture1 = new CompletableFuture<>(); + CompletableFuture<Void> setFuture2 = new CompletableFuture<>(); + EnabledStateListener listener = new EnabledStateListener(controller); + listener.expectThreadEnabledState(STATE_ENABLED); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> { + controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1)); + controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2)); + }); + + setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS); + + listener.expectThreadEnabledState(STATE_DISABLING); + listener.expectThreadEnabledState(STATE_DISABLED); + + runAsShell( + ACCESS_NETWORK_STATE, + () -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED)); + + listener.unregisterStateCallback(); + } + } + // TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands + // (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested + // because DISABLING has very short lifecycle, it's not possible to guarantee the command can be + // sent before state changes to DISABLED. + @Test public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception { ThreadNetworkController controller = mManager.getAllThreadNetworkControllers().get(0); ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller); - SettableFuture<Void> joinFuture = SettableFuture.create(); - SettableFuture<Network> networkFuture = SettableFuture.create(); + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + CompletableFuture<Network> networkFuture = new CompletableFuture<>(); ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); NetworkRequest networkRequest = new NetworkRequest.Builder() @@ -774,7 +1067,7 @@ public class ThreadNetworkControllerTest { new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { - networkFuture.set(network); + networkFuture.complete(network); } }; @@ -782,12 +1075,11 @@ public class ThreadNetworkControllerTest { PERMISSION_THREAD_NETWORK_PRIVILEGED, () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture))); runAsShell( - permission.ACCESS_NETWORK_STATE, + ACCESS_NETWORK_STATE, () -> cm.registerNetworkCallback(networkRequest, networkCallback)); joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS); - runAsShell( - permission.ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue()); + runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue()); assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull(); } } -- GitLab