[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:
parent
192e4b0289
commit
b1c75dbe8c
19 changed files with 2388 additions and 23 deletions
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
25
thread/framework/java/android/net/thread/IStateCallback.aidl
Normal file
25
thread/framework/java/android/net/thread/IStateCallback.aidl
Normal 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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
37
thread/service/jni/onload.cpp
Normal file
37
thread/service/jni/onload.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue