Merge "[Thread] support Thread NetworkRequest" into main

This commit is contained in:
Treehugger Robot 2025-09-14 21:59:51 -07:00 • committed by Android (Google) Code Review
commit 4741933ee8
11 changed files with 662 additions and 34 deletions

View file

@ -21,6 +21,7 @@ package {
filegroup {
name: "framework-thread-sources",
defaults: ["framework-sources-module-defaults"],
srcs: [
"java/**/*.java",
"java/**/*.aidl",

View file

@ -602,17 +602,18 @@ public final class ActiveOperationalDataset implements Parcelable {
// SparseArray<byte[]> is not hashable
int result = byteArraySparseArrayHashCode(mChannelMask);
result = result * 31 + byteArraySparseArrayHashCode(mUnknownTlvs);
return result * 31 + deepHashCode(
mActiveTimestamp,
mNetworkName,
mExtendedPanId,
mPanId,
mChannel,
mChannelPage,
mPskc,
mNetworkKey,
mMeshLocalPrefix,
mSecurityPolicy);
return result * 31
+ deepHashCode(
mActiveTimestamp,
mNetworkName,
mExtendedPanId,
mPanId,
mChannel,
mChannelPage,
mPskc,
mNetworkKey,
mMeshLocalPrefix,
mSecurityPolicy);
}
@Override

View file

@ -112,6 +112,10 @@ public final class ThreadNetworkSpecifier extends NetworkSpecifier implements Pa
/** @hide */
@Override
public NetworkSpecifier redact(long redactions) {
if (mActiveDataset == null) {
return new ThreadNetworkSpecifier.Builder(this).build();
}
var redactedDatasetBuilder = new ActiveOperationalDataset.Builder(mActiveDataset);
if ((redactions & REDACT_FOR_THREAD_NETWORK_PRIVILEGED) != 0
|| (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0) {

View file

@ -66,6 +66,7 @@ import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import android.Manifest.permission;
import android.annotation.NonNull;
@ -89,7 +90,6 @@ import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.NetworkScore;
import android.net.TestNetworkSpecifier;
@ -110,11 +110,13 @@ import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkException.ErrorCode;
import android.net.thread.ThreadNetworkSpecifier;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
@ -198,7 +200,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
private final NetworkProvider mNetworkProvider;
private final ThreadNetworkFactory mNetworkFactory;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final RoutingCoordinatorManager mRoutingCoordinatorManager;
@ -233,12 +235,14 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private InfraLinkState mInfraLinkState;
@Nullable private ThreadNetworkSpecifier mJoinNetworkSpecifier;
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
Handler handler,
MockableSystemProperties systemProperties,
NetworkProvider networkProvider,
ThreadNetworkFactory networkFactory,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
RoutingCoordinatorManager routingCoordinatorManager,
@ -253,7 +257,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mContext = context;
mHandler = handler;
mSystemProperties = systemProperties;
mNetworkProvider = networkProvider;
mNetworkFactory = networkFactory;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mRoutingCoordinatorManager = routingCoordinatorManager;
@ -278,8 +282,6 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
Map<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
final ConnectivityManager connectivityManager =
context.getSystemService(ConnectivityManager.class);
@ -291,7 +293,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
context,
handler,
new MockableSystemProperties(),
networkProvider,
ThreadNetworkFactory.newInstance(context, handler.getLooper()),
() -> IOtDaemon.Stub.asInterface(CommonConnectivityJni.waitForService("ot_daemon")),
connectivityManager,
routingCoordinatorManager,
@ -501,7 +503,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
} catch (IOException e) {
throw new IllegalStateException("Failed to create Thread tunnel interface", e);
}
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
mNetworkFactory.initialize(this);
mNetworkFactory.register();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
@ -904,31 +907,47 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mTestNetworkAgent = testNetworkAgent;
}
private NetworkCapabilities computeNetworkCapabilities(
@Nullable ActiveOperationalDataset activeDataset) {
final var netCapsBuilder =
new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
if (activeDataset != null) {
boolean createPartition =
(mJoinNetworkSpecifier != null)
? mJoinNetworkSpecifier.shouldCreatePartitionIfNotFound()
: false;
final var actualSpecifier =
new ThreadNetworkSpecifier.Builder()
.setActiveOperationalDataset(activeDataset)
.setShouldCreatePartitionIfNotFound(createPartition)
.build();
netCapsBuilder.setNetworkSpecifier(actualSpecifier);
}
return netCapsBuilder.build();
}
private NetworkAgent newNetworkAgent() {
if (mTestNetworkAgent != null) {
return mTestNetworkAgent;
}
final var netCapsBuilder =
new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
final var scoreBuilder = new NetworkScore.Builder();
netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
return new NetworkAgent(
mContext,
mHandler.getLooper(),
LOG.getTag(),
netCapsBuilder.build(),
computeNetworkCapabilities(mOtDaemonCallbackProxy.mActiveDataset),
getTunIfLinkProperties(),
newLocalNetworkConfig(),
scoreBuilder.build(),
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {
mNetworkFactory.getProvider()) {
// TODO(b/374037595): use NetworkFactory to handle dynamic network requests
@Override
@ -1235,6 +1254,22 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
};
}
private IOtStatusReceiver newOtStatusReceiver(
OutcomeReceiver<Void, ThreadNetworkException> receiver) {
return new IOtStatusReceiver.Stub() {
@Override
public void onSuccess() {
receiver.onResult(null);
}
@Override
public void onError(int otError, String message) {
receiver.onError(
new ThreadNetworkException(otErrorToAndroidError(otError), message));
}
};
}
private IOtOutputReceiver newOtOutputReceiver(OutputReceiverWrapper receiver) {
return new IOtOutputReceiver.Stub() {
@Override
@ -1299,15 +1334,46 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
@NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
mJoinNetworkSpecifier = null;
try {
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
getOtDaemon()
.join(
activeDataset.toThreadTlvs(),
true /* createPartitionIfNotFound */,
newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
LOG.e("otDaemon.join failed", e);
receiver.onError(e);
}
}
void joinWithSpecifier(
ThreadNetworkSpecifier specifier,
OutcomeReceiver<Void, ThreadNetworkException> receiver) {
mHandler.post(() -> joinWithSpecifierInternal(specifier, receiver));
}
private void joinWithSpecifierInternal(
ThreadNetworkSpecifier specifier,
OutcomeReceiver<Void, ThreadNetworkException> receiver) {
checkOnHandlerThread();
mJoinNetworkSpecifier = specifier;
byte[] dataset = requireNonNull(specifier.getActiveOperationalDataset()).toThreadTlvs();
boolean createPartitionIfNotFound = specifier.shouldCreatePartitionIfNotFound();
try {
getOtDaemon().join(dataset, createPartitionIfNotFound, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
LOG.e("otDaemon.joinWithSpecifier failed", e);
if (e instanceof RemoteException re) {
receiver.onError(new ThreadNetworkException(ERROR_INTERNAL_ERROR, re.getMessage()));
} else {
receiver.onError((ThreadNetworkException) e);
}
}
}
@Override
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@ -1509,6 +1575,13 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
setInfraLinkState(newInfraLinkStateBuilder().build());
}
private void handleActiveDatasetChanged(@Nullable ActiveOperationalDataset newActiveDataset) {
if (mNetworkAgent == null) {
return;
}
mNetworkAgent.sendNetworkCapabilities(computeNetworkCapabilities(newActiveDataset));
}
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
@ -1994,6 +2067,14 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
ActiveOperationalDataset activeDataset, long listenerId) {
checkOnHandlerThread();
boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
if (hasChange) {
LOG.i(
"Current Active Operational Dataset changed: "
+ mActiveDataset
+ " -> "
+ activeDataset);
handleActiveDatasetChanged(activeDataset);
}
for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
if (!hasChange && callbackEntry.getValue().id != listenerId) {

View file

@ -0,0 +1,163 @@
/*
* Copyright (C) 2025 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 com.android.server.thread;
import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.MatchAllNetworkSpecifier;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkRequest;
import android.net.thread.IOperationReceiver;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkSpecifier;
import android.os.Looper;
import android.os.OutcomeReceiver;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
// TODO(b/442787508): replace NetworkFactory subclass with direct usage of NetworkProvider
/**
* Serves network requests that are initiated from apps via the {@link
* ConnectivityManager#requestNetwork} API.
*/
public final class ThreadNetworkFactory extends NetworkFactory {
private static final SharedLog LOG =
ThreadNetworkLogger.forSubComponent("ThreadNetworkFactory");
private final List<NetworkRequest> mActiveRequests = new ArrayList<>();
private @Nullable ThreadNetworkControllerService mControllerService;
/** Creates a new {@link ThreadNetworkFactory} instance. */
public static ThreadNetworkFactory newInstance(Context context, Looper looper) {
return new ThreadNetworkFactory(context, looper);
}
@VisibleForTesting
ThreadNetworkFactory(Context context, Looper looper) {
super(looper, context, LOG.getTag(), createBaseCapabilities());
mControllerService = null;
}
void initialize(ThreadNetworkControllerService controllerService) {
mControllerService = controllerService;
}
/** Returns the basic capabilities that all Thread networks initially have. */
private static NetworkCapabilities createBaseCapabilities() {
return new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.setLinkUpstreamBandwidthKbps(100)
.setLinkDownstreamBandwidthKbps(100)
// By default accepts all network requests which have a specifier
.setNetworkSpecifier(new MatchAllNetworkSpecifier())
.build();
}
@Override
public boolean acceptRequest(@NonNull final NetworkRequest request) {
// the filtering is done in {@link #needNetworkFor}
return true;
}
@Override
public void needNetworkFor(@NonNull final NetworkRequest request) {
LOG.i("network request is received for " + request);
if (!request.hasTransport(TRANSPORT_THREAD)) {
return;
}
if (request.getNetworkSpecifier() instanceof ThreadNetworkSpecifier specifier
&& specifier.getActiveOperationalDataset() != null) {
mActiveRequests.add(request);
join(specifier);
} else {
// Can do nothing if ThreadNetworkSpecifier or the Dataset is missing
return;
}
}
private void join(ThreadNetworkSpecifier specifier) {
var receiver =
new OutcomeReceiver<Void, ThreadNetworkException>() {
@Override
public void onError(ThreadNetworkException e) {
LOG.e("Failed to join Thread network: " + specifier, e);
}
@Override
public void onResult(Void v) {
LOG.i("Successfully joined Thread network: " + specifier);
}
};
mControllerService.joinWithSpecifier(specifier, receiver);
}
@Override
public void releaseNetworkFor(@NonNull final NetworkRequest request) {
LOG.i("network release request is received for " + request);
if (mActiveRequests.isEmpty()) {
LOG.w("Unexpect release of untracked request " + request);
return;
}
NetworkRequest lastRequest = mActiveRequests.getLast();
mActiveRequests.removeAll(Collections.singletonList(request));
if (mActiveRequests.isEmpty()) {
leave(request);
} else if (!Objects.equals(mActiveRequests.getLast(), lastRequest)) {
// If the last request is changed, let's re-join the network with the new latest
// request
join((ThreadNetworkSpecifier) mActiveRequests.getLast().getNetworkSpecifier());
}
}
private void leave(NetworkRequest request) {
var receiver =
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
LOG.i("Successfully left Thread network for releasing request " + request);
}
@Override
public void onError(int errorCode, String errorMessage) {
var ex = new ThreadNetworkException(errorCode, errorMessage);
LOG.e(
"Failed to leave Thread network for releasing request " + request,
ex);
}
};
mControllerService.leave(receiver);
}
}

View file

@ -0,0 +1,227 @@
/*
* Copyright (C) 2025 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 android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.THREAD_NETWORK_PRIVILEGED;
import static android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO;
import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.net.thread.flags.Flags.FLAG_THREAD_MOBILE_ENABLED;
import static com.google.common.io.BaseEncoding.base16;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkRequest;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ThreadNetworkSpecifier;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.platform.test.annotations.RequiresFlagsEnabled;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import com.android.testutils.TestableNetworkCallback;
import com.android.testutils.TestableNetworkCallback.Event;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Tests for requesting a Thread network via the {@link ConnectivityManager#requestNetwork} and
* {@link ThreadNetworkSpecifier} API.
*/
@LargeTest
@RequiresThreadFeature
@RequiresFlagsEnabled(FLAG_THREAD_MOBILE_ENABLED)
public final class ThreadNetworkRequestTest {
private static final int JOIN_TIMEOUT_MILLIS = 30_000;
private static final Duration STOP_TIMEOUT = Duration.ofSeconds(2_000);
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 13
// Wake-up Channel: 18
// Channel Mask: 0x07fff800
// Ext PAN ID: 492cc112e634e51b
// Mesh Local Prefix: fd2f:357c:dd90:2120::/64
// Network Key: 9f55333f52eb25dbfdbe62121b39bee4
// Network Name: OpenThread-145a
// PAN ID: 0x145a
// PSKc: 6d81fe700160ab9f63f50f78ccf0733c
// Security Policy: 672 onrc 0
private static final byte[] DATASET_TLVS_1 =
base16().ignoreCase()
.decode(
"0e080000000000010000000300000d4a0300001235060004001fff"
+ "e00208492cc112e634e51b0708fd2f357cdd90212005109f55333f"
+ "52eb25dbfdbe62121b39bee4030f4f70656e5468726561642d3134"
+ "35610102145a04106d81fe700160ab9f63f50f78ccf0733c0c0402a0f7f8");
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 24
// Wake-up Channel: 19
// Channel Mask: 0x07fff800
// Ext PAN ID: 779732128edb4591
// Mesh Local Prefix: fdbd:3671:821d:18c1::/64
// Network Key: 6a643ec02d38ef55b21b26cec46f622d
// Network Name: OpenThread-64be
// PAN ID: 0x64be
// PSKc: 37968aab079fac83fa67b14c9bb7f598
// Security Policy: 672 onrc 0
private static final byte[] DATASET_TLVS_2 =
base16().ignoreCase()
.decode(
"0e08000000000001000000030000184a0300001335060004001fff"
+ "e00208779732128edb45910708fdbd3671821d18c105106a643ec0"
+ "2d38ef55b21b26cec46f622d030f4f70656e5468726561642d3634"
+ "6265010264be041037968aab079fac83fa67b14c9bb7f5980c0402a0f7f8");
private static final ActiveOperationalDataset TEST_DATASET_1 =
ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS_1);
private static final ActiveOperationalDataset TEST_DATASET_2 =
ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS_2);
private final Context mContext = ApplicationProvider.getApplicationContext();
private ConnectivityManager cm;
private ThreadNetworkControllerWrapper mController;
private List<TestableNetworkCallback> mCallbacks;
private Set<String> mGrantedPermissions;
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
}
String[] allPermissions = new String[mGrantedPermissions.size()];
mGrantedPermissions.toArray(allPermissions);
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
private static void dropAllPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
@Before
public void setUp() throws Exception {
mCallbacks = new ArrayList<>();
mGrantedPermissions = new HashSet<String>();
cm = mContext.getSystemService(ConnectivityManager.class);
mController = ThreadNetworkControllerWrapper.newInstance(mContext);
mController.setBorderRouterEnabledAndWait(false);
mController.leaveAndWait();
}
@After
public void tearDown() throws Exception {
dropAllPermissions();
for (var callback : mCallbacks) {
cm.unregisterNetworkCallback(callback);
}
mController.waitForRole(DEVICE_ROLE_STOPPED, STOP_TIMEOUT);
}
private TestableNetworkCallback newNetworkCallback() {
var callback =
new TestableNetworkCallback(
250L /* timeoutMs */,
250L /* noCallbackTimeoutMs */,
() -> {} /* waiterFunc */,
"ThreadNetworkRequestTest" /* logTag */,
FLAG_INCLUDE_LOCATION_INFO);
mCallbacks.add(callback);
return callback;
}
private NetworkRequest newThreadNetworkRequest(
ActiveOperationalDataset dataset, boolean createPartitionIfNotFound) {
var specifier =
new ThreadNetworkSpecifier.Builder()
.setActiveOperationalDataset(dataset)
.setShouldCreatePartitionIfNotFound(createPartitionIfNotFound)
.build();
return new NetworkRequest.Builder()
.addTransportType(TRANSPORT_THREAD)
.setNetworkSpecifier(specifier)
.build();
}
@Test
public void requestNetwork_withoutThreadPrivilegedPermission_failsWithUnavailable() {
dropAllPermissions();
var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */);
var callback = newNetworkCallback();
assertThrows(SecurityException.class, () -> cm.requestNetwork(request, callback));
mCallbacks.remove(callback);
}
@Test
public void requestNetwork_withCreatePartitionEnabled_succeedsWithAvailable() {
grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION);
var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */);
var callback = newNetworkCallback();
cm.requestNetwork(request, callback);
callback.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS);
}
@Test
public void requestNetwork_multipleRequests_theLastOneWins() {
grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION);
var request1 =
newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */);
var request2 =
newThreadNetworkRequest(TEST_DATASET_2, true /* createPartitionIfNotFound */);
var callback1 = newNetworkCallback();
var callback2 = newNetworkCallback();
cm.requestNetwork(request1, callback1);
cm.requestNetwork(request2, callback2);
callback2.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS);
}
@Test
public void unregisterNetworkCallback_threadIsStopped() throws Exception {
grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION);
var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */);
var callback = newNetworkCallback();
cm.requestNetwork(request, callback);
callback.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS);
cm.unregisterNetworkCallback(callback);
mCallbacks.remove(callback);
dropAllPermissions();
mController.waitForRole(DEVICE_ROLE_STOPPED, STOP_TIMEOUT);
}
}

View file

@ -20,6 +20,7 @@
package="android.net.thread.unittests">
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.NETWORK_FACTORY" />
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />

View file

@ -179,8 +179,11 @@ public final class ThreadNetworkControllerServiceTest {
private static final String TEST_VENDOR_NAME = "test vendor";
private static final String TEST_MODEL_NAME = "test model";
private static final LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24");
private static final int THREAD_NETWORK_PROVIDER_ID = 1;
@Mock private MockableSystemProperties mMockSystemProperties;
@Mock private ThreadNetworkFactory mMockNetworkFactory;
@Mock private NetworkProvider mMockNetworkProvider;
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
@Mock private NetworkAgent mMockNetworkAgent;
@ -233,8 +236,12 @@ public final class ThreadNetworkControllerServiceTest {
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
when(mMockNetworkFactory.getProvider()).thenReturn(mMockNetworkProvider);
doNothing().when(mMockNetworkFactory).register();
when(mMockConnectivityManager.registerNetworkProvider(any(NetworkProvider.class)))
.thenReturn(THREAD_NETWORK_PROVIDER_ID);
when(mMockRoutingCoordinatorManager.requestDownstreamAddress(any()))
.thenReturn(TEST_NAT64_CIDR);
@ -272,7 +279,7 @@ public final class ThreadNetworkControllerServiceTest {
mContext,
handler,
mMockSystemProperties,
networkProvider,
mMockNetworkFactory,
() -> mFakeOtDaemon,
mMockConnectivityManager,
mMockRoutingCoordinatorManager,

View file

@ -0,0 +1,137 @@
/*
* Copyright (C) 2025 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 com.android.server.thread;
import static com.google.common.io.BaseEncoding.base16;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import android.content.Context;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ThreadNetworkSpecifier;
import android.os.test.TestLooper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link ThreadNetworkFactory}. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public final class ThreadNetworkFactoryTest {
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 19
// Channel Mask: 0x07FFF800
// Ext PAN ID: ACC214689BC40BDF
// Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
// Network Key: F26B3153760F519A63BAFDDFFC80D2AF
// Network Name: OpenThread-d9a0
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
private static final byte[] DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS);
private Context mContext;
private TestLooper mTestLooper;
@Mock private ThreadNetworkControllerService mMockControllerService;
private ThreadNetworkFactory mFactory;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = ApplicationProvider.getApplicationContext();
mTestLooper = new TestLooper();
mFactory = new ThreadNetworkFactory(mContext, mTestLooper.getLooper());
mFactory.initialize(mMockControllerService);
}
@Test
public void needNetworkFor_noThreadTransport_doNothing() {
NetworkRequest request =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build();
mFactory.needNetworkFor(request);
verifyNoInteractions(mMockControllerService);
}
@Test
public void needNetworkFor_threadTransportAndNoSpecifier_doNothing() {
NetworkRequest request =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.build();
mFactory.needNetworkFor(request);
verifyNoInteractions(mMockControllerService);
}
@Test
public void needNetworkFor_threadTransportAndSpecifier_joinIsCalled() {
ThreadNetworkSpecifier specifier =
new ThreadNetworkSpecifier.Builder()
.setActiveOperationalDataset(ACTIVE_DATASET)
.build();
NetworkRequest request =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.setNetworkSpecifier(specifier)
.build();
mFactory.needNetworkFor(request);
verify(mMockControllerService).joinWithSpecifier(eq(specifier), any());
}
@Test
public void needNetworkFor_specifierWithNoDataset_doNothing() {
NetworkRequest request =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.setNetworkSpecifier(new ThreadNetworkSpecifier.Builder().build())
.build();
mFactory.needNetworkFor(request);
verifyNoInteractions(mMockControllerService);
}
}

View file

@ -17,8 +17,8 @@ package android.net.thread.utils;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static com.android.testutils.TestableNetworkCallback.Event.LINK_PROPERTIES_CHANGED;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.android.testutils.TestableNetworkCallback.Event.LINK_PROPERTIES_CHANGED;
import android.content.Context;
import android.net.ConnectivityManager;

View file

@ -189,7 +189,7 @@ public final class ThreadNetworkControllerWrapper {
try {
return future.get(timeout.toSeconds(), SECONDS);
} finally {
mController.unregisterStateCallback(callback);
runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
}
}
@ -230,6 +230,12 @@ public final class ThreadNetworkControllerWrapper {
future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS);
}
public void setBorderRouterEnabledAndWait(boolean enabled) throws Exception {
final ThreadConfiguration config =
new ThreadConfiguration.Builder().setBorderRouterEnabled(enabled).build();
setConfigurationAndWait(config);
}
public void setNat64EnabledAndWait(boolean enabled) throws Exception {
final ThreadConfiguration config = getConfiguration();
final ThreadConfiguration newConfig =