Merge changes from topic "set-thread-enabled" into main
* changes:
Add service implementation for ThreadNetworkController#setEnabled
Add setEnabled API
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index d346af3..8251f85 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -500,6 +500,7 @@
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 @@
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 @@
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 @@
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
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index d7cbda9..9d0a571 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -22,4 +22,5 @@
oneway interface IStateCallback {
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
+ void onThreadEnableStateChanged(int enabledState);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index a9da8d6..485e25d 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -42,4 +42,6 @@
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
+
+ void setEnabled(boolean enabled, in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 7242ed7..db761a3 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -68,6 +68,15 @@
/** 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 @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 @@
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 @@
* @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 @@
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);
+ }
+ }
}
/**
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index af0a84b..23ed53e 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -48,6 +48,7 @@
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
ERROR_UNKNOWN,
+ ERROR_THREAD_DISABLED,
})
public @interface ErrorCode {}
@@ -129,6 +130,13 @@
*/
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. */
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 1c51c42..7b9f290 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -27,6 +27,9 @@
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_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_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 @@
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
+ private final ThreadPersistentSettings mPersistentSettings;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -171,7 +180,8 @@
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
- InfraInterfaceController infraIfController) {
+ InfraInterfaceController infraIfController,
+ ThreadPersistentSettings persistentSettings) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -182,9 +192,11 @@
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 @@
() -> 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 @@
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 @@
});
}
+ 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 @@
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 @@
}
}
+ 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)) {
@@ -1065,6 +1111,31 @@
}
@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));
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index 53f2d4f..5cf27f7 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -18,16 +18,21 @@
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 @@
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 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 @@
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);
+ }
}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 7a6c9aa..7a129dc 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -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.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 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 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 @@
@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 @@
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 @@
// 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 @@
}
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 @@
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 @@
}
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 @@
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,16 +342,17 @@
@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);
}
@@ -238,6 +360,36 @@
}
@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 @@
@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 @@
@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 @@
SecurityException.class,
() -> controller.unregisterStateCallback(callback));
} finally {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
+ grantPermissions(ACCESS_NETWORK_STATE);
controller.unregisterStateCallback(callback);
}
}
@@ -286,10 +438,10 @@
@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 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 @@
@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 @@
@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 @@
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 @@
@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 @@
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 @@
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 @@
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);
}
@@ -446,6 +597,20 @@
}
@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 @@
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 @@
(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 @@
@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);
}
}
@@ -507,30 +667,46 @@
}
@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 @@
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 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 @@
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 @@
@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 @@
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 @@
@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 @@
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,18 +893,41 @@
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()) {
assertThrows(
@@ -760,11 +959,105 @@
}
@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 @@
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
- networkFuture.set(network);
+ networkFuture.complete(network);
}
};
@@ -782,12 +1075,11 @@
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();
}
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 44a8ab7..1d83abc 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -24,6 +24,7 @@
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 @@
@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 @@
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+ when(mMockPersistentSettings.get(any())).thenReturn(true);
+
mService =
new ThreadNetworkControllerService(
ApplicationProvider.getApplicationContext(),
@@ -112,7 +116,8 @@
() -> mFakeOtDaemon,
mMockConnectivityManager,
mMockTunIfController,
- mMockInfraIfController);
+ mMockInfraIfController,
+ mMockPersistentSettings);
mService.setTestNetworkAgent(mMockNetworkAgent);
}