/* * 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 mStateCallbackMap = new HashMap<>(); private final Object mOpDatasetCallbackMapLock = new Object(); @GuardedBy("mOpDatasetCallbackMapLock") private final Map mOpDatasetCallbackMap = new HashMap<>(); /** @hide */ public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) { requireNonNull(controllerService, "controllerService cannot be null"); mControllerService = controllerService; } /** * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will * be persistent and survives device reboots. * *

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 receiver) { try { mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Returns the Thread version this device is operating on. */ @ThreadVersion public int getThreadVersion() { try { return mControllerService.getThreadVersion(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Creates a new Active Operational Dataset with randomized parameters. * *

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 receiver) { ActiveOperationalDataset.checkNetworkName(networkName); requireNonNull(executor, "executor cannot be null"); requireNonNull(receiver, "receiver cannot be null"); try { mControllerService.createRandomizedDataset( networkName, new ActiveDatasetReceiverProxy(executor, receiver)); } catch (RemoteException e) { 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. * *

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: * *

* * @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 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. * *

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 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. * *

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 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(); } } /** * Sets to use a specified test network as the upstream. * * @param testNetworkInterfaceName The name of the test network interface. When it's null, * forbids using test network as an upstream. * @param executor the executor to execute {@code receiver} * @param receiver the receiver to receive result of this operation * @hide */ @VisibleForTesting @RequiresPermission( allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS}) public void setTestNetworkAsUpstream( @Nullable String testNetworkInterfaceName, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { requireNonNull(executor, "executor cannot be null"); requireNonNull(receiver, "receiver cannot be null"); try { mControllerService.setTestNetworkAsUpstream( testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Sets max power of each channel. * *

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 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 receiver) { requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null"); requireNonNull(executor, "executor cannot be null"); requireNonNull(receiver, "receiver cannot be null"); if (channelMaxPowers.size() < 1) { throw new IllegalArgumentException("channelMaxPowers cannot be empty"); } for (int i = 0; i < channelMaxPowers.size(); i++) { int channel = channelMaxPowers.keyAt(i); int maxPower = channelMaxPowers.get(channel); if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ) || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) { throw new IllegalArgumentException( "Channel " + channel + " exceeds allowed range [" + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ + ", " + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ + "]"); } if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) { throw new IllegalArgumentException( "Channel power ({channel: " + channel + ", maxPower: " + maxPower + "}) exceeds allowed range [" + POWER_LIMITATION_MIN + ", " + POWER_LIMITATION_MAX + "]"); } } try { mControllerService.setChannelMaxPowers( toChannelMaxPowerArray(channelMaxPowers), new OperationReceiverProxy(executor, receiver)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private static ChannelMaxPower[] toChannelMaxPowerArray( @NonNull SparseIntArray channelMaxPowers) { final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()]; for (int i = 0; i < channelMaxPowers.size(); i++) { powerArray[i] = new ChannelMaxPower(); powerArray[i].channel = channelMaxPowers.keyAt(i); powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel); } return powerArray; } private static void propagateError( Executor executor, OutcomeReceiver receiver, int errorCode, String errorMsg) { final long identity = Binder.clearCallingIdentity(); try { executor.execute( () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg))); } finally { Binder.restoreCallingIdentity(identity); } } private static final class ActiveDatasetReceiverProxy extends IActiveOperationalDatasetReceiver.Stub { final Executor mExecutor; final OutcomeReceiver mResultReceiver; ActiveDatasetReceiverProxy( @CallbackExecutor Executor executor, OutcomeReceiver resultReceiver) { this.mExecutor = executor; this.mResultReceiver = resultReceiver; } @Override public void onSuccess(ActiveOperationalDataset dataset) { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mResultReceiver.onResult(dataset)); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onError(int errorCode, String errorMessage) { propagateError(mExecutor, mResultReceiver, errorCode, errorMessage); } } private static final class OperationReceiverProxy extends IOperationReceiver.Stub { final Executor mExecutor; final OutcomeReceiver mResultReceiver; OperationReceiverProxy( @CallbackExecutor Executor executor, OutcomeReceiver resultReceiver) { this.mExecutor = executor; this.mResultReceiver = resultReceiver; } @Override public void onSuccess() { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mResultReceiver.onResult(null)); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onError(int errorCode, String errorMessage) { propagateError(mExecutor, mResultReceiver, errorCode, errorMessage); } } }