[Thread] add ThreadNetworkController APIs

The ThreadNetworkController class provides APIs for managing the Thread
network, for example, attach to a specific network, form a network with
given dataset or update/migrate an existing network.

Bug: 262683651
Test: atest CtsThreadNetworkTestCases
Change-Id: Ib3c267d2c81a8c3c7772ed3c9cd2092487cc941a
This commit is contained in:
Kangping Dong 2023-08-30 19:15:04 +08:00
commit b1c75dbe8c
19 changed files with 2388 additions and 23 deletions

View file

@ -80,6 +80,7 @@ apex {
first: {
jni_libs: [
"libservice-connectivity",
"libservice-thread-jni",
"libandroid_net_connectivity_com_android_net_module_util_jni",
],
native_shared_libs: [

View file

@ -494,9 +494,47 @@ package android.net.thread {
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
method public int getThreadVersion();
method public static boolean isAttached(int);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @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 leave(@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 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 public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
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 THREAD_VERSION_1_3 = 4; // 0x4
}
public static interface ThreadNetworkController.OperationalDatasetCallback {
method public void onActiveOperationalDatasetChanged(@Nullable android.net.thread.ActiveOperationalDataset);
method public default void onPendingOperationalDatasetChanged(@Nullable android.net.thread.PendingOperationalDataset);
}
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
method public default void onPartitionIdChanged(long);
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
ctor public ThreadNetworkException(int, @NonNull String);
method public int getErrorCode();
field public static final int ERROR_ABORTED = 2; // 0x2
field public static final int ERROR_BUSY = 5; // 0x5
field public static final int ERROR_FAILED_PRECONDITION = 6; // 0x6
field public static final int ERROR_INTERNAL_ERROR = 1; // 0x1
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_TIMEOUT = 3; // 0x3
field public static final int ERROR_UNAVAILABLE = 4; // 0x4
field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import android.net.thread.ActiveOperationalDataset;
/** Receives the result of an operation which returns an Active Operational Dataset. @hide */
oneway interface IActiveOperationalDatasetReceiver {
void onSuccess(in ActiveOperationalDataset dataset);
void onError(int errorCode, String errorMessage);
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
/** Receives the result of a Thread network operation. @hide */
oneway interface IOperationReceiver {
void onSuccess();
void onError(int errorCode, String errorMessage);
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.PendingOperationalDataset;
/**
* @hide
*/
oneway interface IOperationalDatasetCallback {
void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
/** Receives the result of {@link ThreadNetworkManager#scheduleMigration}. @hide */
oneway interface IScheduleMigrationReceiver {
void onScheduled(long delayTimerMillis);
void onMigrated();
void onError(int errorCode, String errorMessage);
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
/**
* @hide
*/
oneway interface IStateCallback {
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
}

View file

@ -16,10 +16,28 @@
package android.net.thread;
import android.net.Network;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IOperationReceiver;
import android.net.thread.IScheduleMigrationReceiver;
import android.net.thread.IStateCallback;
import android.net.thread.PendingOperationalDataset;
/**
* Interface for communicating with ThreadNetworkControllerService.
* @hide
*/
interface IThreadNetworkController {
void registerStateCallback(in IStateCallback callback);
void unregisterStateCallback(in IStateCallback callback);
void registerOperationalDatasetCallback(in IOperationalDatasetCallback callback);
void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
int getThreadVersion();
}

View file

@ -18,23 +18,64 @@ package android.net.thread;
import static java.util.Objects.requireNonNull;
import android.Manifest.permission;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Provides the primary API for controlling all aspects of a Thread network.
* Provides the primary APIs for controlling all aspects of a Thread network.
*
* <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
* migrate an existing network.
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ThreadNetworkController {
private static final String TAG = "ThreadNetworkController";
/** The Thread stack is stopped. */
public static final int DEVICE_ROLE_STOPPED = 0;
/** The device is not currently participating in a Thread network/partition. */
public static final int DEVICE_ROLE_DETACHED = 1;
/** The device is a Thread Child. */
public static final int DEVICE_ROLE_CHILD = 2;
/** The device is a Thread Router. */
public static final int DEVICE_ROLE_ROUTER = 3;
/** The device is a Thread Leader. */
public static final int DEVICE_ROLE_LEADER = 4;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DEVICE_ROLE_STOPPED,
DEVICE_ROLE_DETACHED,
DEVICE_ROLE_CHILD,
DEVICE_ROLE_ROUTER,
DEVICE_ROLE_LEADER
})
public @interface DeviceRole {}
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@ -46,9 +87,19 @@ public final class ThreadNetworkController {
private final IThreadNetworkController mControllerService;
private final Object mStateCallbackMapLock = new Object();
@GuardedBy("mStateCallbackMapLock")
private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
private final Object mOpDatasetCallbackMapLock = new Object();
@GuardedBy("mOpDatasetCallbackMapLock")
private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
mOpDatasetCallbackMap = new HashMap<>();
ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
requireNonNull(controllerService, "controllerService cannot be null");
mControllerService = controllerService;
}
@ -61,4 +112,362 @@ public final class ThreadNetworkController {
throw e.rethrowFromSystemServer();
}
}
/** Returns {@code true} if {@code deviceRole} indicates an attached state. */
public static boolean isAttached(@DeviceRole int deviceRole) {
return deviceRole == DEVICE_ROLE_CHILD
|| deviceRole == DEVICE_ROLE_ROUTER
|| deviceRole == DEVICE_ROLE_LEADER;
}
/**
* Callback to receive notifications when the Thread network states are changed.
*
* <p>Applications which are interested in monitoring Thread network states should implement
* this interface and register the callback with {@link #registerStateCallback}.
*/
public interface StateCallback {
/**
* The Thread device role has changed.
*
* @param deviceRole the new Thread device role
*/
void onDeviceRoleChanged(@DeviceRole int deviceRole);
/**
* The Thread network partition ID has changed.
*
* @param partitionId the new Thread partition ID
*/
default void onPartitionIdChanged(long partitionId) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
private final Executor mExecutor;
private final StateCallback mCallback;
StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
}
@Override
public void onPartitionIdChanged(long partitionId) {
mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
}
}
/**
* Registers a callback to be called when Thread network states are changed.
*
* <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
* existing states.
*
* @param executor the executor to execute the {@code callback}
* @param callback the callback to receive Thread network state changes
* @throws IllegalArgumentException if {@code callback} has already been registered
*/
@RequiresPermission(permission.ACCESS_NETWORK_STATE)
public void registerStateCallback(
@NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
requireNonNull(executor, "executor cannot be null");
requireNonNull(callback, "callback cannot be null");
synchronized (mStateCallbackMapLock) {
if (mStateCallbackMap.containsKey(callback)) {
throw new IllegalArgumentException("callback has already been registered");
}
StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
mStateCallbackMap.put(callback, callbackProxy);
try {
mControllerService.registerStateCallback(callbackProxy);
} catch (RemoteException e) {
mStateCallbackMap.remove(callback);
e.rethrowFromSystemServer();
}
}
}
/**
* Unregisters the Thread state changed callback.
*
* @param callback the callback which has been registered with {@link #registerStateCallback}
* @throws IllegalArgumentException if {@code callback} hasn't been registered
*/
public void unregisterStateCallback(@NonNull StateCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mStateCallbackMapLock) {
StateCallbackProxy callbackProxy = mStateCallbackMap.remove(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterStateCallback(callbackProxy);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}
/**
* Callback to receive notifications when the Thread Operational Datasets are changed.
*
* <p>Applications which are interested in monitoring Thread network datasets should implement
* this interface and register the callback with {@link #registerOperationalDatasetCallback}.
*/
public interface OperationalDatasetCallback {
/**
* Called when the Active Operational Dataset is changed.
*
* @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
* absent
*/
void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
/**
* Called when the Pending Operational Dataset is changed.
*
* @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
* has been committed and removed
*/
default void onPendingOperationalDatasetChanged(
@Nullable PendingOperationalDataset pendingDataset) {}
}
private static final class OperationalDatasetCallbackProxy
extends IOperationalDatasetCallback.Stub {
private final Executor mExecutor;
private final OperationalDatasetCallback mCallback;
OperationalDatasetCallbackProxy(
@CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onActiveOperationalDatasetChanged(
@Nullable ActiveOperationalDataset activeDataset) {
mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
}
@Override
public void onPendingOperationalDatasetChanged(
@Nullable PendingOperationalDataset pendingDataset) {
mExecutor.execute(() -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
}
}
/**
* Registers a callback to be called when Thread Operational Datasets are changed.
*
* <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
* existing Operational Datasets.
*
* @param executor the executor to execute {@code callback}
* @param callback the callback to receive Operational Dataset changes
* @throws IllegalArgumentException if {@code callback} has already been registered
*/
@RequiresPermission(
allOf = {
permission.ACCESS_NETWORK_STATE,
"android.permission.THREAD_NETWORK_PRIVILEGED"
})
public void registerOperationalDatasetCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull OperationalDatasetCallback callback) {
requireNonNull(executor, "executor cannot be null");
requireNonNull(callback, "callback cannot be null");
synchronized (mOpDatasetCallbackMapLock) {
if (mOpDatasetCallbackMap.containsKey(callback)) {
throw new IllegalArgumentException("callback has already been registered");
}
OperationalDatasetCallbackProxy callbackProxy =
new OperationalDatasetCallbackProxy(executor, callback);
mOpDatasetCallbackMap.put(callback, callbackProxy);
try {
mControllerService.registerOperationalDatasetCallback(callbackProxy);
} catch (RemoteException e) {
mOpDatasetCallbackMap.remove(callback);
e.rethrowFromSystemServer();
}
}
}
/**
* Unregisters the Thread Operational Dataset callback.
*
* @param callback the callback which has been registered with {@link
* #registerOperationalDatasetCallback}
* @throws IllegalArgumentException if {@code callback} hasn't been registered
*/
public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mOpDatasetCallbackMapLock) {
OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.remove(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}
/**
* Joins to a Thread network with given Active Operational Dataset.
*
* <p>This method does nothing if this device has already joined to the same network specified
* by {@code activeDataset}. If this device has already joined to a different network, this
* device will first leave from that network and then join the new network. This method changes
* only this device and all other connected devices will stay in the old network. To change the
* network for all connected devices together, use {@link #scheduleMigration}.
*
* <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
* will be persisted on this device; this device will try to attach to the Thread network and
* the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
* OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
*
* <ul>
* <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
* specifies a channel which is not supported in the current country or region; the {@code
* activeDataset} is rejected and not persisted so this device won't auto re-join the next
* time
* <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
* {@code join} or {@code leave} operation
* </ul>
*
* @param activeDataset the Active Operational Dataset represents the Thread network to join
* @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 join(
@NonNull ActiveOperationalDataset activeDataset,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
requireNonNull(activeDataset, "activeDataset cannot be null");
requireNonNull(executor, "executor cannot be null");
requireNonNull(receiver, "receiver cannot be null");
try {
mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Schedules a network migration which moves all devices in the current connected network to a
* new network or updates parameters of the current connected network.
*
* <p>The migration doesn't happen immediately but is registered to the Leader device so that
* all devices in the current Thread network can be scheduled to apply the new dataset together.
*
* <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
* {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
* changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
* has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
* OutcomeReceiver#onError} will be called with a specific error:
*
* <ul>
* <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
* because this device is not attached
* <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
* specifies a channel which is not supported in the current country or region; the {@code
* pendingDataset} is rejected and not persisted
* <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
* by the Leader device
* <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
* being processed
* <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
* been received before deadline
* </ul>
*
* <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
* It's important to select a proper value to safely migrate all devices in the network without
* leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
* if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
* network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
*
* @param pendingDataset the Pending Operational Dataset
* @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 scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
requireNonNull(pendingDataset, "pendingDataset cannot be null");
requireNonNull(executor, "executor cannot be null");
requireNonNull(receiver, "receiver cannot be null");
try {
mControllerService.scheduleMigration(
pendingDataset, new OperationReceiverProxy(executor, receiver));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Leaves from the Thread network.
*
* <p>This undoes a {@link join} operation. On success, this device is disconnected from the
* joined network and will not automatically join a network before {@link #join} is called
* again. Active and Pending Operational Dataset configured and persisted on this device will be
* removed too.
*
* @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 leave(
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
requireNonNull(executor, "executor cannot be null");
requireNonNull(receiver, "receiver cannot be null");
try {
mControllerService.leave(new OperationReceiverProxy(executor, receiver));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
int errorCode,
String errorMsg) {
executor.execute(() -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
}
private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
final Executor mExecutor;
final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
OperationReceiverProxy(
@CallbackExecutor Executor executor,
OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
this.mExecutor = executor;
this.mResultReceiver = resultReceiver;
}
@Override
public void onSuccess() {
mExecutor.execute(() -> mResultReceiver.onResult(null));
}
@Override
public void onError(int errorCode, String errorMessage) {
propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
}
}
}

View file

@ -0,0 +1,137 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Represents a Thread network specific failure.
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public class ThreadNetworkException extends Exception {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
ERROR_INTERNAL_ERROR,
ERROR_ABORTED,
ERROR_TIMEOUT,
ERROR_UNAVAILABLE,
ERROR_BUSY,
ERROR_FAILED_PRECONDITION,
ERROR_UNSUPPORTED_CHANNEL,
ERROR_REJECTED_BY_PEER,
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
})
public @interface ErrorCode {}
/**
* The operation failed because some invariants expected by the underlying system have been
* broken. This error code is reserved for serious errors. The caller can do nothing to recover
* from this error. A bugreport should be created and sent to the Android community if this
* error is ever returned.
*/
public static final int ERROR_INTERNAL_ERROR = 1;
/**
* The operation failed because concurrent operations are overriding this one. Retrying an
* aborted operation has the risk of aborting another ongoing operation again. So the caller
* should retry at a higher level where it knows there won't be race conditions.
*/
public static final int ERROR_ABORTED = 2;
/**
* The operation failed because a deadline expired before the operation could complete. This may
* be caused by connectivity unavailability and the caller can retry the same operation when the
* connectivity issue is fixed.
*/
public static final int ERROR_TIMEOUT = 3;
/**
* The operation failed because the service is currently unavailable and that this is most
* likely a transient condition. The caller can recover from this error by retrying with a
* back-off scheme. Note that it is not always safe to retry non-idempotent operations.
*/
public static final int ERROR_UNAVAILABLE = 4;
/**
* The operation failed because this device is currently busy processing concurrent requests.
* The caller may recover from this error when the current operations has been finished.
*/
public static final int ERROR_BUSY = 5;
/**
* The operation failed because required preconditions were not satisfied. For example, trying
* to schedule a network migration when this device is not attached will receive this error. The
* caller should not retry the same operation before the precondition is satisfied.
*/
public static final int ERROR_FAILED_PRECONDITION = 6;
/**
* The operation was rejected because the specified channel is currently not supported by this
* device in this country. For example, trying to join or migrate to a network with channel
* which is not supported. The caller should should change the channel or return an error to the
* user if the channel cannot be changed.
*/
public static final int ERROR_UNSUPPORTED_CHANNEL = 7;
/**
* The operation failed because a request is rejected by the peer device. This happens because
* the peer device is not capable of processing the request, or a request from another device
* has already been accepted by the peer device. The caller may not be able to recover from this
* error by retrying the same operation.
*/
public static final int ERROR_REJECTED_BY_PEER = 8;
/**
* The operation failed because the received response is malformed. This is typically because
* the peer device is misbehaving. The caller may only recover from this error by retrying with
* a different peer device.
*/
public static final int ERROR_RESPONSE_BAD_FORMAT = 9;
/**
* The operation failed because some resource has been exhausted. For example, no enough
* allocated memory buffers, or maximum number of supported operations has been exceeded. The
* caller may retry and recover from this error when the resource has been freed.
*/
public static final int ERROR_RESOURCE_EXHAUSTED = 10;
private final int mErrorCode;
/** Creates a new {@link ThreadNetworkException} object with given error code and message. */
public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
super(requireNonNull(errorMessage, "errorMessage cannot be null"));
this.mErrorCode = errorCode;
}
/** Returns the error code. */
public @ErrorCode int getErrorCode() {
return mErrorCode;
}
}

View file

@ -66,6 +66,19 @@ public final class ThreadNetworkManager {
*/
public static final String FEATURE_NAME = "android.hardware.thread_network";
/**
* Permission allows changing Thread network state and access to Thread network credentials such
* as Network Key and PSKc.
*
* <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_PRIVILEGED. That
* symbol is not available on U while this feature needs to support Android U TV devices, so
* here is making a copy of android.Manifest.permission.THREAD_NETWORK_PRIVILEGED.
*
* @hide
*/
public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;

View file

@ -33,11 +33,35 @@ java_library {
min_sdk_version: "30",
srcs: [":service-thread-sources"],
libs: [
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"service-connectivity-pre-jarjar",
],
static_libs: [
"net-utils-device-common",
"net-utils-device-common-netlink",
"ot-daemon-aidl-java",
],
apex_available: ["com.android.tethering"],
}
cc_library_shared {
name: "libservice-thread-jni",
min_sdk_version: "30",
cflags: [
"-Wall",
"-Werror",
"-Wno-unused-parameter",
"-Wthread-safety",
],
srcs: [
"jni/**/*.cpp",
],
shared_libs: [
"libbase",
"libcutils",
"liblog",
"libnativehelper",
],
apex_available: ["com.android.tethering"],
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.thread;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import android.net.thread.IOperationReceiver;
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
import java.util.HashSet;
import java.util.Set;
/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
final class OperationReceiverWrapper {
private final IOperationReceiver mReceiver;
private static final Object sPendingReceiversLock = new Object();
@GuardedBy("sPendingReceiversLock")
private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
public OperationReceiverWrapper(IOperationReceiver receiver) {
this.mReceiver = receiver;
synchronized (sPendingReceiversLock) {
sPendingReceivers.add(this);
}
}
public static void onOtDaemonDied() {
synchronized (sPendingReceiversLock) {
for (OperationReceiverWrapper receiver : sPendingReceivers) {
try {
receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
} catch (RemoteException e) {
// The client is dead, do nothing
}
}
sPendingReceivers.clear();
}
}
public void onSuccess() {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);
}
try {
mReceiver.onSuccess();
} catch (RemoteException e) {
// The client is dead, do nothing
}
}
public void onError(int errorCode, String errorMessage, Object... messageArgs) {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);
}
try {
mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
} catch (RemoteException e) {
// The client is dead, do nothing
}
}
}

View file

@ -1,31 +1,729 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.server.thread;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
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;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
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_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
import static android.net.thread.ThreadNetworkException.ErrorCode;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
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_UNSUPPORTED_CHANNEL;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import android.Manifest.permission;
import android.annotation.NonNull;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkProvider;
import android.net.NetworkScore;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IStateCallback;
import android.net.thread.IThreadNetworkController;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
/** Implementation of the {@link ThreadNetworkController} API. */
public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.OtDaemonState;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Implementation of the {@link ThreadNetworkController} API.
*
* <p>Threading model: This class is not Thread-safe and should only be accessed from the
* ThreadNetworkService class. Additional attention should be paid to handle the threading code
* correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
* `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
* HandlerThread except for arguments or permissions checking
*/
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
private static final String TAG = "ThreadNetworkService";
// Below member fields can be accessed from both the binder and handler threads
private final Context mContext;
private final Handler mHandler;
// Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
private final HandlerThread mHandlerThread;
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
private final LinkProperties mLinkProperties = new LinkProperties();
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
private IOtDaemon mOtDaemon;
private NetworkAgent mNetworkAgent;
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
HandlerThread handlerThread,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController) {
mContext = context;
mHandlerThread = handlerThread;
mHandler = new Handler(handlerThread.getLooper());
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
}
public static ThreadNetworkControllerService newInstance(Context context) {
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
return new ThreadNetworkControllerService(
context,
handlerThread,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
new TunInterfaceController(TUN_IF_NAME));
}
private static NetworkCapabilities newNetworkCapabilities() {
return new NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.build();
}
private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
try {
return InetAddress.getByAddress(addressInfo.address);
} catch (UnknownHostException e) {
// This is impossible unless the Thread daemon is critically broken
return null;
}
}
private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
long deprecationTimeMillis =
addressInfo.isPreferred
? LinkAddress.LIFETIME_PERMANENT
: SystemClock.elapsedRealtime();
InetAddress address = addressInfoToInetAddress(addressInfo);
// flags and scope will be adjusted automatically depending on the address and
// its lifetimes.
return new LinkAddress(
address,
addressInfo.prefixLength,
0 /* flags */,
0 /* scope */,
deprecationTimeMillis,
LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
private void initializeOtDaemon() {
try {
getOtDaemon();
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize ot-daemon");
}
}
private IOtDaemon getOtDaemon() throws RemoteException {
if (mOtDaemon != null) {
return mOtDaemon;
}
IOtDaemon otDaemon = mOtDaemonSupplier.get();
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
otDaemon.initialize(mTunIfController.getTunFd());
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
mOtDaemon = otDaemon;
return mOtDaemon;
}
private void onOtDaemonDied() {
Log.w(TAG, "OT daemon became dead, clean up...");
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mOtDaemon = null;
}
public void initialize() {
mHandler.post(
() -> {
Log.d(TAG, "Initializing Thread system service...");
try {
mTunIfController.createTunInterface();
} catch (IOException e) {
throw new IllegalStateException(
"Failed to create Thread tunnel interface", e);
}
mLinkProperties.setInterfaceName(TUN_IF_NAME);
mLinkProperties.setMtu(TunInterfaceController.MTU);
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
initializeOtDaemon();
});
}
private void registerThreadNetwork() {
if (mNetworkAgent != null) {
return;
}
NetworkCapabilities netCaps = newNetworkCapabilities();
NetworkScore score =
new NetworkScore.Builder()
.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
.build();
mNetworkAgent =
new NetworkAgent(
mContext,
mHandlerThread.getLooper(),
TAG,
netCaps,
mLinkProperties,
score,
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {};
mNetworkAgent.register();
mNetworkAgent.markConnected();
Log.i(TAG, "Registered Thread network");
}
private void unregisterThreadNetwork() {
if (mNetworkAgent == null) {
// unregisterThreadNetwork can be called every time this device becomes detached or
// disabled and the mNetworkAgent may not be created in this cases
return;
}
Log.d(TAG, "Unregistering Thread network agent");
mNetworkAgent.unregister();
mNetworkAgent = null;
}
private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
try {
if (isAdded) {
mTunIfController.addAddress(linkAddress);
} else {
mTunIfController.removeAddress(linkAddress);
}
} catch (IOException e) {
Log.e(
TAG,
String.format(
"Failed to %s Thread tun interface address %s",
(isAdded ? "add" : "remove"), linkAddress),
e);
}
}
private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
if (isAdded) {
mLinkProperties.addLinkAddress(linkAddress);
} else {
mLinkProperties.removeLinkAddress(linkAddress);
}
// The Thread daemon can send link property updates before the networkAgent is
// registered
if (mNetworkAgent != null) {
mNetworkAgent.sendLinkProperties(mLinkProperties);
}
}
@Override
public int getThreadVersion() {
return THREAD_VERSION_1_3;
}
private void enforceAllCallingPermissionsGranted(String... permissions) {
for (String permission : permissions) {
mContext.enforceCallingPermission(
permission, "Permission " + permission + " is missing");
}
}
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@Override
public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
enforceAllCallingPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
}
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
private void checkOnHandlerThread() {
if (Looper.myLooper() != mHandlerThread.getLooper()) {
Log.wtf(TAG, "Must be on the handler thread!");
}
}
private IOtStatusReceiver newOtStatusReceiver(OperationReceiverWrapper receiver) {
return new IOtStatusReceiver.Stub() {
@Override
public void onSuccess() {
receiver.onSuccess();
}
@Override
public void onError(int otError, String message) {
receiver.onError(otErrorToAndroidError(otError), message);
}
};
}
@ErrorCode
private static int otErrorToAndroidError(int otError) {
// See external/openthread/include/openthread/error.h for OT error definition
switch (otError) {
case OT_ERROR_ABORT:
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
case OT_ERROR_DETACHED:
case OT_ERROR_INVALID_STATE:
return ERROR_FAILED_PRECONDITION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
return ERROR_RESPONSE_BAD_FORMAT;
case OT_ERROR_REASSEMBLY_TIMEOUT:
case OT_ERROR_RESPONSE_TIMEOUT:
return ERROR_TIMEOUT;
case OT_ERROR_REJECTED:
return ERROR_REJECTED_BY_PEER;
case OT_ERROR_UNSUPPORTED_CHANNEL:
return ERROR_UNSUPPORTED_CHANNEL;
default:
return ERROR_INTERNAL_ERROR;
}
}
@Override
public void join(
@NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
}
private void joinInternal(
@NonNull ActiveOperationalDataset activeDataset,
@NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
try {
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException e) {
Log.e(TAG, "otDaemon.join failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
@Override
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull IOperationReceiver receiver) {
enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
}
public void scheduleMigrationInternal(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
try {
getOtDaemon()
.scheduleMigration(
pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException e) {
Log.e(TAG, "otDaemon.scheduleMigration failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
@Override
public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
}
private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
try {
getOtDaemon().leave(newOtStatusReceiver(receiver));
} catch (RemoteException e) {
// Oneway AIDL API should never throw?
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
} catch (IOException e) {
Log.e(TAG, "Failed to handle Thread interface state changes", e);
}
}
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
Log.d(TAG, "Attached to the Thread network");
registerThreadNetwork();
} else {
Log.d(TAG, "Detached from the Thread network");
unregisterThreadNetwork();
}
}
private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
checkOnHandlerThread();
InetAddress address = addressInfoToInetAddress(addressInfo);
if (address.isMulticastAddress()) {
Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
return;
}
LinkAddress linkAddress = newLinkAddress(addressInfo);
Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
updateTunInterfaceAddress(linkAddress, isAdded);
updateNetworkLinkProperties(linkAddress, isAdded);
}
private static final class CallbackMetadata {
private static long gId = 0;
// The unique ID
final long id;
final IBinder.DeathRecipient deathRecipient;
CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
this.id = allocId();
this.deathRecipient = deathRecipient;
}
private static long allocId() {
if (gId == Long.MAX_VALUE) {
gId = 0;
}
return gId++;
}
}
/**
* Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
* mHandlerThread}.
*/
private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
private final Map<IOperationalDatasetCallback, CallbackMetadata> mOpDatasetCallbacks =
new HashMap<>();
private OtDaemonState mState;
private ActiveOperationalDataset mActiveDataset;
private PendingOperationalDataset mPendingDataset;
public void registerStateCallback(IStateCallback callback) {
checkOnHandlerThread();
if (mStateCallbacks.containsKey(callback)) {
return;
}
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterStateCallback(callback));
CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
mStateCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
mStateCallbacks.remove(callback);
// This is thrown when the client is dead, do nothing
}
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException e) {
// oneway operation should never fail
}
}
public void unregisterStateCallback(IStateCallback callback) {
checkOnHandlerThread();
if (!mStateCallbacks.containsKey(callback)) {
return;
}
callback.asBinder().unlinkToDeath(mStateCallbacks.remove(callback).deathRecipient, 0);
}
public void registerDatasetCallback(IOperationalDatasetCallback callback) {
checkOnHandlerThread();
if (mOpDatasetCallbacks.containsKey(callback)) {
return;
}
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterDatasetCallback(callback));
CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
mOpDatasetCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
mOpDatasetCallbacks.remove(callback);
}
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException e) {
// oneway operation should never fail
}
}
public void unregisterDatasetCallback(IOperationalDatasetCallback callback) {
checkOnHandlerThread();
if (!mOpDatasetCallbacks.containsKey(callback)) {
return;
}
callback.asBinder()
.unlinkToDeath(mOpDatasetCallbacks.remove(callback).deathRecipient, 0);
}
public void onOtDaemonDied() {
checkOnHandlerThread();
if (mState == null) {
return;
}
// If this device is already STOPPED or DETACHED, do nothing
if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
return;
}
// The Thread device role is considered DETACHED when the OT daemon process is dead
handleDeviceRoleChanged(DEVICE_ROLE_DETACHED);
for (IStateCallback callback : mStateCallbacks.keySet()) {
try {
callback.onDeviceRoleChanged(DEVICE_ROLE_DETACHED);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
}
@Override
public void onStateChanged(OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
checkOnHandlerThread();
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
mState = newState;
ActiveOperationalDataset newActiveDataset;
try {
if (newState.activeDatasetTlvs.length != 0) {
newActiveDataset =
ActiveOperationalDataset.fromThreadTlvs(newState.activeDatasetTlvs);
} else {
newActiveDataset = null;
}
onActiveOperationalDatasetChanged(newActiveDataset, listenerId);
mActiveDataset = newActiveDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
Log.w(TAG, "Ignoring invalid Active Operational Dataset changes", e);
}
PendingOperationalDataset newPendingDataset;
try {
if (newState.pendingDatasetTlvs.length != 0) {
newPendingDataset =
PendingOperationalDataset.fromThreadTlvs(newState.pendingDatasetTlvs);
} else {
newPendingDataset = null;
}
onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
mPendingDataset = newPendingDataset;
} catch (IllegalArgumentException e) {
Log.w(TAG, "Ignoring invalid Pending Operational Dataset changes", e);
}
}
private void onInterfaceStateChanged(boolean isUp) {
checkOnHandlerThread();
if (mState == null || mState.isInterfaceUp != isUp) {
handleThreadInterfaceStateChanged(isUp);
}
}
private void onDeviceRoleChanged(@DeviceRole int deviceRole, long listenerId) {
checkOnHandlerThread();
boolean hasChange = (mState == null || mState.deviceRole != deviceRole);
if (hasChange) {
handleDeviceRoleChanged(deviceRole);
}
for (var callbackEntry : mStateCallbacks.entrySet()) {
if (!hasChange && callbackEntry.getValue().id != listenerId) {
continue;
}
try {
callbackEntry.getKey().onDeviceRoleChanged(deviceRole);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
}
private void onPartitionIdChanged(long partitionId, long listenerId) {
checkOnHandlerThread();
boolean hasChange = (mState == null || mState.partitionId != partitionId);
for (var callbackEntry : mStateCallbacks.entrySet()) {
if (!hasChange && callbackEntry.getValue().id != listenerId) {
continue;
}
try {
callbackEntry.getKey().onPartitionIdChanged(partitionId);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
}
private void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset, long listenerId) {
checkOnHandlerThread();
boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
if (!hasChange && callbackEntry.getValue().id != listenerId) {
continue;
}
try {
callbackEntry.getKey().onActiveOperationalDatasetChanged(activeDataset);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
}
private void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingDataset, long listenerId) {
checkOnHandlerThread();
boolean hasChange = !Objects.equals(mPendingDataset, pendingDataset);
for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
if (!hasChange && callbackEntry.getValue().id != listenerId) {
continue;
}
try {
callbackEntry.getKey().onPendingOperationalDatasetChanged(pendingDataset);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
}
}
@Override
public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
}
}
}

View file

@ -16,6 +16,7 @@
package com.android.server.thread;
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
@ -29,16 +30,12 @@ import java.util.List;
* Implementation of the Thread network service. This is the entry point of Android Thread feature.
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
private final ThreadNetworkControllerService mControllerService;
private final Context mContext;
@Nullable private ThreadNetworkControllerService mControllerService;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
this(context, new ThreadNetworkControllerService());
}
private ThreadNetworkService(
Context context, ThreadNetworkControllerService controllerService) {
mControllerService = controllerService;
mContext = context;
}
/**
@ -48,12 +45,16 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
*/
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_BOOT_COMPLETED) {
// TODO: initialize ThreadNetworkManagerService
mControllerService = ThreadNetworkControllerService.newInstance(mContext);
mControllerService.initialize();
}
}
@Override
public List<IThreadNetworkController> getAllThreadNetworkControllers() {
if (mControllerService == null) {
return Collections.emptyList();
}
return Collections.singletonList(mControllerService);
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.thread;
import android.net.LinkAddress;
import android.net.util.SocketUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
import java.io.FileDescriptor;
import java.io.IOException;
/** Controller for virtual/tunnel network interfaces. */
public class TunInterfaceController {
private static final String TAG = "TunIfController";
static final int MTU = 1280;
static {
System.loadLibrary("service-thread-jni");
}
private final String mIfName;
private ParcelFileDescriptor mParcelTunFd;
private FileDescriptor mNetlinkSocket;
private static int sNetlinkSeqNo = 0;
/** Creates a new {@link TunInterfaceController} instance for given interface. */
public TunInterfaceController(String interfaceName) {
this.mIfName = interfaceName;
}
/**
* Creates the tunnel interface.
*
* @throws IOException if failed to create the interface
*/
public void createTunInterface() throws IOException {
mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
try {
mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
} catch (ErrnoException e) {
throw new IOException("Failed to create netlink socket", e);
}
}
public void destroyTunInterface() {
try {
mParcelTunFd.close();
SocketUtils.closeSocket(mNetlinkSocket);
} catch (IOException e) {
// Should never fail
}
mParcelTunFd = null;
mNetlinkSocket = null;
}
/** Returns the FD of the tunnel interface. */
public ParcelFileDescriptor getTunFd() {
return mParcelTunFd;
}
private native int nativeCreateTunInterface(String interfaceName, int mtu) throws IOException;
/** Sets the interface up or down according to {@code isUp}. */
public void setInterfaceUp(boolean isUp) throws IOException {
nativeSetInterfaceUp(mIfName, isUp);
}
private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
/** Adds a new address to the interface. */
public void addAddress(LinkAddress address) throws IOException {
Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
long validLifetimeSeconds;
long preferredLifetimeSeconds;
if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
|| address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
validLifetimeSeconds = 0xffffffffL;
} else {
validLifetimeSeconds =
Math.max(
(address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
0L);
}
if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
|| address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
preferredLifetimeSeconds = 0xffffffffL;
} else {
preferredLifetimeSeconds =
Math.max(
(address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
0L);
}
byte[] message =
RtNetlinkAddressMessage.newRtmNewAddressMessage(
sNetlinkSeqNo,
address.getAddress(),
(short) address.getPrefixLength(),
address.getFlags(),
(byte) address.getScope(),
Os.if_nametoindex(mIfName),
validLifetimeSeconds,
preferredLifetimeSeconds);
try {
Os.write(mNetlinkSocket, message, 0, message.length);
} catch (ErrnoException e) {
throw new IOException("Failed to send netlink message", e);
}
}
/** Removes an address from the interface. */
public void removeAddress(LinkAddress address) throws IOException {
// TODO(b/263222068): remove address with netlink
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "jniThreadTun"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/if_arp.h>
#include <linux/if_tun.h>
#include <linux/ioctl.h>
#include <log/log.h>
#include <net/if.h>
#include <spawn.h>
#include <sys/wait.h>
#include <string>
#include <private/android_filesystem_config.h>
#include "jni.h"
#include "nativehelper/JNIHelp.h"
#include "nativehelper/scoped_utf_chars.h"
namespace android {
static jint com_android_server_thread_TunInterfaceController_createTunInterface(
JNIEnv* env, jobject clazz, jstring interfaceName, jint mtu) {
ScopedUtfChars ifName(env, interfaceName);
int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
if (fd == -1) {
jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
strerror(errno));
return -1;
}
struct ifreq ifr = {
.ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL),
};
strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
close(fd);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
strerror(errno));
return -1;
}
int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
if (inet6 == -1) {
close(fd);
jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
strerror(errno));
return -1;
}
ifr.ifr_mtu = mtu;
if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
close(fd);
close(inet6);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
strerror(errno));
return -1;
}
close(inet6);
return fd;
}
static void com_android_server_thread_TunInterfaceController_setInterfaceUp(
JNIEnv* env, jobject clazz, jstring interfaceName, jboolean isUp) {
struct ifreq ifr;
ScopedUtfChars ifName(env, interfaceName);
ifr.ifr_flags = isUp ? IFF_UP : 0;
strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
if (inet6 == -1) {
jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
strerror(errno));
}
if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
close(inet6);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
strerror(errno));
}
close(inet6);
}
/*
* JNI registration.
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{"nativeCreateTunInterface",
"(Ljava/lang/String;I)I",
(void*)com_android_server_thread_TunInterfaceController_createTunInterface},
{"nativeSetInterfaceUp",
"(Ljava/lang/String;Z)V",
(void*)com_android_server_thread_TunInterfaceController_setInterfaceUp},
};
int register_com_android_server_thread_TunInterfaceController(JNIEnv* env) {
return jniRegisterNativeMethods(env, "com/android/server/thread/TunInterfaceController",
gMethods, NELEM(gMethods));
}
}; // namespace android

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "jni.h"
#include "utils/Log.h"
namespace android {
int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
}
using namespace android;
extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
JNIEnv* env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("GetEnv failed!");
return -1;
}
ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
register_com_android_server_thread_TunInterfaceController(env);
return JNI_VERSION_1_4;
}

View file

@ -16,58 +16,576 @@
package android.net.thread.cts;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
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.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.os.Build;
import android.os.OutcomeReceiver;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.filters.LargeTest;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** CTS tests for {@link ThreadNetworkController}. */
@SmallTest
@LargeTest
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkManager mManager;
private Set<String> mGrantedPermissions;
@Before
public void setUp() {
mExecutor = Executors.newSingleThreadExecutor();
mManager = mContext.getSystemService(ThreadNetworkManager.class);
mGrantedPermissions = new HashSet<String>();
// 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);
}
@After
public void tearDown() throws Exception {
if (mManager != null) {
leaveAndWait();
dropPermissions();
}
}
private List<ThreadNetworkController> getAllControllers() {
return mManager.getAllThreadNetworkControllers();
}
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();
}
}
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
}
String[] allPermissions = new String[mGrantedPermissions.size()];
mGrantedPermissions.toArray(allPermissions);
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
private static void dropPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
private static boolean isAttached(ThreadNetworkController controller) throws Exception {
return ThreadNetworkController.isAttached(getDeviceRole(controller));
}
private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
SettableFuture<Integer> future = SettableFuture.create();
StateCallback callback = future::set;
controller.registerStateCallback(directExecutor(), callback);
int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterStateCallback(callback);
return role;
}
private static int waitForStateAnyOf(
ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
SettableFuture<Integer> future = SettableFuture.create();
StateCallback callback =
newRole -> {
if (deviceRoles.contains(newRole)) {
future.set(newRole);
}
};
controller.registerStateCallback(directExecutor(), callback);
int role = future.get();
controller.unregisterStateCallback(callback);
return role;
}
private static ActiveOperationalDataset getActiveOperationalDataset(
ThreadNetworkController controller) throws Exception {
SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
OperationalDatasetCallback callback = future::set;
controller.registerOperationalDatasetCallback(directExecutor(), callback);
ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterOperationalDatasetCallback(callback);
return dataset;
}
private static PendingOperationalDataset getPendingOperationalDataset(
ThreadNetworkController controller) throws Exception {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
controller.registerOperationalDatasetCallback(
directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
return pendingFuture.get();
}
private static OperationalDatasetCallback newDatasetCallback(
SettableFuture<ActiveOperationalDataset> activeFuture,
SettableFuture<PendingOperationalDataset> pendingFuture) {
return new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeOpDataset) {
activeFuture.set(activeOpDataset);
}
@Override
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingOpDataset) {
pendingFuture.set(pendingOpDataset);
}
};
}
@Test
public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
for (ThreadNetworkController controller : getAllControllers()) {
assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
}
}
@Test
public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
controller.registerStateCallback(mExecutor, role -> deviceRole.set(role));
assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
}
}
@Test
public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
dropPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
assertThrows(
SecurityException.class,
() -> controller.registerStateCallback(mExecutor, role -> {}));
}
}
@Test
public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
controller.registerStateCallback(mExecutor, callback);
assertThrows(
IllegalArgumentException.class,
() -> controller.registerStateCallback(mExecutor, callback));
}
}
@Test
public void unregisterStateCallback_callbackRegistered_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
controller.registerStateCallback(mExecutor, callback);
controller.unregisterStateCallback(callback);
}
}
@Test
public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
throws Exception {
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
assertThrows(
IllegalArgumentException.class,
() -> controller.unregisterStateCallback(callback));
}
}
@Test
public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
StateCallback callback = role -> deviceRole.set(role);
controller.registerStateCallback(mExecutor, callback);
controller.unregisterStateCallback(callback);
assertThrows(
IllegalArgumentException.class,
() -> controller.unregisterStateCallback(callback));
}
}
@Test
public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
controller.registerOperationalDatasetCallback(
mExecutor, newDatasetCallback(activeFuture, pendingFuture));
assertThat(activeFuture.get()).isNull();
assertThat(pendingFuture.get()).isNull();
}
}
private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
SettableFuture<V> future) {
return new OutcomeReceiver<V, ThreadNetworkException>() {
@Override
public void onResult(V result) {
future.set(result);
}
@Override
public void onError(ThreadNetworkException e) {
future.setException(e);
}
};
}
@Test
public void join_withPrivilegedPermission_success() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
SettableFuture<Void> joinFuture = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
assertThat(isAttached(controller)).isTrue();
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
}
}
@Test
public void join_withoutPrivilegedPermission_throwsSecurityException() {
dropPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
assertThrows(
SecurityException.class,
() -> controller.join(activeDataset, mExecutor, v -> {}));
}
}
@Test
public void join_concurrentRequests_firstOneIsAborted() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset1 =
new ActiveOperationalDataset.Builder(
ActiveOperationalDataset.createRandomDataset())
.setNetworkKey(KEY_1)
.build();
ActiveOperationalDataset activeDataset2 =
new ActiveOperationalDataset.Builder(activeDataset1)
.setNetworkKey(KEY_2)
.build();
SettableFuture<Void> joinFuture1 = SettableFuture.create();
SettableFuture<Void> joinFuture2 = SettableFuture.create();
controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
ThreadNetworkException thrown =
(ThreadNetworkException)
assertThrows(ExecutionException.class, joinFuture1::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
joinFuture2.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
assertThat(isAttached(controller)).isTrue();
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
}
}
@Test
public void leave_withPrivilegedPermission_success() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> leaveFuture = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
leaveFuture.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
}
}
@Test
public void leave_withoutPrivilegedPermission_throwsSecurityException() {
dropPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
}
}
@Test
public void leave_concurrentRequests_bothSuccess() throws Exception {
grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> leaveFuture1 = SettableFuture.create();
SettableFuture<Void> leaveFuture2 = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
leaveFuture1.get();
leaveFuture2.get();
grantPermissions(permission.ACCESS_NETWORK_STATE);
assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
}
}
@Test
public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset1 =
new ActiveOperationalDataset.Builder(
ActiveOperationalDataset.createRandomDataset())
.setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
.setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
.build();
ActiveOperationalDataset activeDataset2 =
new ActiveOperationalDataset.Builder(activeDataset1)
.setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
.setNetworkName("ThreadNet2")
.build();
PendingOperationalDataset pendingDataset2 =
new PendingOperationalDataset(
activeDataset2,
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture = SettableFuture.create();
controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
migrateFuture.get();
Thread.sleep(35 * 1000);
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
assertThat(getPendingOperationalDataset(controller)).isNull();
}
}
@Test
public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
PendingOperationalDataset pendingDataset =
new PendingOperationalDataset(
ActiveOperationalDataset.createRandomDataset(),
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
SettableFuture<Void> migrateFuture = SettableFuture.create();
controller.scheduleMigration(
pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
ThreadNetworkException thrown =
(ThreadNetworkException)
assertThrows(ExecutionException.class, migrateFuture::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
}
@Test
public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
final ActiveOperationalDataset activeDataset =
new ActiveOperationalDataset.Builder(
ActiveOperationalDataset.createRandomDataset())
.setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
.setNetworkName("testNet")
.build();
ActiveOperationalDataset activeDataset1 =
new ActiveOperationalDataset.Builder(activeDataset)
.setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
.setNetworkName("testNet1")
.build();
PendingOperationalDataset pendingDataset1 =
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(100, 0, false),
Duration.ofSeconds(30));
ActiveOperationalDataset activeDataset2 =
new ActiveOperationalDataset.Builder(activeDataset)
.setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
.setNetworkName("testNet2")
.build();
PendingOperationalDataset pendingDataset2 =
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(20, 0, false),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture1 = SettableFuture.create();
SettableFuture<Void> migrateFuture2 = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
controller.scheduleMigration(
pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
migrateFuture1.get();
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
ThreadNetworkException thrown =
(ThreadNetworkException)
assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
}
}
@Test
public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
final ActiveOperationalDataset activeDataset =
new ActiveOperationalDataset.Builder(
ActiveOperationalDataset.createRandomDataset())
.setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
.setNetworkName("testNet")
.build();
ActiveOperationalDataset activeDataset1 =
new ActiveOperationalDataset.Builder(activeDataset)
.setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
.setNetworkName("testNet1")
.build();
PendingOperationalDataset pendingDataset1 =
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(100, 0, false),
Duration.ofSeconds(30));
ActiveOperationalDataset activeDataset2 =
new ActiveOperationalDataset.Builder(activeDataset)
.setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
.setNetworkName("testNet2")
.build();
PendingOperationalDataset pendingDataset2 =
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(200, 0, false),
Duration.ofSeconds(30));
SettableFuture<Void> joinFuture = SettableFuture.create();
SettableFuture<Void> migrateFuture1 = SettableFuture.create();
SettableFuture<Void> migrateFuture2 = SettableFuture.create();
controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
joinFuture.get();
controller.scheduleMigration(
pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
migrateFuture1.get();
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
migrateFuture2.get();
Thread.sleep(35 * 1000);
assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
assertThat(getPendingOperationalDataset(controller)).isNull();
}
}
}