[Thread] Make ThreadConfiguraiton a system API

This CL adds 3 things as @SystemApi:
- The ThreadConfiguration class except the Builder nested class.
- The ThreadNetworkController#registerConfigurationCallback method.
- The ThreadNetworkController#unregisterConfigurationCallback method.

Bug: 342519412
Test: CtsThreadNetworkTestCases

Change-Id: I8d3e48b73d471af1515f251451a1259a4bde69af
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e09b3a6..be2632c 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -15,7 +15,9 @@
  */
 package android.net.thread;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -37,19 +39,19 @@
  * @see ThreadNetworkController#unregisterConfigurationCallback
  * @hide
  */
-// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-// @SystemApi
+@FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
 public final class ThreadConfiguration implements Parcelable {
     private final boolean mNat64Enabled;
-    private final boolean mDhcp6PdEnabled;
+    private final boolean mDhcpv6PdEnabled;
 
     private ThreadConfiguration(Builder builder) {
-        this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+        this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
     }
 
-    private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
         this.mNat64Enabled = nat64Enabled;
-        this.mDhcp6PdEnabled = dhcp6PdEnabled;
+        this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
     }
 
     /** Returns {@code true} if NAT64 is enabled. */
@@ -58,8 +60,8 @@
     }
 
     /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
-    public boolean isDhcp6PdEnabled() {
-        return mDhcp6PdEnabled;
+    public boolean isDhcpv6PdEnabled() {
+        return mDhcpv6PdEnabled;
     }
 
     @Override
@@ -71,13 +73,13 @@
         } else {
             ThreadConfiguration otherConfig = (ThreadConfiguration) other;
             return mNat64Enabled == otherConfig.mNat64Enabled
-                    && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+                    && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+        return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
     }
 
     @Override
@@ -85,7 +87,7 @@
         StringBuilder sb = new StringBuilder();
         sb.append('{');
         sb.append("Nat64Enabled=").append(mNat64Enabled);
-        sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+        sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
         sb.append('}');
         return sb.toString();
     }
@@ -98,7 +100,7 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeBoolean(mNat64Enabled);
-        dest.writeBoolean(mDhcp6PdEnabled);
+        dest.writeBoolean(mDhcpv6PdEnabled);
     }
 
     public static final @NonNull Creator<ThreadConfiguration> CREATOR =
@@ -107,7 +109,7 @@
                 public ThreadConfiguration createFromParcel(Parcel in) {
                     ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
                     builder.setNat64Enabled(in.readBoolean());
-                    builder.setDhcp6PdEnabled(in.readBoolean());
+                    builder.setDhcpv6PdEnabled(in.readBoolean());
                     return builder.build();
                 }
 
@@ -117,10 +119,14 @@
                 }
             };
 
-    /** The builder for creating {@link ThreadConfiguration} objects. */
+    /**
+     * The builder for creating {@link ThreadConfiguration} objects.
+     *
+     * @hide
+     */
     public static final class Builder {
         private boolean mNat64Enabled = false;
-        private boolean mDhcp6PdEnabled = false;
+        private boolean mDhcpv6PdEnabled = false;
 
         /** Creates a new {@link Builder} object with all features disabled. */
         public Builder() {}
@@ -134,7 +140,7 @@
             Objects.requireNonNull(config);
 
             mNat64Enabled = config.mNat64Enabled;
-            mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+            mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
         }
 
         /**
@@ -156,8 +162,8 @@
          * IPv6.
          */
         @NonNull
-        public Builder setDhcp6PdEnabled(boolean enabled) {
-            this.mDhcp6PdEnabled = enabled;
+        public Builder setDhcpv6PdEnabled(boolean enabled) {
+            this.mDhcpv6PdEnabled = enabled;
             return this;
         }
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 30b3d6a..b4e581c 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -619,16 +619,15 @@
     /**
      * Registers a callback to be called when the configuration is changed.
      *
-     * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+     * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
      * {@link ThreadConfiguration}.
      *
      * @param executor the executor to execute the {@code callback}
      * @param callback the callback to receive Thread configuration changes
      * @throws IllegalArgumentException if {@code callback} has already been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void registerConfigurationCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<ThreadConfiguration> callback) {
@@ -656,10 +655,9 @@
      * @param callback the callback which has been registered with {@link
      *     #registerConfigurationCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
         requireNonNull(callback, "callback cannot be null");
         synchronized (mConfigurationCallbackMapLock) {
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 747cc96..7c4c72d 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -197,7 +197,7 @@
             return false;
         }
         putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
-        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
         writeToStoreFile();
         return true;
     }
@@ -206,7 +206,7 @@
     public ThreadConfiguration getConfiguration() {
         return new ThreadConfiguration.Builder()
                 .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
-                .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
                 .build();
     }
 
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..6db7c9c 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
 
 android_test {
     name: "CtsThreadNetworkTestCases",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
     min_sdk_version: "33",
-    sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     srcs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..386412e
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    public final boolean mIsNat64Enabled;
+    public final boolean mIsDhcpv6PdEnabled;
+
+    @Parameterized.Parameters
+    public static Collection configArguments() {
+        return Arrays.asList(
+                new Object[][] {
+                    {false, false}, // All disabled
+                    {true, false}, // NAT64 enabled
+                    {false, true}, // DHCP6-PD enabled
+                    {true, true}, // All enabled
+                });
+    }
+
+    public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+        mIsNat64Enabled = isNat64Enabled;
+        mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        assertParcelingIsLossless(config);
+    }
+
+    @Test
+    public void builder_correctValuesAreSet() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+
+        assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+        assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+    }
+
+    @Test
+    public void builderConstructor_configsAreEqual() {
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+        assertThat(config1).isEqualTo(config2);
+    }
+}
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 11c4819..1a101b6 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -58,6 +58,7 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
 import android.net.thread.ThreadNetworkController.StateCallback;
@@ -95,9 +96,11 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /** CTS tests for {@link ThreadNetworkController}. */
@@ -110,11 +113,14 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+    private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
     private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
     private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
+    private static final ThreadConfiguration DEFAULT_CONFIG =
+            new ThreadConfiguration.Builder().build();
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
@@ -127,6 +133,9 @@
     private HandlerThread mHandlerThread;
     private TapTestNetworkTracker mTestNetworkTracker;
 
+    private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+            new ArrayList<>();
+
     @Before
     public void setUp() throws Exception {
         mController =
@@ -141,6 +150,7 @@
         mHandlerThread.start();
 
         setEnabledAndWait(mController, true);
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
     }
 
     @After
@@ -148,6 +158,18 @@
         dropAllPermissions();
         leaveAndWait(mController);
         tearDownTestNetwork();
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
+        for (Consumer<ThreadConfiguration> configurationCallback :
+                mConfigurationCallbacksToCleanUp) {
+            try {
+                runAsShell(
+                        THREAD_NETWORK_PRIVILEGED,
+                        () -> mController.unregisterConfigurationCallback(configurationCallback));
+            } catch (IllegalArgumentException e) {
+                // Ignore the exception when the callback is not registered.
+            }
+        }
+        mConfigurationCallbacksToCleanUp.clear();
     }
 
     @Test
@@ -832,6 +854,152 @@
                         NET_CAPABILITY_TRUSTED);
     }
 
+    @Test
+    public void setConfiguration_null_throwsNullPointerException() throws Exception {
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mController.setConfiguration(
+                                null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+    }
+
+    @Test
+    public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> {
+                    mController.setConfiguration(
+                            configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+                });
+    }
+
+    @Test
+    public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+            throws Exception {
+        CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+        Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+        assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                .isEqualTo(DEFAULT_CONFIG);
+    }
+
+    @Test
+    public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+    }
+
+    @Test
+    public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        ConfigurationListener listener = new ConfigurationListener(mController);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config1, mExecutor, newOutcomeReceiver(setFuture1)));
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config2, mExecutor, newOutcomeReceiver(setFuture2)));
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            listener.expectConfiguration(DEFAULT_CONFIG);
+            listener.expectConfiguration(config1);
+            listener.expectConfiguration(config2);
+            listener.expectNoMoreConfiguration();
+        } finally {
+            listener.unregisterConfigurationCallback();
+        }
+    }
+
+    @Test
+    public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    registerConfigurationCallback(mController, mExecutor, callback);
+                    mController.unregisterConfigurationCallback(callback);
+                });
+    }
+
+    @Test
+    public void
+            unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+                    throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+        mController.unregisterConfigurationCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
     private void grantPermissions(String... permissions) {
         for (String permission : permissions) {
             mGrantedPermissions.add(permission);
@@ -1038,6 +1206,35 @@
         }
     }
 
+    private class ConfigurationListener {
+        private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+                mConfigurations.newReadHead();
+        ThreadNetworkController mController;
+        Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+        ConfigurationListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+        }
+
+        public void expectConfiguration(ThreadConfiguration config) {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+        }
+
+        public void expectNoMoreConfiguration() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+        }
+
+        public void unregisterConfigurationCallback() {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterConfigurationCallback(mCallback));
+        }
+    }
+
     private int booleanToEnabledState(boolean enabled) {
         return enabled ? STATE_ENABLED : STATE_DISABLED;
     }
@@ -1052,6 +1249,18 @@
         waitForEnabledState(controller, booleanToEnabledState(enabled));
     }
 
+    private void setConfigurationAndWait(
+            ThreadNetworkController controller, ThreadConfiguration configuration)
+            throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.setConfiguration(
+                                configuration, mExecutor, newOutcomeReceiver(setFuture)));
+        setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
     private CompletableFuture joinRandomizedDataset(
             ThreadNetworkController controller, String networkName) throws Exception {
         ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1118,6 +1327,14 @@
         };
     }
 
+    private void registerConfigurationCallback(
+            ThreadNetworkController controller,
+            Executor executor,
+            Consumer<ThreadConfiguration> callback) {
+        controller.registerConfigurationCallback(executor, callback);
+        mConfigurationCallbacksToCleanUp.add(callback);
+    }
+
     private static void assertDoesNotThrow(ThrowingRunnable runnable) {
         try {
             runnable.run();
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index eaf11b1..a5dc25a 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -716,12 +716,12 @@
         ThreadConfiguration config1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         ThreadConfiguration config2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         ThreadConfiguration config3 =
                 new ThreadConfiguration.Builder(config2).build(); // Same as config2
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index c932ac8..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -152,7 +152,7 @@
         ThreadConfiguration configuration =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
@@ -164,13 +164,13 @@
         ThreadConfiguration configuration1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration1);
         ThreadConfiguration configuration2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
 
         assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
@@ -188,9 +188,9 @@
     }
 
     @Test
-    public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+    public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
         ThreadConfiguration configuration =
-                new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+                new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
         assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);