diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java index 1eddebf586..5d869dfff4 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java @@ -19,10 +19,12 @@ package com.android.server.thread; import android.annotation.Nullable; import android.content.Context; import android.net.thread.ActiveOperationalDataset; +import android.net.thread.IConfigurationReceiver; import android.net.thread.IOperationReceiver; import android.net.thread.IOutputReceiver; import android.net.thread.OperationalDatasetTimestamp; import android.net.thread.PendingOperationalDataset; +import android.net.thread.ThreadConfiguration; import android.net.thread.ThreadNetworkException; import android.os.Binder; import android.os.Process; @@ -56,6 +58,7 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2); private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1); private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5); + private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1); private static final String PERMISSION_THREAD_NETWORK_TESTING = "android.permission.THREAD_NETWORK_TESTING"; @@ -118,6 +121,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { pw.println(" Sets country code to or left for normal value"); pw.println(" ot-ctl "); pw.println(" Runs ot-ctl command"); + pw.println(" config [name] [value]"); + pw.println(" Gets the config or sets the value for a config entry"); } @Override @@ -144,6 +149,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { return forceCountryCode(); case "get-country-code": return getCountryCode(); + case "config": + return handleConfigCommand(); case "ot-ctl": return handleOtCtlCommand(); default: @@ -261,6 +268,68 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { return 0; } + private int handleConfigCommand() { + ensureTestingPermission(); + + // Get config + if (peekNextArg() == null) { + try { + final ThreadConfiguration config = getConfig(); + getOutputWriter().println("Thread configuration = " + config); + } catch (AssertionError e) { + getErrorWriter().println("Failed: " + e.getMessage()); + return -1; + } + return 0; + } + + // Set config + final String name = getNextArg(); + final String value = getNextArg(); + try { + setConfig(name, value); + } catch (AssertionError | IllegalArgumentException e) { + getErrorWriter().println(e.getMessage()); + return -1; + } + return 0; + } + + private ThreadConfiguration getConfig() throws AssertionError { + final CompletableFuture future = new CompletableFuture<>(); + mControllerService.registerConfigurationCallback( + new IConfigurationReceiver.Stub() { + @Override + public void onConfigurationChanged(ThreadConfiguration config) { + future.complete(config); + } + }); + try { + return future.get(CONFIG_TIMEOUT.toSeconds(), TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new AssertionError("Failed to get config within timeout", e); + } + } + + private void setConfig(String name, String value) + throws IllegalArgumentException, AssertionError { + if (name == null || value == null) { + throw new IllegalArgumentException( + "Invalid config name = " + name + ", value=" + value); + } + final ThreadConfiguration oldConfig = getConfig(); + final ThreadConfiguration.Builder newConfigBuilder = + new ThreadConfiguration.Builder(oldConfig); + switch (name) { + case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value)); + case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value)); + default -> throw new IllegalArgumentException("Invalid config name: " + name); + } + CompletableFuture future = new CompletableFuture(); + mControllerService.setConfiguration(newConfigBuilder.build(), newOperationReceiver(future)); + waitForFuture(future, CONFIG_TIMEOUT, mErrorWriter); + } + private static final class OutputReceiver extends IOutputReceiver.Stub { private final CompletableFuture future; private final PrintWriter outputWriter; @@ -359,6 +428,10 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { } } + private static boolean argEnabledOrDisabled(String arg) { + return argTrueOrFalse(arg, "enabled", "disabled"); + } + private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) { String nextArg = getNextArgRequired(); return argTrueOrFalse(nextArg, trueString, falseString); diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java index 87219d33a8..32e3b9565c 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java @@ -19,6 +19,7 @@ package android.net.thread; import static android.net.thread.ThreadNetworkController.STATE_DISABLED; import static android.net.thread.ThreadNetworkController.STATE_ENABLED; import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED; +import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG; import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET; import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; @@ -79,6 +80,7 @@ public class ThreadNetworkShellCommandTest { public void tearDown() throws Exception { mFtd.destroy(); ensureThreadEnabled(); + mController.setConfigurationAndWait(DEFAULT_CONFIG); } private static void ensureThreadEnabled() { @@ -179,6 +181,27 @@ public class ThreadNetworkShellCommandTest { assertThat(result).endsWith("Done\r\n"); } + @Test + public void config_getConfig_expectedValueIsPrinted() throws Exception { + ThreadConfiguration config = + new ThreadConfiguration.Builder().setNat64Enabled(true).build(); + mController.setConfigurationAndWait(config); + + final String result = runThreadCommand("config"); + + assertThat(result).contains("Nat64Enabled=true"); + } + + @Test + public void config_setConfig_expectedValueIsSet() throws Exception { + ThreadConfiguration config = new ThreadConfiguration.Builder().build(); + mController.setConfigurationAndWait(config); + + runThreadCommand("config nat64 enabled"); + + assertThat(mController.getConfiguration().isNat64Enabled()).isTrue(); + } + private static String runThreadCommand(String cmd) { return runShellCommandOrThrow("cmd thread_network " + cmd); } diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt index 116fb72402..d903636c6d 100644 --- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt +++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt @@ -32,6 +32,7 @@ import android.net.TestNetworkInterface import android.net.nsd.NsdManager import android.net.nsd.NsdServiceInfo import android.net.thread.ActiveOperationalDataset +import android.net.thread.ThreadConfiguration import android.net.thread.ThreadNetworkController import android.os.Build import android.os.Handler @@ -108,6 +109,9 @@ object IntegrationTestUtils { val DEFAULT_DATASET: ActiveOperationalDataset = ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS) + @JvmField + val DEFAULT_CONFIG = ThreadConfiguration.Builder().build() + /** * Waits for the given [Supplier] to be true until given timeout. * diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java index af5c9aacf2..c0e99d712d 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java @@ -38,8 +38,11 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.net.thread.ActiveOperationalDataset; +import android.net.thread.IConfigurationReceiver; +import android.net.thread.IOperationReceiver; import android.net.thread.IOutputReceiver; import android.net.thread.PendingOperationalDataset; +import android.net.thread.ThreadConfiguration; import android.os.Binder; import android.os.Process; @@ -320,4 +323,108 @@ public class ThreadNetworkShellCommandTest { inOrder.verify(mOutputWriter).print("Done"); inOrder.verify(mOutputWriter).print("\r\n"); } + + @Test + public void config_getConfig_testingPermissionIsChecked() { + runShellCommand("config"); + + verify(mContext, times(1)) + .enforceCallingOrSelfPermission( + eq("android.permission.THREAD_NETWORK_TESTING"), anyString()); + } + + @Test + public void config_getConfig_serviceTimeOut_failsWithTimeoutError() { + runShellCommand("config"); + + verify(mControllerService, times(1)).registerConfigurationCallback(any()); + verify(mErrorWriter, atLeastOnce()).println(contains("timeout")); + verify(mOutputWriter, never()).println(); + } + + @Test + public void config_getConfig_expectedValueIsPrinted() { + doAnswer( + inv -> { + ((IConfigurationReceiver) inv.getArgument(0)) + .onConfigurationChanged( + new ThreadConfiguration.Builder() + .setNat64Enabled(true) + .build()); + return null; + }) + .when(mControllerService) + .registerConfigurationCallback(any()); + + runShellCommand("config"); + + verify(mErrorWriter, never()).println(); + verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true")); + } + + @Test + public void config_setConfig_testingPermissionIsChecked() { + runShellCommand("config", "nat64", "enabled"); + + verify(mContext, times(1)) + .enforceCallingOrSelfPermission( + eq("android.permission.THREAD_NETWORK_TESTING"), anyString()); + } + + @Test + public void config_setConfig_serviceTimeOut_failedWithTimeoutError() { + runShellCommand("config", "nat64", "enabled"); + + verify(mControllerService, times(1)).registerConfigurationCallback(any()); + verify(mErrorWriter, atLeastOnce()).println(contains("timeout")); + verify(mOutputWriter, never()).println(); + } + + @Test + public void config_invalidArgument_failsWithInvalidArgumentError() { + doAnswer( + inv -> { + ((IConfigurationReceiver) inv.getArgument(0)) + .onConfigurationChanged( + new ThreadConfiguration.Builder().build()); + return null; + }) + .when(mControllerService) + .registerConfigurationCallback(any()); + + runShellCommand("config", "invalidName", "invalidValue"); + + verify(mErrorWriter, atLeastOnce()).println(contains("Invalid config")); + verify(mOutputWriter, never()).println(); + } + + @Test + public void config_setConfig_expectedValueIsSet() { + doAnswer( + inv -> { + ((IConfigurationReceiver) inv.getArgument(0)) + .onConfigurationChanged( + new ThreadConfiguration.Builder() + .setNat64Enabled(false) + .build()); + return null; + }) + .when(mControllerService) + .registerConfigurationCallback(any()); + doAnswer( + inv -> { + ((IOperationReceiver) inv.getArgument(0)).onSuccess(); + return null; + }) + .when(mControllerService) + .setConfiguration(any(), any()); + + runShellCommand("config", "nat64", "enabled"); + + verify(mControllerService, times(1)) + .setConfiguration( + eq(new ThreadConfiguration.Builder().setNat64Enabled(true).build()), any()); + verify(mErrorWriter, never()).println(); + verify(mOutputWriter, never()).println(); + } }