Merge changes from topic "set-thread-enabled" into main

* changes:
  Add service implementation for ThreadNetworkController#setEnabled
  Add setEnabled API
This commit is contained in:
Yang Sun 2024-01-26 11:22:37 +00:00 • committed by Gerrit Code Review
commit 8f9544539b
9 changed files with 616 additions and 132 deletions

View file

@ -500,6 +500,7 @@ package android.net.thread {
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
@ -507,6 +508,9 @@ package android.net.thread {
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
field public static final int STATE_DISABLED = 0; // 0x0
field public static final int STATE_DISABLING = 2; // 0x2
field public static final int STATE_ENABLED = 1; // 0x1
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
@ -518,6 +522,7 @@ package android.net.thread {
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
method public default void onPartitionIdChanged(long);
method public default void onThreadEnableStateChanged(int);
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
@ -530,6 +535,7 @@ package android.net.thread {
field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
field public static final int ERROR_THREAD_DISABLED = 12; // 0xc
field public static final int ERROR_TIMEOUT = 3; // 0x3
field public static final int ERROR_UNAVAILABLE = 4; // 0x4
field public static final int ERROR_UNKNOWN = 11; // 0xb

View file

@ -22,4 +22,5 @@ package android.net.thread;
oneway interface IStateCallback {
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
void onThreadEnableStateChanged(int enabledState);
}

View file

@ -42,4 +42,6 @@ interface IThreadNetworkController {
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
void setEnabled(boolean enabled, in IOperationReceiver receiver);
}

View file

@ -68,6 +68,15 @@ public final class ThreadNetworkController {
/** The device is a Thread Leader. */
public static final int DEVICE_ROLE_LEADER = 4;
/** The Thread radio is disabled. */
public static final int STATE_DISABLED = 0;
/** The Thread radio is enabled. */
public static final int STATE_ENABLED = 1;
/** The Thread radio is being disabled. */
public static final int STATE_DISABLING = 2;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@ -79,6 +88,13 @@ public final class ThreadNetworkController {
})
public @interface DeviceRole {}
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"STATE_"},
value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
public @interface EnabledState {}
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@ -106,6 +122,40 @@ public final class ThreadNetworkController {
mControllerService = controllerService;
}
/**
* Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
* be persistent and survives device reboots.
*
* <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
* require the Thread radio will fail with error code {@link
* ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
* {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
* with error code {@link ThreadNetworkException#ERROR_BUSY}.
*
* <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
* the operation has completed. But there maybe subsequent calls to update the enabled state,
* callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
* enabled state changes.
*
* <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
* specific error in {@link ThreadNetworkException#ERROR_}.
*
* @param enabled {@code true} for enabling Thread
* @param executor the executor to execute {@code receiver}
* @param receiver the receiver to receive result of this operation
*/
@RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
public void setEnabled(
boolean enabled,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
try {
mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@ -170,6 +220,16 @@ public final class ThreadNetworkController {
* @param partitionId the new Thread partition ID
*/
default void onPartitionIdChanged(long partitionId) {}
/**
* The Thread enabled state has changed.
*
* <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
* airplane mode or admin control.
*
* @param enabledState the new Thread enabled state
*/
default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@ -200,6 +260,16 @@ public final class ThreadNetworkController {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onThreadEnableStateChanged(@EnabledState int enabled) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/**

View file

@ -48,6 +48,7 @@ public class ThreadNetworkException extends Exception {
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
ERROR_UNKNOWN,
ERROR_THREAD_DISABLED,
})
public @interface ErrorCode {}
@ -129,6 +130,13 @@ public class ThreadNetworkException extends Exception {
*/
public static final int ERROR_UNKNOWN = 11;
/**
* The operation failed because the Thread radio is disabled by {@link
* ThreadNetworkController#setEnabled}, airplane mode or device admin. The caller should retry
* only after Thread is enabled.
*/
public static final int ERROR_THREAD_DISABLED = 12;
private final int mErrorCode;
/** Creates a new {@link ThreadNetworkException} object with given error code and message. */

View file

@ -27,6 +27,9 @@ import static android.net.thread.ActiveOperationalDataset.LENGTH_PSKC;
import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
@ -35,6 +38,7 @@ import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
@ -48,7 +52,11 @@ import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_THREAD_DISABLED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLED;
import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLING;
import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import android.Manifest.permission;
@ -160,6 +168,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
private final ThreadPersistentSettings mPersistentSettings;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@ -171,7 +180,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController) {
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@ -182,9 +192,11 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
mPersistentSettings = persistentSettings;
}
public static ThreadNetworkControllerService newInstance(Context context) {
public static ThreadNetworkControllerService newInstance(
Context context, ThreadPersistentSettings persistentSettings) {
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
NetworkProvider networkProvider =
@ -197,7 +209,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController());
new InfraInterfaceController(),
persistentSettings);
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@ -273,7 +286,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
otDaemon.initialize(mTunIfController.getTunFd());
otDaemon.initialize(
mTunIfController.getTunFd(),
mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED));
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
@ -308,6 +323,26 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
});
}
public void setEnabled(@NonNull boolean isEnabled, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> setEnabledInternal(isEnabled, new OperationReceiverWrapper(receiver)));
}
private void setEnabledInternal(
@NonNull boolean isEnabled, @Nullable OperationReceiverWrapper receiver) {
// The persistent setting keeps the desired enabled state, thus it's set regardless
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException e) {
Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");
@ -658,6 +693,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
return ERROR_REJECTED_BY_PEER;
case OT_ERROR_UNSUPPORTED_CHANNEL:
return ERROR_UNSUPPORTED_CHANNEL;
case OT_ERROR_THREAD_DISABLED:
return ERROR_THREAD_DISABLED;
default:
return ERROR_INTERNAL_ERROR;
}
@ -1001,6 +1038,15 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
}
private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
try {
callback.onThreadEnableStateChanged(enabledState);
Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
public void unregisterStateCallback(IStateCallback callback) {
checkOnHandlerThread();
if (!mStateCallbacks.containsKey(callback)) {
@ -1064,6 +1110,31 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
}
@Override
public void onThreadEnabledChanged(int state) {
mHandler.post(() -> onThreadEnabledChangedInternal(state));
}
private void onThreadEnabledChangedInternal(int state) {
checkOnHandlerThread();
for (IStateCallback callback : mStateCallbacks.keySet()) {
notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
}
}
private static int otStateToAndroidState(int state) {
switch (state) {
case OT_STATE_ENABLED:
return STATE_ENABLED;
case OT_STATE_DISABLED:
return STATE_DISABLED;
case OT_STATE_DISABLING:
return STATE_DISABLING;
default:
throw new IllegalArgumentException("Unknown ot state " + state);
}
}
@Override
public void onStateChanged(OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));

View file

@ -18,16 +18,21 @@ package com.android.server.thread;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ApexEnvironment;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.util.AtomicFile;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
@ -40,11 +45,18 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
private final Context mContext;
@Nullable private ThreadNetworkCountryCode mCountryCode;
@Nullable private ThreadNetworkControllerService mControllerService;
private final ThreadPersistentSettings mPersistentSettings;
@Nullable private ThreadNetworkShellCommand mShellCommand;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
mContext = context;
mPersistentSettings =
new ThreadPersistentSettings(
new AtomicFile(
new File(
getOrCreateThreadnetworkDir(),
ThreadPersistentSettings.FILE_NAME)));
}
/**
@ -54,7 +66,9 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
*/
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mControllerService = ThreadNetworkControllerService.newInstance(mContext);
mPersistentSettings.initialize();
mControllerService =
ThreadNetworkControllerService.newInstance(mContext, mPersistentSettings);
mControllerService.initialize();
} else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
// Country code initialization is delayed to the BOOT_COMPLETED phase because it will
@ -109,4 +123,19 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
pw.println();
}
/** Get device protected storage dir for the tethering apex. */
private static File getOrCreateThreadnetworkDir() {
final File threadnetworkDir;
final File apexDataDir =
ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
.getDeviceProtectedDataDir();
threadnetworkDir = new File(apexDataDir, "thread");
if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
return threadnetworkDir;
}
throw new IllegalStateException(
"Cannot write into thread network data directory: " + threadnetworkDir);
}
}

View file

@ -16,11 +16,19 @@
package android.net.thread.cts;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@ -29,12 +37,12 @@ import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.Manifest.permission;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
@ -54,12 +62,11 @@ import android.os.OutcomeReceiver;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import com.android.net.module.util.ArrayTrackRecord;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@ -68,9 +75,11 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -81,8 +90,11 @@ import java.util.concurrent.Executors;
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
private static final int MIGRATION_TIMEOUT_MILLIS = 40 * 1_000;
private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@ -95,7 +107,7 @@ public class ThreadNetworkControllerTest {
private Set<String> mGrantedPermissions;
@Before
public void setUp() {
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
mManager = mContext.getSystemService(ThreadNetworkManager.class);
mGrantedPermissions = new HashSet<String>();
@ -103,13 +115,17 @@ public class ThreadNetworkControllerTest {
// TODO: we will also need it in tearDown(), it's better to have a Rule to skip
// tests if a feature is not available.
assumeNotNull(mManager);
for (ThreadNetworkController controller : getAllControllers()) {
setEnabledAndWait(controller, true);
}
}
@After
public void tearDown() throws Exception {
if (mManager != null) {
leaveAndWait();
dropAllPermissions();
leaveAndWait();
}
}
@ -118,12 +134,10 @@ public class ThreadNetworkControllerTest {
}
private void leaveAndWait() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Void> future = SettableFuture.create();
controller.leave(mExecutor, future::set);
future.get();
CompletableFuture<Void> future = new CompletableFuture<>();
leave(controller, future::complete);
future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
}
}
@ -142,8 +156,8 @@ public class ThreadNetworkControllerTest {
private static ActiveOperationalDataset newRandomizedDataset(
String networkName, ThreadNetworkController controller) throws Exception {
SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
controller.createRandomizedDataset(networkName, directExecutor(), future::set);
CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
@ -152,33 +166,140 @@ public class ThreadNetworkControllerTest {
}
private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
SettableFuture<Integer> future = SettableFuture.create();
StateCallback callback = future::set;
CompletableFuture<Integer> future = new CompletableFuture<>();
StateCallback callback = future::complete;
controller.registerStateCallback(directExecutor(), callback);
int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterStateCallback(callback);
return role;
}
private static int waitForAttachedState(ThreadNetworkController controller) throws Exception {
List<Integer> attachedRoles = new ArrayList<>();
attachedRoles.add(DEVICE_ROLE_CHILD);
attachedRoles.add(DEVICE_ROLE_ROUTER);
attachedRoles.add(DEVICE_ROLE_LEADER);
return waitForStateAnyOf(controller, attachedRoles);
}
private static int waitForStateAnyOf(
ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
SettableFuture<Integer> future = SettableFuture.create();
CompletableFuture<Integer> future = new CompletableFuture<>();
StateCallback callback =
newRole -> {
if (deviceRoles.contains(newRole)) {
future.set(newRole);
future.complete(newRole);
}
};
controller.registerStateCallback(directExecutor(), callback);
int role = future.get();
int role = future.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterStateCallback(callback);
return role;
}
private static void waitForEnabledState(ThreadNetworkController controller, int state)
throws Exception {
CompletableFuture<Integer> future = new CompletableFuture<>();
StateCallback callback =
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int r) {}
@Override
public void onThreadEnableStateChanged(int enabled) {
if (enabled == state) {
future.complete(enabled);
}
}
};
runAsShell(
ACCESS_NETWORK_STATE,
() -> controller.registerStateCallback(directExecutor(), callback));
future.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
}
private void leave(
ThreadNetworkController controller,
OutcomeReceiver<Void, ThreadNetworkException> receiver) {
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
}
private void scheduleMigration(
ThreadNetworkController controller,
PendingOperationalDataset pendingDataset,
OutcomeReceiver<Void, ThreadNetworkException> receiver) {
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.scheduleMigration(pendingDataset, mExecutor, receiver));
}
private class EnabledStateListener {
private ArrayTrackRecord<Integer> mEnabledStates = new ArrayTrackRecord<>();
private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mEnabledStates.newReadHead();
ThreadNetworkController mController;
StateCallback mCallback =
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int r) {}
@Override
public void onThreadEnableStateChanged(int enabled) {
mEnabledStates.add(enabled);
}
};
EnabledStateListener(ThreadNetworkController controller) {
this.mController = controller;
runAsShell(
ACCESS_NETWORK_STATE,
() -> controller.registerStateCallback(mExecutor, mCallback));
}
public void expectThreadEnabledState(int enabled) {
assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
}
public void unregisterStateCallback() {
runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
}
}
private int booleanToEnabledState(boolean enabled) {
return enabled ? STATE_ENABLED : STATE_DISABLED;
}
private void setEnabledAndWait(ThreadNetworkController controller, boolean enabled)
throws Exception {
CompletableFuture<Void> setFuture = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture)));
setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
waitForEnabledState(controller, booleanToEnabledState(enabled));
}
private CompletableFuture joinRandomizedDataset(ThreadNetworkController controller)
throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
return joinFuture;
}
private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception {
CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller);
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
}
private static ActiveOperationalDataset getActiveOperationalDataset(
ThreadNetworkController controller) throws Exception {
SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
OperationalDatasetCallback callback = future::set;
CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
OperationalDatasetCallback callback = future::complete;
controller.registerOperationalDatasetCallback(directExecutor(), callback);
ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterOperationalDatasetCallback(callback);
@ -187,27 +308,27 @@ public class ThreadNetworkControllerTest {
private static PendingOperationalDataset getPendingOperationalDataset(
ThreadNetworkController controller) throws Exception {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
controller.registerOperationalDatasetCallback(
directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
return pendingFuture.get();
return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
private static OperationalDatasetCallback newDatasetCallback(
SettableFuture<ActiveOperationalDataset> activeFuture,
SettableFuture<PendingOperationalDataset> pendingFuture) {
CompletableFuture<ActiveOperationalDataset> activeFuture,
CompletableFuture<PendingOperationalDataset> pendingFuture) {
return new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeOpDataset) {
activeFuture.set(activeOpDataset);
activeFuture.complete(activeOpDataset);
}
@Override
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingOpDataset) {
pendingFuture.set(pendingOpDataset);
pendingFuture.complete(pendingOpDataset);
}
};
}
@ -221,22 +342,53 @@ public class ThreadNetworkControllerTest {
@Test
public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = deviceRole::set;
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = deviceRole::complete;
try {
controller.registerStateCallback(mExecutor, callback);
assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
.isEqualTo(DEVICE_ROLE_STOPPED);
} finally {
controller.unregisterStateCallback(callback);
}
}
}
@Test
public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
EnabledStateListener listener = new EnabledStateListener(controller);
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
});
setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
});
setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
listener.expectThreadEnabledState(STATE_ENABLED);
listener.expectThreadEnabledState(STATE_DISABLING);
listener.expectThreadEnabledState(STATE_DISABLED);
listener.expectThreadEnabledState(STATE_ENABLED);
listener.unregisterStateCallback();
}
}
@Test
public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
dropAllPermissions();
@ -251,11 +403,11 @@ public class ThreadNetworkControllerTest {
@Test
public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = role -> deviceRole.complete(role);
controller.registerStateCallback(mExecutor, callback);
assertThrows(
@ -267,9 +419,9 @@ public class ThreadNetworkControllerTest {
@Test
public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
grantPermissions(permission.ACCESS_NETWORK_STATE);
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = role -> deviceRole.complete(role);
grantPermissions(ACCESS_NETWORK_STATE);
controller.registerStateCallback(mExecutor, callback);
try {
@ -278,7 +430,7 @@ public class ThreadNetworkControllerTest {
SecurityException.class,
() -> controller.unregisterStateCallback(callback));
} finally {
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
controller.unregisterStateCallback(callback);
}
}
@ -286,10 +438,10 @@ public class ThreadNetworkControllerTest {
@Test
public void unregisterStateCallback_callbackRegistered_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = role -> deviceRole.complete(role);
controller.registerStateCallback(mExecutor, callback);
controller.unregisterStateCallback(callback);
@ -300,8 +452,8 @@ public class ThreadNetworkControllerTest {
public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = role -> deviceRole.complete(role);
assertThrows(
IllegalArgumentException.class,
@ -312,10 +464,10 @@ public class ThreadNetworkControllerTest {
@Test
public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = deviceRole::set;
CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
StateCallback callback = deviceRole::complete;
controller.registerStateCallback(mExecutor, callback);
controller.unregisterStateCallback(callback);
@ -328,18 +480,18 @@ public class ThreadNetworkControllerTest {
@Test
public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
var callback = newDatasetCallback(activeFuture, pendingFuture);
try {
controller.registerOperationalDatasetCallback(mExecutor, callback);
assertThat(activeFuture.get()).isNull();
assertThat(pendingFuture.get()).isNull();
assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
} finally {
controller.unregisterOperationalDatasetCallback(callback);
}
@ -352,8 +504,8 @@ public class ThreadNetworkControllerTest {
dropAllPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
var callback = newDatasetCallback(activeFuture, pendingFuture);
assertThrows(
@ -364,10 +516,10 @@ public class ThreadNetworkControllerTest {
@Test
public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
var callback = newDatasetCallback(activeFuture, pendingFuture);
controller.registerOperationalDatasetCallback(mExecutor, callback);
@ -381,10 +533,10 @@ public class ThreadNetworkControllerTest {
dropAllPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
var callback = newDatasetCallback(activeFuture, pendingFuture);
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
controller.registerOperationalDatasetCallback(mExecutor, callback);
try {
@ -393,24 +545,23 @@ public class ThreadNetworkControllerTest {
SecurityException.class,
() -> controller.unregisterOperationalDatasetCallback(callback));
} finally {
grantPermissions(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
controller.unregisterOperationalDatasetCallback(callback);
}
}
}
private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
SettableFuture<V> future) {
CompletableFuture<V> future) {
return new OutcomeReceiver<V, ThreadNetworkException>() {
@Override
public void onResult(V result) {
future.set(result);
future.complete(result);
}
@Override
public void onError(ThreadNetworkException e) {
future.setException(e);
future.completeExceptionally(e);
}
};
}
@ -421,12 +572,12 @@ public class ThreadNetworkControllerTest {
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
SettableFuture<Void> joinFuture = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
assertThat(isAttached(controller)).isTrue();
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
}
@ -445,6 +596,20 @@ public class ThreadNetworkControllerTest {
}
}
@Test
public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
setEnabledAndWait(controller, false);
CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller);
ThreadNetworkException thrown =
(ThreadNetworkException)
assertThrows(ExecutionException.class, joinFuture::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
}
}
@Test
public void join_concurrentRequests_firstOneIsAborted() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
@ -461,8 +626,8 @@ public class ThreadNetworkControllerTest {
new ActiveOperationalDataset.Builder(activeDataset1)
.setNetworkKey(KEY_2)
.build();
SettableFuture<Void> joinFuture1 = SettableFuture.create();
SettableFuture<Void> joinFuture2 = SettableFuture.create();
CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
@ -471,8 +636,8 @@ public class ThreadNetworkControllerTest {
(ThreadNetworkException)
assertThrows(ExecutionException.class, joinFuture1::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
joinFuture2.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
grantPermissions(ACCESS_NETWORK_STATE);
assertThat(isAttached(controller)).isTrue();
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
}
@ -480,19 +645,14 @@ public class ThreadNetworkControllerTest {
@Test
public void leave_withPrivilegedPermission_success() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> leaveFuture = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinRandomizedDatasetAndWait(controller);
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
leaveFuture.get();
CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
leave(controller, newOutcomeReceiver(leaveFuture));
leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
grantPermissions(permission.ACCESS_NETWORK_STATE);
grantPermissions(ACCESS_NETWORK_STATE);
assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
}
}
@ -506,31 +666,47 @@ public class ThreadNetworkControllerTest {
}
}
@Test
public void leave_threadDisabled_success() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
joinRandomizedDatasetAndWait(controller);
CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
setEnabledAndWait(controller, false);
leave(controller, newOutcomeReceiver(leaveFuture));
leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
runAsShell(
ACCESS_NETWORK_STATE,
() -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED));
}
}
@Test
public void leave_concurrentRequests_bothSuccess() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> leaveFuture1 = SettableFuture.create();
SettableFuture<Void> leaveFuture2 = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
leaveFuture1.get();
leaveFuture2.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
grantPermissions(ACCESS_NETWORK_STATE);
assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
}
}
@Test
public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset1 =
@ -549,24 +725,24 @@ public class ThreadNetworkControllerTest {
activeDataset2,
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
migrateFuture.get();
migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
OperationalDatasetCallback datasetCallback =
new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset) {
if (activeDataset.equals(activeDataset2)) {
dataset2IsApplied.set(true);
dataset2IsApplied.complete(true);
}
}
@ -574,20 +750,20 @@ public class ThreadNetworkControllerTest {
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingDataset) {
if (pendingDataset == null) {
pendingDatasetIsRemoved.set(true);
pendingDatasetIsRemoved.complete(true);
}
}
};
controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
assertThat(dataset2IsApplied.get()).isTrue();
assertThat(pendingDatasetIsRemoved.get()).isTrue();
assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
@Test
public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
PendingOperationalDataset pendingDataset =
@ -595,7 +771,7 @@ public class ThreadNetworkControllerTest {
newRandomizedDataset("TestNet", controller),
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
SettableFuture<Void> migrateFuture = SettableFuture.create();
CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
controller.scheduleMigration(
pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
@ -610,7 +786,7 @@ public class ThreadNetworkControllerTest {
@Test
public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
final ActiveOperationalDataset activeDataset =
@ -638,15 +814,15 @@ public class ThreadNetworkControllerTest {
activeDataset2,
new OperationalDatasetTimestamp(20, 0, false),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture1 = SettableFuture.create();
SettableFuture<Void> migrateFuture2 = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.scheduleMigration(
pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
migrateFuture1.get();
migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
@ -660,7 +836,7 @@ public class ThreadNetworkControllerTest {
@Test
public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
grantPermissions(ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
final ActiveOperationalDataset activeDataset =
@ -688,28 +864,28 @@ public class ThreadNetworkControllerTest {
activeDataset2,
new OperationalDatasetTimestamp(200, 0, false),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture1 = SettableFuture.create();
SettableFuture<Void> migrateFuture2 = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.scheduleMigration(
pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
migrateFuture1.get();
migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
migrateFuture2.get();
migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
OperationalDatasetCallback datasetCallback =
new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset) {
if (activeDataset.equals(activeDataset2)) {
dataset2IsApplied.set(true);
dataset2IsApplied.complete(true);
}
}
@ -717,17 +893,40 @@ public class ThreadNetworkControllerTest {
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingDataset) {
if (pendingDataset == null) {
pendingDatasetIsRemoved.set(true);
pendingDatasetIsRemoved.complete(true);
}
}
};
controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
assertThat(dataset2IsApplied.get()).isTrue();
assertThat(pendingDatasetIsRemoved.get()).isTrue();
assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
@Test
public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
PendingOperationalDataset pendingDataset =
new PendingOperationalDataset(
activeDataset,
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
joinRandomizedDatasetAndWait(controller);
CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
setEnabledAndWait(controller, false);
scheduleMigration(controller, pendingDataset, newOutcomeReceiver(migrationFuture));
ThreadNetworkException thrown =
(ThreadNetworkException)
assertThrows(ExecutionException.class, migrationFuture::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
}
}
@Test
public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
for (ThreadNetworkController controller : getAllControllers()) {
@ -759,12 +958,106 @@ public class ThreadNetworkControllerTest {
}
}
@Test
public void setEnabled_permissionsGranted_succeeds() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
waitForEnabledState(controller, booleanToEnabledState(false));
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
waitForEnabledState(controller, booleanToEnabledState(true));
}
}
@Test
public void setEnabled_noPermissions_throwsSecurityException() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
CompletableFuture<Void> setFuture = new CompletableFuture<>();
assertThrows(
SecurityException.class,
() -> controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
}
}
@Test
public void setEnabled_disable_leavesThreadNetwork() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
joinRandomizedDatasetAndWait(controller);
setEnabledAndWait(controller, false);
runAsShell(
ACCESS_NETWORK_STATE,
() -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED));
}
}
@Test
public void setEnabled_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
joinRandomizedDatasetAndWait(controller);
setEnabledAndWait(controller, false);
runAsShell(
ACCESS_NETWORK_STATE,
() -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED));
setEnabledAndWait(controller, true);
runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(controller));
}
}
@Test
public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
joinRandomizedDatasetAndWait(controller);
CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
EnabledStateListener listener = new EnabledStateListener(controller);
listener.expectThreadEnabledState(STATE_ENABLED);
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
controller.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1));
controller.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2));
});
setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
listener.expectThreadEnabledState(STATE_DISABLING);
listener.expectThreadEnabledState(STATE_DISABLED);
runAsShell(
ACCESS_NETWORK_STATE,
() -> assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED));
listener.unregisterStateCallback();
}
}
// TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
// (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
// because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
// sent before state changes to DISABLED.
@Test
public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
ThreadNetworkController controller = mManager.getAllThreadNetworkControllers().get(0);
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Network> networkFuture = SettableFuture.create();
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
CompletableFuture<Network> networkFuture = new CompletableFuture<>();
ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
NetworkRequest networkRequest =
new NetworkRequest.Builder()
@ -774,7 +1067,7 @@ public class ThreadNetworkControllerTest {
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
networkFuture.set(network);
networkFuture.complete(network);
}
};
@ -782,12 +1075,11 @@ public class ThreadNetworkControllerTest {
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
runAsShell(
permission.ACCESS_NETWORK_STATE,
ACCESS_NETWORK_STATE,
() -> cm.registerNetworkCallback(networkRequest, networkCallback));
joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
runAsShell(
permission.ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
runAsShell(ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
}
}

View file

@ -24,6 +24,7 @@ import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@ -85,6 +86,7 @@ public final class ThreadNetworkControllerServiceTest {
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
@Mock private ThreadPersistentSettings mMockPersistentSettings;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
@ -104,6 +106,8 @@ public final class ThreadNetworkControllerServiceTest {
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
when(mMockPersistentSettings.get(any())).thenReturn(true);
mService =
new ThreadNetworkControllerService(
ApplicationProvider.getApplicationContext(),
@ -112,7 +116,8 @@ public final class ThreadNetworkControllerServiceTest {
() -> mFakeOtDaemon,
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController);
mMockInfraIfController,
mMockPersistentSettings);
mService.setTestNetworkAgent(mMockNetworkAgent);
}