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 7a129dca1b5d73794ced4e4bb134fa20a2f2b306..aab4b2e454f5056efe640a92fdd36d59438eeb9b 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -39,6 +39,7 @@ 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.Assert.fail;
 import static org.junit.Assume.assumeNotNull;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -66,6 +67,7 @@ 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.android.testutils.FunctionalUtils.ThrowingRunnable;
 
 import org.junit.After;
 import org.junit.Before;
@@ -95,288 +97,85 @@ public class ThreadNetworkControllerTest {
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
-    private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+    private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
 
     @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private ExecutorService mExecutor;
-    private ThreadNetworkManager mManager;
+    private ThreadNetworkController mController;
 
     private Set<String> mGrantedPermissions;
 
     @Before
     public void setUp() throws Exception {
-        mExecutor = Executors.newSingleThreadExecutor();
-        mManager = mContext.getSystemService(ThreadNetworkManager.class);
+
         mGrantedPermissions = new HashSet<String>();
+        mExecutor = Executors.newSingleThreadExecutor();
+        ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+        if (manager != null) {
+            mController = manager.getAllThreadNetworkControllers().get(0);
+        }
 
         // 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);
+        assumeNotNull(mController);
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            setEnabledAndWait(controller, true);
-        }
+        setEnabledAndWait(mController, true);
     }
 
     @After
     public void tearDown() throws Exception {
-        if (mManager != null) {
-            dropAllPermissions();
-            leaveAndWait();
-        }
-    }
-
-    private List<ThreadNetworkController> getAllControllers() {
-        return mManager.getAllThreadNetworkControllers();
-    }
-
-    private void leaveAndWait() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
+        if (mController != null) {
+            grantPermissions(THREAD_NETWORK_PRIVILEGED);
             CompletableFuture<Void> future = new CompletableFuture<>();
-            leave(controller, future::complete);
+            mController.leave(mExecutor, future::complete);
             future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
         }
-    }
-
-    private void grantPermissions(String... permissions) {
-        for (String permission : permissions) {
-            mGrantedPermissions.add(permission);
-        }
-        String[] allPermissions = new String[mGrantedPermissions.size()];
-        mGrantedPermissions.toArray(allPermissions);
-        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
-    }
-
-    private static void dropAllPermissions() {
-        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
-    }
-
-    private static ActiveOperationalDataset newRandomizedDataset(
-            String networkName, ThreadNetworkController controller) throws Exception {
-        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
-        controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
-        return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-    }
-
-    private static boolean isAttached(ThreadNetworkController controller) throws Exception {
-        return ThreadNetworkController.isAttached(getDeviceRole(controller));
-    }
-
-    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
-        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 {
-        CompletableFuture<Integer> future = new CompletableFuture<>();
-        StateCallback callback =
-                newRole -> {
-                    if (deviceRoles.contains(newRole)) {
-                        future.complete(newRole);
-                    }
-                };
-        controller.registerStateCallback(directExecutor(), callback);
-        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 {
-        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
-        OperationalDatasetCallback callback = future::complete;
-        controller.registerOperationalDatasetCallback(directExecutor(), callback);
-        ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-        controller.unregisterOperationalDatasetCallback(callback);
-        return dataset;
-    }
-
-    private static PendingOperationalDataset getPendingOperationalDataset(
-            ThreadNetworkController controller) throws Exception {
-        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
-        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
-        controller.registerOperationalDatasetCallback(
-                directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
-        return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-    }
-
-    private static OperationalDatasetCallback newDatasetCallback(
-            CompletableFuture<ActiveOperationalDataset> activeFuture,
-            CompletableFuture<PendingOperationalDataset> pendingFuture) {
-        return new OperationalDatasetCallback() {
-            @Override
-            public void onActiveOperationalDatasetChanged(
-                    ActiveOperationalDataset activeOpDataset) {
-                activeFuture.complete(activeOpDataset);
-            }
-
-            @Override
-            public void onPendingOperationalDatasetChanged(
-                    PendingOperationalDataset pendingOpDataset) {
-                pendingFuture.complete(pendingOpDataset);
-            }
-        };
+        dropAllPermissions();
     }
 
     @Test
     public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
-        }
+        assertThat(mController.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
     }
 
     @Test
     public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = deviceRole::complete;
-
-            try {
-                controller.registerStateCallback(mExecutor, callback);
+        try {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    () -> mController.registerStateCallback(mExecutor, callback));
 
-                assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
-                        .isEqualTo(DEVICE_ROLE_STOPPED);
-            } finally {
-                controller.unregisterStateCallback(callback);
-            }
+            assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                    .isEqualTo(DEVICE_ROLE_STOPPED);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.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);
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
 
+        try {
             runAsShell(
-                    PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                    THREAD_NETWORK_PRIVILEGED,
                     () -> {
-                        controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
+                        mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
                     });
             setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
 
             runAsShell(
-                    PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                    THREAD_NETWORK_PRIVILEGED,
                     () -> {
-                        controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
+                        mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
                     });
             setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
 
@@ -384,7 +183,7 @@ public class ThreadNetworkControllerTest {
             listener.expectThreadEnabledState(STATE_DISABLING);
             listener.expectThreadEnabledState(STATE_DISABLED);
             listener.expectThreadEnabledState(STATE_ENABLED);
-
+        } finally {
             listener.unregisterStateCallback();
         }
     }
@@ -393,108 +192,91 @@ public class ThreadNetworkControllerTest {
     public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
         dropAllPermissions();
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerStateCallback(mExecutor, role -> {}));
-        }
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerStateCallback(mExecutor, role -> {}));
     }
 
     @Test
     public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
             throws Exception {
         grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = role -> deviceRole.complete(role);
-            controller.registerStateCallback(mExecutor, callback);
+        mController.registerStateCallback(mExecutor, callback);
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.registerStateCallback(mExecutor, callback));
-        }
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.registerStateCallback(mExecutor, callback));
     }
 
     @Test
     public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = role -> deviceRole.complete(role);
-            grantPermissions(ACCESS_NETWORK_STATE);
-            controller.registerStateCallback(mExecutor, callback);
-
-            try {
-                dropAllPermissions();
-                assertThrows(
-                        SecurityException.class,
-                        () -> controller.unregisterStateCallback(callback));
-            } finally {
-                grantPermissions(ACCESS_NETWORK_STATE);
-                controller.unregisterStateCallback(callback);
-            }
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+        runAsShell(
+                ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class, () -> mController.unregisterStateCallback(callback));
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
         }
     }
 
     @Test
     public void unregisterStateCallback_callbackRegistered_success() throws Exception {
         grantPermissions(ACCESS_NETWORK_STATE);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = role -> deviceRole.complete(role);
-            controller.registerStateCallback(mExecutor, callback);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
 
-            controller.unregisterStateCallback(callback);
-        }
+        assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+        mController.unregisterStateCallback(callback);
     }
 
     @Test
     public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
             throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = role -> deviceRole.complete(role);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
     }
 
     @Test
     public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
             throws Exception {
         grantPermissions(ACCESS_NETWORK_STATE);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = deviceRole::complete;
-            controller.registerStateCallback(mExecutor, callback);
-            controller.unregisterStateCallback(callback);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+        mController.registerStateCallback(mExecutor, callback);
+        mController.unregisterStateCallback(callback);
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.unregisterStateCallback(callback));
-        }
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
     }
 
     @Test
     public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
             throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
-            CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
 
-            try {
-                controller.registerOperationalDatasetCallback(mExecutor, callback);
+        try {
+            mController.registerOperationalDatasetCallback(mExecutor, callback);
 
-                assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
-                assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
-            } finally {
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
+            assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
         }
     }
 
@@ -502,52 +284,47 @@ public class ThreadNetworkControllerTest {
     public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
             throws Exception {
         dropAllPermissions();
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
-            CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
-        }
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
     }
 
     @Test
     public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
-            CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-            controller.registerOperationalDatasetCallback(mExecutor, callback);
-
-            controller.unregisterOperationalDatasetCallback(callback);
-        }
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+        assertDoesNotThrow(() -> mController.unregisterOperationalDatasetCallback(callback));
     }
 
     @Test
     public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
             throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
-            CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
-            var callback = newDatasetCallback(activeFuture, pendingFuture);
-            grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-            controller.registerOperationalDatasetCallback(mExecutor, callback);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
 
-            try {
-                dropAllPermissions();
-                assertThrows(
-                        SecurityException.class,
-                        () -> controller.unregisterOperationalDatasetCallback(callback));
-            } finally {
-                grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
+        try {
+            dropAllPermissions();
+            assertThrows(
+                    SecurityException.class,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
         }
     }
 
@@ -568,485 +345,431 @@ public class ThreadNetworkControllerTest {
 
     @Test
     public void join_withPrivilegedPermission_success() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-            CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
 
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
 
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(isAttached(controller)).isTrue();
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
-        }
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
     }
 
     @Test
     public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
         dropAllPermissions();
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.join(activeDataset, mExecutor, v -> {}));
-        }
+        assertThrows(
+                SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
     }
 
     @Test
     public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            setEnabledAndWait(controller, false);
+        setEnabledAndWait(mController, false);
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
 
-            CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller);
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
 
-            ThreadNetworkException thrown =
-                    (ThreadNetworkException)
-                            assertThrows(ExecutionException.class, joinFuture::get).getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
-        }
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
     }
 
     @Test
     public void join_concurrentRequests_firstOneIsAborted() throws Exception {
-        grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
         final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
         final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("TestNet", controller))
-                            .setNetworkKey(KEY_1)
-                            .build();
-            ActiveOperationalDataset activeDataset2 =
-                    new ActiveOperationalDataset.Builder(activeDataset1)
-                            .setNetworkKey(KEY_2)
-                            .build();
-            CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
-            CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
-
-            controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
-            controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
-
-            ThreadNetworkException thrown =
-                    (ThreadNetworkException)
-                            assertThrows(ExecutionException.class, joinFuture1::get).getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
-            joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(isAttached(controller)).isTrue();
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
-        }
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setNetworkKey(KEY_1)
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1).setNetworkKey(KEY_2).build();
+        CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+                    mController.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+                });
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> joinFuture1.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_ABORTED);
+        joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset2);
     }
 
     @Test
     public void leave_withPrivilegedPermission_success() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            joinRandomizedDatasetAndWait(controller);
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
 
-            CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
-            leave(controller, newOutcomeReceiver(leaveFuture));
-            leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
 
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
     }
 
     @Test
     public void leave_withoutPrivilegedPermission_throwsSecurityException() {
         dropAllPermissions();
 
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
-        }
+        assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
     }
 
     @Test
     public void leave_threadDisabled_success() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            joinRandomizedDatasetAndWait(controller);
+        setEnabledAndWait(mController, false);
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
 
-            CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
-            setEnabledAndWait(controller, false);
-            leave(controller, newOutcomeReceiver(leaveFuture));
+        leave(mController, newOutcomeReceiver(leaveFuture));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
 
-            leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
-            runAsShell(
-                    ACCESS_NETWORK_STATE,
-                    () -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED));
-        }
+        assertThat(getDeviceRole(mController)).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);
-            CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-            CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
-            CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-
-            controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
-            controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
-
-            leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
-            leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
+        CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+                    mController.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+                });
+
+        leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
     }
 
     @Test
     public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("TestNet", controller))
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
-                            .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
-                            .build();
-            ActiveOperationalDataset activeDataset2 =
-                    new ActiveOperationalDataset.Builder(activeDataset1)
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
-                            .setNetworkName("ThreadNet2")
-                            .build();
-            PendingOperationalDataset pendingDataset2 =
-                    new PendingOperationalDataset(
-                            activeDataset2,
-                            OperationalDatasetTimestamp.fromInstant(Instant.now()),
-                            Duration.ofSeconds(30));
-            CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-            CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
-            controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-
-            controller.scheduleMigration(
-                    pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
-            migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-
-            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.complete(true);
-                            }
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+                        .build();
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset1)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("ThreadNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+        mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+        migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        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.complete(true);
                         }
+                    }
 
-                        @Override
-                        public void onPendingOperationalDatasetChanged(
-                                PendingOperationalDataset pendingDataset) {
-                            if (pendingDataset == null) {
-                                pendingDatasetIsRemoved.complete(true);
-                            }
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
                         }
-                    };
-            controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
             assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
             assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
-            controller.unregisterOperationalDatasetCallback(datasetCallback);
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
         }
     }
 
     @Test
     public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            PendingOperationalDataset pendingDataset =
-                    new PendingOperationalDataset(
-                            newRandomizedDataset("TestNet", controller),
-                            OperationalDatasetTimestamp.fromInstant(Instant.now()),
-                            Duration.ofSeconds(30));
-            CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
-
-            controller.scheduleMigration(
-                    pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
-
-            ThreadNetworkException thrown =
-                    (ThreadNetworkException)
-                            assertThrows(ExecutionException.class, migrateFuture::get).getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
-        }
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        newRandomizedDataset("TestNet", mController),
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+
+        mController.scheduleMigration(pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
     }
 
     @Test
     public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
             throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            final ActiveOperationalDataset activeDataset =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("testNet", controller))
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
-                            .build();
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(activeDataset)
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
-                            .setNetworkName("testNet1")
-                            .build();
-            PendingOperationalDataset pendingDataset1 =
-                    new PendingOperationalDataset(
-                            activeDataset1,
-                            new OperationalDatasetTimestamp(100, 0, false),
-                            Duration.ofSeconds(30));
-            ActiveOperationalDataset activeDataset2 =
-                    new ActiveOperationalDataset.Builder(activeDataset)
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
-                            .setNetworkName("testNet2")
-                            .build();
-            PendingOperationalDataset pendingDataset2 =
-                    new PendingOperationalDataset(
-                            activeDataset2,
-                            new OperationalDatasetTimestamp(20, 0, false),
-                            Duration.ofSeconds(30));
-            CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-            CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
-            CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-
-            controller.scheduleMigration(
-                    pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
-            migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-            controller.scheduleMigration(
-                    pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
-            ThreadNetworkException thrown =
-                    (ThreadNetworkException)
-                            assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
-        }
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("testNet", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(20, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+        ThreadNetworkException thrown =
+                (ThreadNetworkException)
+                        assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+        assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
     }
 
     @Test
     public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
             throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            final ActiveOperationalDataset activeDataset =
-                    new ActiveOperationalDataset.Builder(
-                                    newRandomizedDataset("validName", controller))
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
-                            .build();
-            ActiveOperationalDataset activeDataset1 =
-                    new ActiveOperationalDataset.Builder(activeDataset)
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
-                            .setNetworkName("testNet1")
-                            .build();
-            PendingOperationalDataset pendingDataset1 =
-                    new PendingOperationalDataset(
-                            activeDataset1,
-                            new OperationalDatasetTimestamp(100, 0, false),
-                            Duration.ofSeconds(30));
-            ActiveOperationalDataset activeDataset2 =
-                    new ActiveOperationalDataset.Builder(activeDataset)
-                            .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
-                            .setNetworkName("testNet2")
-                            .build();
-            PendingOperationalDataset pendingDataset2 =
-                    new PendingOperationalDataset(
-                            activeDataset2,
-                            new OperationalDatasetTimestamp(200, 0, false),
-                            Duration.ofSeconds(30));
-            CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-            CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
-            CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-
-            controller.scheduleMigration(
-                    pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
-            migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-            controller.scheduleMigration(
-                    pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-            migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
-
-            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.complete(true);
-                            }
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        final ActiveOperationalDataset activeDataset =
+                new ActiveOperationalDataset.Builder(newRandomizedDataset("validName", mController))
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+                        .build();
+        ActiveOperationalDataset activeDataset1 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+                        .setNetworkName("testNet1")
+                        .build();
+        PendingOperationalDataset pendingDataset1 =
+                new PendingOperationalDataset(
+                        activeDataset1,
+                        new OperationalDatasetTimestamp(100, 0, false),
+                        Duration.ofSeconds(30));
+        ActiveOperationalDataset activeDataset2 =
+                new ActiveOperationalDataset.Builder(activeDataset)
+                        .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+                        .setNetworkName("testNet2")
+                        .build();
+        PendingOperationalDataset pendingDataset2 =
+                new PendingOperationalDataset(
+                        activeDataset2,
+                        new OperationalDatasetTimestamp(200, 0, false),
+                        Duration.ofSeconds(30));
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+        mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        mController.scheduleMigration(
+                pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+        migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        mController.scheduleMigration(
+                pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+        migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+        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.complete(true);
                         }
+                    }
 
-                        @Override
-                        public void onPendingOperationalDatasetChanged(
-                                PendingOperationalDataset pendingDataset) {
-                            if (pendingDataset == null) {
-                                pendingDatasetIsRemoved.complete(true);
-                            }
+                    @Override
+                    public void onPendingOperationalDatasetChanged(
+                            PendingOperationalDataset pendingDataset) {
+                        if (pendingDataset == null) {
+                            pendingDatasetIsRemoved.complete(true);
                         }
-                    };
-            controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
             assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
             assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
-            controller.unregisterOperationalDatasetCallback(datasetCallback);
+        } finally {
+            mController.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);
-        }
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        activeDataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(30));
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
+
+        setEnabledAndWait(mController, false);
+
+        scheduleMigration(mController, 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()) {
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
 
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () ->
-                            controller.createRandomizedDataset(
-                                    "ANetNameIs17Bytes", mExecutor, dataset -> {}));
-        }
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mController.createRandomizedDataset(
+                                "ANetNameIs17Bytes", mExecutor, dataset -> {}));
     }
 
     @Test
     public void createRandomizedDataset_validNetworkName_success() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
-
-            assertThat(dataset.getNetworkName()).isEqualTo("validName");
-            assertThat(dataset.getPanId()).isLessThan(0xffff);
-            assertThat(dataset.getChannelMask().size()).isAtLeast(1);
-            assertThat(dataset.getExtendedPanId()).hasLength(8);
-            assertThat(dataset.getNetworkKey()).hasLength(16);
-            assertThat(dataset.getPskc()).hasLength(16);
-            assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
-            assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
-        }
+        ActiveOperationalDataset dataset = newRandomizedDataset("validName", mController);
+
+        assertThat(dataset.getNetworkName()).isEqualTo("validName");
+        assertThat(dataset.getPanId()).isLessThan(0xffff);
+        assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+        assertThat(dataset.getExtendedPanId()).hasLength(8);
+        assertThat(dataset.getNetworkKey()).hasLength(16);
+        assertThat(dataset.getPskc()).hasLength(16);
+        assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+        assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
     }
 
     @Test
     public void setEnabled_permissionsGranted_succeeds() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
-            CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        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(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(false));
 
-            runAsShell(
-                    PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                    () -> controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
-            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
-            waitForEnabledState(controller, booleanToEnabledState(true));
-        }
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
+        setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, 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)));
-        }
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> mController.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));
-        }
+        joinRandomizedDatasetAndWait(mController);
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).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));
+        joinRandomizedDatasetAndWait(mController);
 
-            setEnabledAndWait(controller, true);
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        setEnabledAndWait(mController, true);
 
-            runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(controller));
-        }
+        runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(mController));
     }
 
     @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);
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
+        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));
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1));
+                    mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2));
+                });
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
 
-            listener.unregisterStateCallback();
-        }
+        listener.expectThreadEnabledState(STATE_DISABLING);
+        listener.expectThreadEnabledState(STATE_DISABLED);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        // FIXME: this is not called when a exception is thrown after the creation of `listener`
+        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
@@ -1054,9 +777,6 @@ public class ThreadNetworkControllerTest {
 
     @Test
     public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
-        ThreadNetworkController controller = mManager.getAllThreadNetworkControllers().get(0);
-        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
         CompletableFuture<Network> networkFuture = new CompletableFuture<>();
         ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
         NetworkRequest networkRequest =
@@ -1071,15 +791,223 @@ public class ThreadNetworkControllerTest {
                     }
                 };
 
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        joinRandomizedDatasetAndWait(mController);
         runAsShell(
                 ACCESS_NETWORK_STATE,
                 () -> cm.registerNetworkCallback(networkRequest, networkCallback));
 
-        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-        runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
+        assertThat(isAttached(mController)).isTrue();
         assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
     }
+
+    private void grantPermissions(String... permissions) {
+        for (String permission : permissions) {
+            mGrantedPermissions.add(permission);
+        }
+        String[] allPermissions = new String[mGrantedPermissions.size()];
+        mGrantedPermissions.toArray(allPermissions);
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
+    }
+
+    private static void dropAllPermissions() {
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    private static ActiveOperationalDataset newRandomizedDataset(
+            String networkName, ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+        controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
+        return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private static boolean isAttached(ThreadNetworkController controller) throws Exception {
+        return ThreadNetworkController.isAttached(getDeviceRole(controller));
+    }
+
+    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback = future::complete;
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> controller.registerStateCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+        }
+    }
+
+    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 {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback =
+                newRole -> {
+                    if (deviceRoles.contains(newRole)) {
+                        future.complete(newRole);
+                    }
+                };
+        controller.registerStateCallback(directExecutor(), callback);
+        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(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
+    }
+
+    private void scheduleMigration(
+            ThreadNetworkController controller,
+            PendingOperationalDataset pendingDataset,
+            OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        runAsShell(
+                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(
+                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(
+                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);
+        assertThat(isAttached(controller)).isTrue();
+    }
+
+    private static ActiveOperationalDataset getActiveOperationalDataset(
+            ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+        OperationalDatasetCallback callback = future::complete;
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> controller.registerOperationalDatasetCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.unregisterOperationalDatasetCallback(callback));
+        }
+    }
+
+    private static PendingOperationalDataset getPendingOperationalDataset(
+            ThreadNetworkController controller) throws Exception {
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        controller.registerOperationalDatasetCallback(
+                directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
+        return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private static OperationalDatasetCallback newDatasetCallback(
+            CompletableFuture<ActiveOperationalDataset> activeFuture,
+            CompletableFuture<PendingOperationalDataset> pendingFuture) {
+        return new OperationalDatasetCallback() {
+            @Override
+            public void onActiveOperationalDatasetChanged(
+                    ActiveOperationalDataset activeOpDataset) {
+                activeFuture.complete(activeOpDataset);
+            }
+
+            @Override
+            public void onPendingOperationalDatasetChanged(
+                    PendingOperationalDataset pendingOpDataset) {
+                pendingFuture.complete(pendingOpDataset);
+            }
+        };
+    }
+
+    private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            fail("Should not have thrown " + e);
+        }
+    }
 }