Merge "Update name threadnetwork.country_code in comments" into main
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index 48ac993..c999398 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -52,8 +52,14 @@
         return nullptr;
     }
 
+    // Find the constructor.
+    jmethodID constructorID = env->GetMethodID(gEntryClass, "<init>", "()V");
+    if (constructorID == nullptr) {
+        return nullptr;
+    }
+
     // Create a new instance of the Java class
-    jobject result = env->AllocObject(gEntryClass);
+    jobject result = env->NewObject(gEntryClass, constructorID);
     if (result == nullptr) {
         return nullptr;
     }
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index e4bd459..c176bfa 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -3,5 +3,7 @@
 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
 
 GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
 
 $GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 3cf31e5..2f38bfd 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -45,7 +45,7 @@
     libs: [
         "android.test.base",
         "android.test.runner",
-        "framework-connectivity-module-api-stubs-including-flagged"
+        "framework-connectivity-module-api-stubs-including-flagged",
     ],
     // Test coverage system runs on different devices. Need to
     // compile for all architectures.
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 7a129dc..aab4b2e 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 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.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,50 +97,707 @@
     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);
         }
+        dropAllPermissions();
+    }
+
+    @Test
+    public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+        assertThat(mController.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+    }
+
+    @Test
+    public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+
+        try {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    () -> mController.registerStateCallback(mExecutor, 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 {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> {
+                        mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
+                    });
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> {
+                        mController.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);
+        } finally {
+            listener.unregisterStateCallback();
+        }
+    }
+
+    @Test
+    public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        dropAllPermissions();
+
+        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);
+
+        mController.registerStateCallback(mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.registerStateCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+        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);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+        mController.unregisterStateCallback(callback);
+    }
+
+    @Test
+    public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = role -> deviceRole.complete(role);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE);
+        CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+        StateCallback callback = deviceRole::complete;
+        mController.registerStateCallback(mExecutor, callback);
+        mController.unregisterStateCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterStateCallback(callback));
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+            throws Exception {
+        grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        try {
+            mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+            assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(callback);
+        }
+    }
+
+    @Test
+    public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+        CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+        CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+        var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+        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 {
+        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,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        } finally {
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterOperationalDatasetCallback(callback));
+        }
+    }
+
+    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+            CompletableFuture<V> future) {
+        return new OutcomeReceiver<V, ThreadNetworkException>() {
+            @Override
+            public void onResult(V result) {
+                future.complete(result);
+            }
+
+            @Override
+            public void onError(ThreadNetworkException e) {
+                future.completeExceptionally(e);
+            }
+        };
+    }
+
+    @Test
+    public void join_withPrivilegedPermission_success() throws Exception {
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
+    }
+
+    @Test
+    public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+        dropAllPermissions();
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+
+        assertThrows(
+                SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
+    }
+
+    @Test
+    public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        setEnabledAndWait(mController, false);
+        ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+
+        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 {
+        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};
+        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 {
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+        joinRandomizedDatasetAndWait(mController);
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+        dropAllPermissions();
+
+        assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
+    }
+
+    @Test
+    public void leave_threadDisabled_success() throws Exception {
+        setEnabledAndWait(mController, false);
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+
+        leave(mController, newOutcomeReceiver(leaveFuture));
+        leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void leave_concurrentRequests_bothSuccess() throws Exception {
+        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, 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);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+        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, 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, 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);
+                        }
+                    }
+                };
+        mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+        try {
+            assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+            assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+        } finally {
+            mController.unregisterOperationalDatasetCallback(datasetCallback);
+        }
+    }
+
+    @Test
+    public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+        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() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mController.createRandomizedDataset(
+                                "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+    }
+
+    @Test
+    public void createRandomizedDataset_validNetworkName_success() throws Exception {
+        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 {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+        setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+        waitForEnabledState(mController, booleanToEnabledState(false));
+
+        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 {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
+    }
+
+    @Test
+    public void setEnabled_disable_leavesThreadNetwork() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+    }
+
+    @Test
+    public void setEnabled_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+
+        setEnabledAndWait(mController, false);
+        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        setEnabledAndWait(mController, true);
+
+        runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(mController));
+    }
+
+    @Test
+    public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        EnabledStateListener listener = new EnabledStateListener(mController);
+        listener.expectThreadEnabledState(STATE_ENABLED);
+
+        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.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
+    // sent before state changes to DISABLED.
+
+    @Test
+    public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
+        CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        NetworkRequest networkRequest =
+                new NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .build();
+        ConnectivityManager.NetworkCallback networkCallback =
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        networkFuture.complete(network);
+                    }
+                };
+
+        joinRandomizedDatasetAndWait(mController);
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> cm.registerNetworkCallback(networkRequest, networkCallback));
+
+        assertThat(isAttached(mController)).isTrue();
+        assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
     }
 
     private void grantPermissions(String... permissions) {
@@ -168,10 +827,14 @@
     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;
+        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 {
@@ -222,8 +885,7 @@
     private void leave(
             ThreadNetworkController controller,
             OutcomeReceiver<Void, ThreadNetworkException> receiver) {
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
+        runAsShell(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
     }
 
     private void scheduleMigration(
@@ -231,7 +893,7 @@
             PendingOperationalDataset pendingDataset,
             OutcomeReceiver<Void, ThreadNetworkException> receiver) {
         runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                THREAD_NETWORK_PRIVILEGED,
                 () -> controller.scheduleMigration(pendingDataset, mExecutor, receiver));
     }
 
@@ -274,7 +936,7 @@
             throws Exception {
         CompletableFuture<Void> setFuture = new CompletableFuture<>();
         runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                THREAD_NETWORK_PRIVILEGED,
                 () -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture)));
         setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
         waitForEnabledState(controller, booleanToEnabledState(enabled));
@@ -285,7 +947,7 @@
         ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
         CompletableFuture<Void> joinFuture = new CompletableFuture<>();
         runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                THREAD_NETWORK_PRIVILEGED,
                 () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
         return joinFuture;
     }
@@ -293,17 +955,25 @@
     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());
+        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;
+        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(
@@ -333,753 +1003,11 @@
         };
     }
 
-    @Test
-    public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+    private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            fail("Should not have thrown " + e);
         }
     }
-
-    @Test
-    public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
-        grantPermissions(ACCESS_NETWORK_STATE);
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
-            StateCallback callback = deviceRole::complete;
-
-            try {
-                controller.registerStateCallback(mExecutor, callback);
-
-                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();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.registerStateCallback(mExecutor, role -> {}));
-        }
-    }
-
-    @Test
-    public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
-            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);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.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);
-            }
-        }
-    }
-
-    @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);
-
-            controller.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);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.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);
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.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);
-
-            try {
-                controller.registerOperationalDatasetCallback(mExecutor, callback);
-
-                assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
-                assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
-            } finally {
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
-        }
-    }
-
-    @Test
-    public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
-            throws Exception {
-        dropAllPermissions();
-
-        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));
-        }
-    }
-
-    @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);
-        }
-    }
-
-    @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);
-
-            try {
-                dropAllPermissions();
-                assertThrows(
-                        SecurityException.class,
-                        () -> controller.unregisterOperationalDatasetCallback(callback));
-            } finally {
-                grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-                controller.unregisterOperationalDatasetCallback(callback);
-            }
-        }
-    }
-
-    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
-            CompletableFuture<V> future) {
-        return new OutcomeReceiver<V, ThreadNetworkException>() {
-            @Override
-            public void onResult(V result) {
-                future.complete(result);
-            }
-
-            @Override
-            public void onError(ThreadNetworkException e) {
-                future.completeExceptionally(e);
-            }
-        };
-    }
-
-    @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<>();
-
-            controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
-            joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(isAttached(controller)).isTrue();
-            assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
-        }
-    }
-
-    @Test
-    public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-
-            assertThrows(
-                    SecurityException.class,
-                    () -> controller.join(activeDataset, mExecutor, v -> {}));
-        }
-    }
-
-    @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);
-
-        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);
-        }
-    }
-
-    @Test
-    public void leave_withPrivilegedPermission_success() throws Exception {
-        for (ThreadNetworkController controller : getAllControllers()) {
-            joinRandomizedDatasetAndWait(controller);
-
-            CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
-            leave(controller, newOutcomeReceiver(leaveFuture));
-            leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
-
-            grantPermissions(ACCESS_NETWORK_STATE);
-            assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
-        }
-    }
-
-    @Test
-    public void leave_withoutPrivilegedPermission_throwsSecurityException() {
-        dropAllPermissions();
-
-        for (ThreadNetworkController controller : getAllControllers()) {
-            assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
-        }
-    }
-
-    @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);
-            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);
-        }
-    }
-
-    @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);
-                            }
-                        }
-
-                        @Override
-                        public void onPendingOperationalDatasetChanged(
-                                PendingOperationalDataset pendingDataset) {
-                            if (pendingDataset == null) {
-                                pendingDatasetIsRemoved.complete(true);
-                            }
-                        }
-                    };
-            controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
-            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(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);
-        }
-    }
-
-    @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);
-        }
-    }
-
-    @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);
-                            }
-                        }
-
-                        @Override
-                        public void onPendingOperationalDatasetChanged(
-                                PendingOperationalDataset pendingDataset) {
-                            if (pendingDataset == null) {
-                                pendingDatasetIsRemoved.complete(true);
-                            }
-                        }
-                    };
-            controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
-            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()) {
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
-
-            assertThrows(
-                    IllegalArgumentException.class,
-                    () ->
-                            controller.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);
-        }
-    }
-
-    @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);
-        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-        CompletableFuture<Network> networkFuture = new CompletableFuture<>();
-        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-        NetworkRequest networkRequest =
-                new NetworkRequest.Builder()
-                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
-                        .build();
-        ConnectivityManager.NetworkCallback networkCallback =
-                new ConnectivityManager.NetworkCallback() {
-                    @Override
-                    public void onAvailable(Network network) {
-                        networkFuture.complete(network);
-                    }
-                };
-
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
-        runAsShell(
-                ACCESS_NETWORK_STATE,
-                () -> cm.registerNetworkCallback(networkRequest, networkCallback));
-
-        joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
-        runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
-        assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
-    }
 }
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 405fb76..633389f 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -43,7 +43,7 @@
     manifest: "AndroidManifest.xml",
     defaults: [
         "framework-connectivity-test-defaults",
-        "ThreadNetworkIntegrationTestsDefaults"
+        "ThreadNetworkIntegrationTestsDefaults",
     ],
     test_suites: [
         "general-tests",