[thread] Support non-BR mode

This commit adds a resource overlay config for OEMs to disable the
Border Router feature by default. Also updates the tests to cover both
BR and non-BR use cases

Bug: 401654864
Change-Id: I7dd4363836269a08853b58304f62fe52f9021a9e
This commit is contained in:
Kangping Dong 2025-03-08 12:50:09 +08:00
commit 82988c3d98
13 changed files with 306 additions and 51 deletions

View file

@ -26,6 +26,10 @@
-->
<bool name="config_thread_default_enabled">true</bool>
<!-- Sets to {@code true} to enable Thread Border Router on the device by default.
-->
<bool name="config_thread_border_router_default_enabled">false</bool>
<!-- Whether to use location APIs in the algorithm to determine country code or not.
If disabled, will use other sources (telephony, wifi, etc) to determine device location for
Thread Network regulatory purposes.

View file

@ -227,8 +227,8 @@ public final class ThreadNetworkController {
* specific error:
*
* <ul>
* <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
* attached to Thread network
* <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not a
* Border Router or not attached to Thread network
* <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
* on the device, caller can recover from this error when the ephemeral key mode gets
* deactivated
@ -267,7 +267,8 @@ public final class ThreadNetworkController {
* connection will be terminated.
*
* <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
* always succeed if the device is not in ephemeral key mode.
* always succeed if the device is not in ephemeral key mode. It returns an error {@link
* ThreadNetworkException#ERROR_FAILED_PRECONDITION} if this device is not a Border Router.
*
* @param executor the executor to execute {@code receiver}
* @param receiver the receiver to receive the result of this operation

View file

@ -879,10 +879,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
final var scoreBuilder = new NetworkScore.Builder();
if (isBorderRouterMode()) {
netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
}
netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
return new NetworkAgent(
mContext,
@ -890,7 +888,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
LOG.getTag(),
netCapsBuilder.build(),
getTunIfLinkProperties(),
isBorderRouterMode() ? newLocalNetworkConfig() : null,
newLocalNetworkConfig(),
scoreBuilder.build(),
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {
@ -899,9 +897,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
@Override
public void onNetworkUnwanted() {
LOG.i("Thread network is unwanted by ConnectivityService");
if (!isBorderRouterMode()) {
leave(false /* eraseDataset */, new LoggingOperationReceiver("leave"));
}
// TODO(b/374037595): leave() the current network when the new APIs for mobile
// is available
}
};
}

View file

@ -82,7 +82,7 @@ public class ThreadPersistentSettings {
* this config is missing.
*/
private static final Key<Boolean> CONFIG_BORDER_ROUTER_ENABLED =
new Key<>("config_border_router_enabled", true);
new Key<>("config_border_router_enabled", false);
/** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
private static final Key<Boolean> CONFIG_NAT64_ENABLED =
@ -123,10 +123,23 @@ public class ThreadPersistentSettings {
readFromStoreFile();
synchronized (mLock) {
if (!mSettings.containsKey(THREAD_ENABLED.key)) {
LOG.i("\"thread_enabled\" is missing in settings file, using default value");
put(
THREAD_ENABLED.key,
mResources.get().getBoolean(R.bool.config_thread_default_enabled));
boolean enabled = mResources.get().getBoolean(R.bool.config_thread_default_enabled);
LOG.i(
"\"thread_enabled\" is missing in settings file, using default value: "
+ enabled);
put(THREAD_ENABLED.key, enabled);
}
if (!mSettings.containsKey(CONFIG_BORDER_ROUTER_ENABLED.key)) {
boolean borderRouterEnabled =
mResources
.get()
.getBoolean(R.bool.config_thread_border_router_default_enabled);
LOG.i(
"\"thread_border_router_enabled\" is missing in settings file, using"
+ " default value: "
+ borderRouterEnabled);
put(CONFIG_BORDER_ROUTER_ENABLED.key, borderRouterEnabled);
}
}
}

View file

@ -49,6 +49,8 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -94,12 +96,15 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -118,6 +123,7 @@ import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
@RequiresThreadFeature
@RunWith(Parameterized.class)
public class ThreadNetworkControllerTest {
private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
@ -134,8 +140,6 @@ public class ThreadNetworkControllerTest {
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();
private static final SparseIntArray CHANNEL_MAX_POWERS =
new SparseIntArray() {
{
@ -161,6 +165,22 @@ public class ThreadNetworkControllerTest {
private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
new ArrayList<>();
public final boolean mIsBorderRouterEnabled;
private final ThreadConfiguration mDefaultConfig;
@Parameterized.Parameters
public static Collection configArguments() {
return Arrays.asList(new Object[][] {{false}, {true}});
}
public ThreadNetworkControllerTest(boolean isBorderRouterEnabled) {
mIsBorderRouterEnabled = isBorderRouterEnabled;
mDefaultConfig =
new ThreadConfiguration.Builder()
.setBorderRouterEnabled(isBorderRouterEnabled)
.build();
}
@Before
public void setUp() throws Exception {
mController =
@ -175,8 +195,10 @@ public class ThreadNetworkControllerTest {
mHandlerThread.start();
setEnabledAndWait(mController, true);
setConfigurationAndWait(mController, DEFAULT_CONFIG);
deactivateEphemeralKeyModeAndWait(mController);
setConfigurationAndWait(mController, mDefaultConfig);
if (mDefaultConfig.isBorderRouterEnabled()) {
deactivateEphemeralKeyModeAndWait(mController);
}
}
@After
@ -185,7 +207,7 @@ public class ThreadNetworkControllerTest {
setEnabledAndWait(mController, true);
leaveAndWait(mController);
tearDownTestNetwork();
setConfigurationAndWait(mController, DEFAULT_CONFIG);
setConfigurationAndWait(mController, mDefaultConfig);
for (Consumer<ThreadConfiguration> configurationCallback :
mConfigurationCallbacksToCleanUp) {
try {
@ -197,7 +219,9 @@ public class ThreadNetworkControllerTest {
}
}
mConfigurationCallbacksToCleanUp.clear();
deactivateEphemeralKeyModeAndWait(mController);
if (mDefaultConfig.isBorderRouterEnabled()) {
deactivateEphemeralKeyModeAndWait(mController);
}
}
@Test
@ -573,7 +597,7 @@ public class ThreadNetworkControllerTest {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset) {
if (activeDataset.equals(activeDataset2)) {
if (Objects.equals(activeDataset, activeDataset2)) {
dataset2IsApplied.complete(true);
}
}
@ -843,6 +867,7 @@ public class ThreadNetworkControllerTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> startFuture = new CompletableFuture<>();
@ -861,6 +886,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
dropAllPermissions();
assertThrows(
@ -874,6 +900,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
assertThrows(
@ -885,6 +912,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
@ -897,6 +925,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> future1 = new CompletableFuture<>();
CompletableFuture<Void> future2 = new CompletableFuture<>();
@ -945,6 +974,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
dropAllPermissions();
assertThrows(
@ -956,9 +986,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition()
throws Exception {
setConfigurationAndWait(
mController,
new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
assumeFalse(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
CompletableFuture<Void> future = new CompletableFuture<>();
@ -975,6 +1003,7 @@ public class ThreadNetworkControllerTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
@ -1011,6 +1040,7 @@ public class ThreadNetworkControllerTest {
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
@ -1050,6 +1080,7 @@ public class ThreadNetworkControllerTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
joinRandomizedDatasetAndWait(mController);
@ -1068,6 +1099,7 @@ public class ThreadNetworkControllerTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
assumeTrue(mDefaultConfig.isBorderRouterEnabled());
EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
Triple<Integer, String, Instant> epskc1;
try {
@ -1173,7 +1205,7 @@ public class ThreadNetworkControllerTest {
THREAD_NETWORK_PRIVILEGED,
() -> registerConfigurationCallback(mController, mExecutor, callback));
assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
.isEqualTo(DEFAULT_CONFIG);
.isEqualTo(mDefaultConfig);
}
@Test
@ -1216,7 +1248,7 @@ public class ThreadNetworkControllerTest {
setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
listener.expectConfiguration(DEFAULT_CONFIG);
listener.expectConfiguration(mDefaultConfig);
listener.expectConfiguration(config1);
listener.expectConfiguration(config2);
listener.expectNoMoreConfiguration();

View file

@ -19,7 +19,7 @@ package android.net.thread;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
import static android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork;
import static android.net.thread.utils.IntegrationTestUtils.enableBorderRouterAndJoinNetwork;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
@ -128,7 +128,7 @@ public class BorderRoutingTest {
@BeforeClass
public static void beforeClass() throws Exception {
enableThreadAndJoinNetwork(DEFAULT_DATASET);
enableBorderRouterAndJoinNetwork(DEFAULT_DATASET);
}
@AfterClass

View file

@ -21,6 +21,7 @@ import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWait;
import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
@ -115,6 +116,8 @@ public class ServiceDiscoveryTest {
public void setUp() throws Exception {
mController.setEnabledAndWait(true);
mController.leaveAndWait();
var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(true).build();
mController.setConfigurationAndWait(config);
mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
@ -157,6 +160,143 @@ public class ServiceDiscoveryTest {
mController.leaveAndWait();
}
@Test
public void advertisingProxy_borderRouterDisabled_clientServiceRemovedWhenLeaveIsCalled()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* SRP Server / AD Proxy -------------- SRP Client
*
* </pre>
*/
// The Border Router / SRP Server mode can only be changed when Thread is disconnected
mController.leaveAndWait();
var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
mController.setConfigurationAndWait(config);
mController.joinAndWait(DEFAULT_DATASET);
FullThreadDevice srpClient = mFtds.get(0);
joinNetworkAndWait(srpClient, DEFAULT_DATASET);
srpClient.setSrpHostname("thread-srp-client-host");
srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
srpClient.addSrpService(
"thread-srp-client-service",
"_matter._tcp",
List.of("_sub1", "_sub2"),
12345 /* port */,
Map.of("key1", bytes(1), "key2", bytes(2)));
NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
assertThat(discoveredService).isNotNull();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(mNsdManager, "_matter._tcp", serviceLostFuture);
mController.leaveAndWait();
// Verify the service becomes lost.
try {
serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_matter._tcp"));
}
@Test
public void advertisingProxy_borderRouterDisabled_clientServiceRemovedWhen2ndSrpServerEnabled()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* SRP Server / AD Proxy -------------- SRP Client
* (Cuttlefish) |
* +------- 2nd SRP Server
*
* </pre>
*/
// The Border Router / SRP Server mode can only be changed when Thread is disconnected
mController.leaveAndWait();
var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
mController.setConfigurationAndWait(config);
mController.joinAndWait(DEFAULT_DATASET);
FullThreadDevice srpClient = mFtds.get(0);
joinNetworkAndWait(srpClient, DEFAULT_DATASET);
srpClient.setSrpHostname("thread-srp-client-host");
srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
srpClient.addSrpService(
"thread-srp-client-service",
"_matter._tcp",
List.of("_sub1", "_sub2"),
12345 /* port */,
Map.of("key1", bytes(1), "key2", bytes(2)));
NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
assertThat(discoveredService).isNotNull();
FullThreadDevice srpServer2 = mFtds.get(1);
joinNetworkAndWait(srpServer2, DEFAULT_DATASET);
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(mNsdManager, "_matter._tcp", serviceLostFuture);
srpServer2.setSrpServerEnabled(true);
// Verify the service becomes lost.
try {
serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_matter._tcp"));
}
@Test
public void advertisingProxy_borderRouterDisabled_clientMleIdAddressIsAdvertised()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* SRP Server / AD Proxy -------------- SRP Client
* (Cuttlefish)
*
* </pre>
*/
// The Border Router / SRP Server mode can only be changed when Thread is disconnected
mController.leaveAndWait();
var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
mController.setConfigurationAndWait(config);
mController.joinAndWait(DEFAULT_DATASET);
FullThreadDevice srpClient = mFtds.getFirst();
joinNetworkAndWait(srpClient, DEFAULT_DATASET);
srpClient.setSrpHostname("thread-srp-client-host");
srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
srpClient.addSrpService(
"thread-srp-client-service",
"_matter._tcp",
List.of("_sub1", "_sub2"),
12345 /* port */,
Map.of("key1", bytes(1), "key2", bytes(2)));
NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
assertThat(discoveredService).isNotNull();
NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
assertThat(resolvedService.getServiceName()).isEqualTo("thread-srp-client-service");
assertThat(resolvedService.getServiceType()).isEqualTo("_matter._tcp");
assertThat(resolvedService.getPort()).isEqualTo(12345);
assertThat(resolvedService.getAttributes())
.comparingValuesUsing(BYTE_ARRAY_EQUALITY)
.containsExactly("key1", bytes(1), "key2", bytes(2));
assertThat(resolvedService.getHostname()).isEqualTo("thread-srp-client-host");
assertThat(resolvedService.getHostAddresses()).containsExactly(srpClient.getMlEid());
}
@Test
public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
throws Exception {

View file

@ -17,12 +17,10 @@
package android.net.thread;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData;
@ -56,6 +54,7 @@ import android.net.NetworkRequest;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.net.thread.utils.ThreadStateListener;
@ -63,13 +62,13 @@ import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.net.DatagramPacket;
@ -78,6 +77,7 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@ -86,7 +86,7 @@ import java.util.concurrent.Executors;
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
@RequiresThreadFeature
@RunWith(AndroidJUnit4.class)
@RunWith(Parameterized.class)
public class ThreadIntegrationTest {
// The byte[] buffer size for UDP tests
private static final int UDP_BUFFER_SIZE = 1024;
@ -109,8 +109,6 @@ public class ThreadIntegrationTest {
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
private static final ThreadConfiguration DEFAULT_CONFIG =
new ThreadConfiguration.Builder().build();
private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
@ -128,11 +126,28 @@ public class ThreadIntegrationTest {
private OtDaemonController mOtCtl;
private FullThreadDevice mFtd;
public final boolean mIsBorderRouterEnabled;
private final ThreadConfiguration mConfig;
@Parameterized.Parameters
public static Collection configArguments() {
return Arrays.asList(new Object[][] {{false}, {true}});
}
public ThreadIntegrationTest(boolean isBorderRouterEnabled) {
mIsBorderRouterEnabled = isBorderRouterEnabled;
mConfig =
new ThreadConfiguration.Builder()
.setBorderRouterEnabled(isBorderRouterEnabled)
.build();
}
@Before
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
mOtCtl = new OtDaemonController();
mController.setEnabledAndWait(true);
mController.setConfigurationAndWait(mConfig);
mController.leaveAndWait();
mFtd = new FullThreadDevice(10 /* nodeId */);
}
@ -143,7 +158,6 @@ public class ThreadIntegrationTest {
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy();
mExecutor.shutdownNow();
@ -163,6 +177,7 @@ public class ThreadIntegrationTest {
@Test
public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
throws Exception {
assumeTrue(mController.getConfiguration().isBorderRouterEnabled());
mController.joinAndWait(DEFAULT_DATASET);
runShellCommand("stop ot-daemon");
@ -228,6 +243,7 @@ public class ThreadIntegrationTest {
}
@Test
@RequiresSimulationThreadDevice
public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
// Topology:
// Test App ------ thread-wpan ------ End Device
@ -287,6 +303,7 @@ public class ThreadIntegrationTest {
}
@Test
@RequiresSimulationThreadDevice
public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
@ -361,21 +378,15 @@ public class ThreadIntegrationTest {
}
@Test
public void setConfiguration_disableBorderRouter_noBrfunctionsEnabled() throws Exception {
NetworkRequest request =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.build();
@RequiresSimulationThreadDevice
public void setConfiguration_disableBorderRouter_borderRoutingDisabled() throws Exception {
startFtdLeader(mFtd, DEFAULT_DATASET);
mController.setConfigurationAndWait(
new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
mController.joinAndWait(DEFAULT_DATASET);
NetworkCapabilities caps = registerNetworkCallbackAndWait(request);
assertThat(caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)).isFalse();
assertThat(mOtCtl.getBorderRoutingState()).ignoringCase().isEqualTo("disabled");
assertThat(mOtCtl.getSrpServerState()).ignoringCase().isNotEqualTo("disabled");
// TODO: b/376217403 - enables / disables Border Agent at runtime
}

View file

@ -34,6 +34,7 @@ import android.content.Context;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
@ -167,6 +168,7 @@ public class ThreadNetworkShellCommandTest {
}
@Test
@RequiresSimulationThreadDevice
public void handleOtCtlCommand_pingFtd_getValidResponse() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);

View file

@ -239,6 +239,12 @@ public final class FullThreadDevice {
executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
}
/** Sets `true` to enable SRP server on this device. */
public void setSrpServerEnabled(boolean enabled) {
String cmd = enabled ? "enable" : "disable";
executeCommand("srp server " + cmd);
}
/** Enables the SRP client and run in autostart mode. */
public void autoStartSrpClient() {
executeCommand("srp client autostart enable");

View file

@ -582,6 +582,17 @@ object IntegrationTestUtils {
return networkFuture[timeout.toSeconds(), TimeUnit.SECONDS]
}
/**
* Let the FTD join the specified Thread network and wait for it becomes a Child or Router.
*/
@JvmStatic
@Throws(Exception::class)
fun joinNetworkAndWait(ftd: FullThreadDevice, dataset: ActiveOperationalDataset) {
ftd.factoryReset()
ftd.joinNetwork(dataset)
ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
}
/**
* Let the FTD join the specified Thread network and wait for border routing to be available.
*
@ -613,6 +624,21 @@ object IntegrationTestUtils {
controller.joinAndWait(dataset);
}
/** Enables Border Router and joins the specified Thread network. */
@JvmStatic
fun enableBorderRouterAndJoinNetwork(dataset: ActiveOperationalDataset) {
val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
controller.leaveAndWait();
controller.setEnabledAndWait(true);
val config = ThreadConfiguration.Builder().setBorderRouterEnabled(true).build();
controller.setConfigurationAndWait(config);
controller.joinAndWait(dataset);
}
/** Leaves the Thread network and disables Thread. */
@JvmStatic
fun leaveNetworkAndDisableThread() {

View file

@ -233,7 +233,10 @@ public final class ThreadNetworkControllerWrapper {
public void setNat64EnabledAndWait(boolean enabled) throws Exception {
final ThreadConfiguration config = getConfiguration();
final ThreadConfiguration newConfig =
new ThreadConfiguration.Builder(config).setNat64Enabled(enabled).build();
new ThreadConfiguration.Builder(config)
.setBorderRouterEnabled(true)
.setNat64Enabled(enabled)
.build();
setConfigurationAndWait(newConfig);
}

View file

@ -42,6 +42,7 @@ import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@ -93,7 +94,6 @@ import android.util.AtomicFile;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.connectivity.resources.R;
@ -112,6 +112,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
@ -124,6 +125,8 @@ import java.time.Clock;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -132,7 +135,7 @@ import java.util.concurrent.atomic.AtomicReference;
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWith(Parameterized.class)
// This test doesn't really need to run on the UI thread, but @Before and @Test annotated methods
// need to run in the same thread because there are code in {@code ThreadNetworkControllerService}
// checking that all its methods are running in the thread of the handler it's using. This is due
@ -200,6 +203,17 @@ public final class ThreadNetworkControllerServiceTest {
@Rule(order = 1)
public final TemporaryFolder tempFolder = new TemporaryFolder();
private final boolean mIsBorderRouterEnabled;
@Parameterized.Parameters
public static Collection configArguments() {
return Arrays.asList(new Object[][] {{false}, {true}});
}
public ThreadNetworkControllerServiceTest(boolean isBorderRouterEnabled) {
mIsBorderRouterEnabled = isBorderRouterEnabled;
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@ -231,6 +245,8 @@ public final class ThreadNetworkControllerServiceTest {
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
when(mResources.getBoolean(eq(R.bool.config_thread_border_router_default_enabled)))
.thenReturn(mIsBorderRouterEnabled);
when(mResources.getBoolean(
eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
.thenReturn(true);
@ -920,7 +936,9 @@ public final class ThreadNetworkControllerServiceTest {
}
@Test
public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
public void initialize_borderRouterEnabled_upstreamNetworkRequestHasExpectedTransportAndCaps() {
assumeTrue(mIsBorderRouterEnabled);
mService.initialize();
mTestLooper.dispatchAll();
@ -999,7 +1017,8 @@ public final class ThreadNetworkControllerServiceTest {
}
@Test
public void activateEphemeralKeyMode_succeed() throws Exception {
public void activateEphemeralKeyMode_borderRouterEnabled_succeed() throws Exception {
assumeTrue(mIsBorderRouterEnabled);
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@ -1010,7 +1029,8 @@ public final class ThreadNetworkControllerServiceTest {
}
@Test
public void deactivateEphemeralKeyMode_succeed() throws Exception {
public void deactivateEphemeralKeyMode_borderRouterEnabled_succeed() throws Exception {
assumeTrue(mIsBorderRouterEnabled);
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);