/* * 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.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.Size; import android.annotation.SystemApi; import android.os.Binder; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; 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 APIs for controlling all aspects of a Thread network. * *
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;
/** The Thread radio is disabled. */
public static final int STATE_DISABLED = 0;
/** The Thread radio is enabled. */
public static final int STATE_ENABLED = 1;
/** The Thread radio is being disabled. */
public static final int STATE_DISABLING = 2;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DEVICE_ROLE_STOPPED,
DEVICE_ROLE_DETACHED,
DEVICE_ROLE_CHILD,
DEVICE_ROLE_ROUTER,
DEVICE_ROLE_LEADER
})
public @interface DeviceRole {}
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"STATE_"},
value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
public @interface EnabledState {}
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
/** Minimum value of max power in unit of 0.01dBm. @hide */
private static final int POWER_LIMITATION_MIN = -32768;
/** Maximum value of max power in unit of 0.01dBm. @hide */
private static final int POWER_LIMITATION_MAX = 32767;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({THREAD_VERSION_1_3})
public @interface ThreadVersion {}
private final IThreadNetworkController mControllerService;
private final Object mStateCallbackMapLock = new Object();
@GuardedBy("mStateCallbackMapLock")
private final Map When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
* require the Thread radio will fail with error code {@link
* ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
* {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
* with error code {@link ThreadNetworkException#ERROR_BUSY}.
*
* On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
* the operation has completed. But there maybe subsequent calls to update the enabled state,
* callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
* enabled state changes.
*
* On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
* specific error in {@link ThreadNetworkException#ERROR_}.
*
* @param enabled {@code true} for enabling Thread
* @param executor the executor to execute {@code receiver}
* @param receiver the receiver to receive result of this operation
*/
@RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
public void setEnabled(
boolean enabled,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver This method is the recommended way to create a randomized dataset which can be used with
* {@link #join} to securely join this device to the specified network . It's highly discouraged
* to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
* the security of a Thread network.
*
* @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
* isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
* #LENGTH_MAX_NETWORK_NAME_BYTES}]
*/
public void createRandomizedDataset(
@NonNull String networkName,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver 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) {}
/**
* The Thread enabled state has changed.
*
* The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
* airplane mode or admin control.
*
* @param enabledState the new Thread enabled state
*/
default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
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) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onPartitionIdChanged(long partitionId) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onThreadEnableStateChanged(@EnabledState int enabled) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/**
* Registers a callback to be called when Thread network states are changed.
*
* 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
*/
@RequiresPermission(permission.ACCESS_NETWORK_STATE)
public void unregisterStateCallback(@NonNull StateCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mStateCallbackMapLock) {
StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterStateCallback(callbackProxy);
mStateCallbackMap.remove(callback);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}
/**
* Callback to receive notifications when the Thread Operational Datasets are changed.
*
* 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) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onPendingOperationalDatasetChanged(
@Nullable PendingOperationalDataset pendingDataset) {
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(
() -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/**
* Registers a callback to be called when Thread Operational Datasets are changed.
*
* 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
*/
@RequiresPermission(
allOf = {
permission.ACCESS_NETWORK_STATE,
"android.permission.THREAD_NETWORK_PRIVILEGED"
})
public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mOpDatasetCallbackMapLock) {
OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
mOpDatasetCallbackMap.remove(callback);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}
/**
* Joins to a Thread network with given Active Operational Dataset.
*
* 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}.
*
* 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:
*
* 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.
*
* 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:
*
* 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 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 If not set, the default max power is set by the Thread HAL service or the Thread radio
* chip firmware.
*
* On success, the Pending Dataset is successfully registered and persisted on the Leader and
* {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
* OutcomeReceiver#onError} will be called with a specific error:
*
*
*
*
* @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
*
*
*
*
*
* @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
* and corresponding max power. Valid channel values should be between {@link
* ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
* ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
* power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
* is set to INT16_MAX, the corresponding channel is not supported.
* @param executor the executor to execute {@code receiver}.
* @param receiver the receiver to receive the result of this operation.
* @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
* or invalid channel or max power is configured.
* @hide
*/
@RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
public final void setChannelMaxPowers(
@NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver