1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.thread;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.Manifest.permission;
22 import android.annotation.CallbackExecutor;
23 import android.annotation.FlaggedApi;
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.RequiresPermission;
28 import android.annotation.Size;
29 import android.annotation.SystemApi;
30 import android.os.Binder;
31 import android.os.OutcomeReceiver;
32 import android.os.RemoteException;
33 import android.util.SparseIntArray;
34 
35 import com.android.internal.annotations.GuardedBy;
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.time.Duration;
41 import java.util.HashMap;
42 import java.util.Map;
43 import java.util.concurrent.Executor;
44 
45 /**
46  * Provides the primary APIs for controlling all aspects of a Thread network.
47  *
48  * <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
49  * migrate an existing network.
50  *
51  * @hide
52  */
53 @FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
54 @SystemApi
55 public final class ThreadNetworkController {
56     private static final String TAG = "ThreadNetworkController";
57 
58     /** The Thread stack is stopped. */
59     public static final int DEVICE_ROLE_STOPPED = 0;
60 
61     /** The device is not currently participating in a Thread network/partition. */
62     public static final int DEVICE_ROLE_DETACHED = 1;
63 
64     /** The device is a Thread Child. */
65     public static final int DEVICE_ROLE_CHILD = 2;
66 
67     /** The device is a Thread Router. */
68     public static final int DEVICE_ROLE_ROUTER = 3;
69 
70     /** The device is a Thread Leader. */
71     public static final int DEVICE_ROLE_LEADER = 4;
72 
73     /** The Thread radio is disabled. */
74     public static final int STATE_DISABLED = 0;
75 
76     /** The Thread radio is enabled. */
77     public static final int STATE_ENABLED = 1;
78 
79     /** The Thread radio is being disabled. */
80     public static final int STATE_DISABLING = 2;
81 
82     /** @hide */
83     @Retention(RetentionPolicy.SOURCE)
84     @IntDef({
85         DEVICE_ROLE_STOPPED,
86         DEVICE_ROLE_DETACHED,
87         DEVICE_ROLE_CHILD,
88         DEVICE_ROLE_ROUTER,
89         DEVICE_ROLE_LEADER
90     })
91     public @interface DeviceRole {}
92 
93     /** @hide */
94     @Retention(RetentionPolicy.SOURCE)
95     @IntDef(
96             prefix = {"STATE_"},
97             value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
98     public @interface EnabledState {}
99 
100     /** Thread standard version 1.3. */
101     public static final int THREAD_VERSION_1_3 = 4;
102 
103     /** Minimum value of max power in unit of 0.01dBm. @hide */
104     private static final int POWER_LIMITATION_MIN = -32768;
105 
106     /** Maximum value of max power in unit of 0.01dBm. @hide */
107     private static final int POWER_LIMITATION_MAX = 32767;
108 
109     /** @hide */
110     @Retention(RetentionPolicy.SOURCE)
111     @IntDef({THREAD_VERSION_1_3})
112     public @interface ThreadVersion {}
113 
114     private final IThreadNetworkController mControllerService;
115 
116     private final Object mStateCallbackMapLock = new Object();
117 
118     @GuardedBy("mStateCallbackMapLock")
119     private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
120 
121     private final Object mOpDatasetCallbackMapLock = new Object();
122 
123     @GuardedBy("mOpDatasetCallbackMapLock")
124     private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
125             mOpDatasetCallbackMap = new HashMap<>();
126 
127     /** @hide */
ThreadNetworkController(@onNull IThreadNetworkController controllerService)128     public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
129         requireNonNull(controllerService, "controllerService cannot be null");
130         mControllerService = controllerService;
131     }
132 
133     /**
134      * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
135      * be persistent and survives device reboots.
136      *
137      * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
138      * require the Thread radio will fail with error code {@link
139      * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
140      * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
141      * with error code {@link ThreadNetworkException#ERROR_BUSY}.
142      *
143      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
144      * the operation has completed. But there maybe subsequent calls to update the enabled state,
145      * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
146      * enabled state changes.
147      *
148      * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
149      * specific error in {@link ThreadNetworkException#ERROR_}.
150      *
151      * @param enabled {@code true} for enabling Thread
152      * @param executor the executor to execute {@code receiver}
153      * @param receiver the receiver to receive result of this operation
154      */
155     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
setEnabled( boolean enabled, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)156     public void setEnabled(
157             boolean enabled,
158             @NonNull @CallbackExecutor Executor executor,
159             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
160         try {
161             mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
162         } catch (RemoteException e) {
163             throw e.rethrowFromSystemServer();
164         }
165     }
166 
167     /** Returns the Thread version this device is operating on. */
168     @ThreadVersion
getThreadVersion()169     public int getThreadVersion() {
170         try {
171             return mControllerService.getThreadVersion();
172         } catch (RemoteException e) {
173             throw e.rethrowFromSystemServer();
174         }
175     }
176 
177     /**
178      * Creates a new Active Operational Dataset with randomized parameters.
179      *
180      * <p>This method is the recommended way to create a randomized dataset which can be used with
181      * {@link #join} to securely join this device to the specified network . It's highly discouraged
182      * to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
183      * the security of a Thread network.
184      *
185      * @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
186      *     isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
187      *     #LENGTH_MAX_NETWORK_NAME_BYTES}]
188      */
createRandomizedDataset( @onNull String networkName, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver)189     public void createRandomizedDataset(
190             @NonNull String networkName,
191             @NonNull @CallbackExecutor Executor executor,
192             @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver) {
193         ActiveOperationalDataset.checkNetworkName(networkName);
194         requireNonNull(executor, "executor cannot be null");
195         requireNonNull(receiver, "receiver cannot be null");
196         try {
197             mControllerService.createRandomizedDataset(
198                     networkName, new ActiveDatasetReceiverProxy(executor, receiver));
199         } catch (RemoteException e) {
200             e.rethrowFromSystemServer();
201         }
202     }
203 
204     /** Returns {@code true} if {@code deviceRole} indicates an attached state. */
isAttached(@eviceRole int deviceRole)205     public static boolean isAttached(@DeviceRole int deviceRole) {
206         return deviceRole == DEVICE_ROLE_CHILD
207                 || deviceRole == DEVICE_ROLE_ROUTER
208                 || deviceRole == DEVICE_ROLE_LEADER;
209     }
210 
211     /**
212      * Callback to receive notifications when the Thread network states are changed.
213      *
214      * <p>Applications which are interested in monitoring Thread network states should implement
215      * this interface and register the callback with {@link #registerStateCallback}.
216      */
217     public interface StateCallback {
218         /**
219          * The Thread device role has changed.
220          *
221          * @param deviceRole the new Thread device role
222          */
onDeviceRoleChanged(@eviceRole int deviceRole)223         void onDeviceRoleChanged(@DeviceRole int deviceRole);
224 
225         /**
226          * The Thread network partition ID has changed.
227          *
228          * @param partitionId the new Thread partition ID
229          */
onPartitionIdChanged(long partitionId)230         default void onPartitionIdChanged(long partitionId) {}
231 
232         /**
233          * The Thread enabled state has changed.
234          *
235          * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
236          * airplane mode or admin control.
237          *
238          * @param enabledState the new Thread enabled state
239          */
onThreadEnableStateChanged(@nabledState int enabledState)240         default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
241     }
242 
243     private static final class StateCallbackProxy extends IStateCallback.Stub {
244         private final Executor mExecutor;
245         private final StateCallback mCallback;
246 
StateCallbackProxy(@allbackExecutor Executor executor, StateCallback callback)247         StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
248             mExecutor = executor;
249             mCallback = callback;
250         }
251 
252         @Override
onDeviceRoleChanged(@eviceRole int deviceRole)253         public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
254             final long identity = Binder.clearCallingIdentity();
255             try {
256                 mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
257             } finally {
258                 Binder.restoreCallingIdentity(identity);
259             }
260         }
261 
262         @Override
onPartitionIdChanged(long partitionId)263         public void onPartitionIdChanged(long partitionId) {
264             final long identity = Binder.clearCallingIdentity();
265             try {
266                 mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
267             } finally {
268                 Binder.restoreCallingIdentity(identity);
269             }
270         }
271 
272         @Override
onThreadEnableStateChanged(@nabledState int enabled)273         public void onThreadEnableStateChanged(@EnabledState int enabled) {
274             final long identity = Binder.clearCallingIdentity();
275             try {
276                 mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
277             } finally {
278                 Binder.restoreCallingIdentity(identity);
279             }
280         }
281     }
282 
283     /**
284      * Registers a callback to be called when Thread network states are changed.
285      *
286      * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
287      * existing states.
288      *
289      * @param executor the executor to execute the {@code callback}
290      * @param callback the callback to receive Thread network state changes
291      * @throws IllegalArgumentException if {@code callback} has already been registered
292      */
293     @RequiresPermission(permission.ACCESS_NETWORK_STATE)
registerStateCallback( @onNull @allbackExecutor Executor executor, @NonNull StateCallback callback)294     public void registerStateCallback(
295             @NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
296         requireNonNull(executor, "executor cannot be null");
297         requireNonNull(callback, "callback cannot be null");
298         synchronized (mStateCallbackMapLock) {
299             if (mStateCallbackMap.containsKey(callback)) {
300                 throw new IllegalArgumentException("callback has already been registered");
301             }
302             StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
303             mStateCallbackMap.put(callback, callbackProxy);
304 
305             try {
306                 mControllerService.registerStateCallback(callbackProxy);
307             } catch (RemoteException e) {
308                 mStateCallbackMap.remove(callback);
309                 e.rethrowFromSystemServer();
310             }
311         }
312     }
313 
314     /**
315      * Unregisters the Thread state changed callback.
316      *
317      * @param callback the callback which has been registered with {@link #registerStateCallback}
318      * @throws IllegalArgumentException if {@code callback} hasn't been registered
319      */
320     @RequiresPermission(permission.ACCESS_NETWORK_STATE)
unregisterStateCallback(@onNull StateCallback callback)321     public void unregisterStateCallback(@NonNull StateCallback callback) {
322         requireNonNull(callback, "callback cannot be null");
323         synchronized (mStateCallbackMapLock) {
324             StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
325             if (callbackProxy == null) {
326                 throw new IllegalArgumentException("callback hasn't been registered");
327             }
328             try {
329                 mControllerService.unregisterStateCallback(callbackProxy);
330                 mStateCallbackMap.remove(callback);
331             } catch (RemoteException e) {
332                 e.rethrowFromSystemServer();
333             }
334         }
335     }
336 
337     /**
338      * Callback to receive notifications when the Thread Operational Datasets are changed.
339      *
340      * <p>Applications which are interested in monitoring Thread network datasets should implement
341      * this interface and register the callback with {@link #registerOperationalDatasetCallback}.
342      */
343     public interface OperationalDatasetCallback {
344         /**
345          * Called when the Active Operational Dataset is changed.
346          *
347          * @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
348          *     absent
349          */
onActiveOperationalDatasetChanged(@ullable ActiveOperationalDataset activeDataset)350         void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
351 
352         /**
353          * Called when the Pending Operational Dataset is changed.
354          *
355          * @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
356          *     has been committed and removed
357          */
onPendingOperationalDatasetChanged( @ullable PendingOperationalDataset pendingDataset)358         default void onPendingOperationalDatasetChanged(
359                 @Nullable PendingOperationalDataset pendingDataset) {}
360     }
361 
362     private static final class OperationalDatasetCallbackProxy
363             extends IOperationalDatasetCallback.Stub {
364         private final Executor mExecutor;
365         private final OperationalDatasetCallback mCallback;
366 
OperationalDatasetCallbackProxy( @allbackExecutor Executor executor, OperationalDatasetCallback callback)367         OperationalDatasetCallbackProxy(
368                 @CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
369             mExecutor = executor;
370             mCallback = callback;
371         }
372 
373         @Override
onActiveOperationalDatasetChanged( @ullable ActiveOperationalDataset activeDataset)374         public void onActiveOperationalDatasetChanged(
375                 @Nullable ActiveOperationalDataset activeDataset) {
376             final long identity = Binder.clearCallingIdentity();
377             try {
378                 mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
379             } finally {
380                 Binder.restoreCallingIdentity(identity);
381             }
382         }
383 
384         @Override
onPendingOperationalDatasetChanged( @ullable PendingOperationalDataset pendingDataset)385         public void onPendingOperationalDatasetChanged(
386                 @Nullable PendingOperationalDataset pendingDataset) {
387             final long identity = Binder.clearCallingIdentity();
388             try {
389                 mExecutor.execute(
390                         () -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
391             } finally {
392                 Binder.restoreCallingIdentity(identity);
393             }
394         }
395     }
396 
397     /**
398      * Registers a callback to be called when Thread Operational Datasets are changed.
399      *
400      * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
401      * existing Operational Datasets.
402      *
403      * @param executor the executor to execute {@code callback}
404      * @param callback the callback to receive Operational Dataset changes
405      * @throws IllegalArgumentException if {@code callback} has already been registered
406      */
407     @RequiresPermission(
408             allOf = {
409                 permission.ACCESS_NETWORK_STATE,
410                 "android.permission.THREAD_NETWORK_PRIVILEGED"
411             })
registerOperationalDatasetCallback( @onNull @allbackExecutor Executor executor, @NonNull OperationalDatasetCallback callback)412     public void registerOperationalDatasetCallback(
413             @NonNull @CallbackExecutor Executor executor,
414             @NonNull OperationalDatasetCallback callback) {
415         requireNonNull(executor, "executor cannot be null");
416         requireNonNull(callback, "callback cannot be null");
417         synchronized (mOpDatasetCallbackMapLock) {
418             if (mOpDatasetCallbackMap.containsKey(callback)) {
419                 throw new IllegalArgumentException("callback has already been registered");
420             }
421             OperationalDatasetCallbackProxy callbackProxy =
422                     new OperationalDatasetCallbackProxy(executor, callback);
423             mOpDatasetCallbackMap.put(callback, callbackProxy);
424 
425             try {
426                 mControllerService.registerOperationalDatasetCallback(callbackProxy);
427             } catch (RemoteException e) {
428                 mOpDatasetCallbackMap.remove(callback);
429                 e.rethrowFromSystemServer();
430             }
431         }
432     }
433 
434     /**
435      * Unregisters the Thread Operational Dataset callback.
436      *
437      * @param callback the callback which has been registered with {@link
438      *     #registerOperationalDatasetCallback}
439      * @throws IllegalArgumentException if {@code callback} hasn't been registered
440      */
441     @RequiresPermission(
442             allOf = {
443                 permission.ACCESS_NETWORK_STATE,
444                 "android.permission.THREAD_NETWORK_PRIVILEGED"
445             })
unregisterOperationalDatasetCallback(@onNull OperationalDatasetCallback callback)446     public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
447         requireNonNull(callback, "callback cannot be null");
448         synchronized (mOpDatasetCallbackMapLock) {
449             OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
450             if (callbackProxy == null) {
451                 throw new IllegalArgumentException("callback hasn't been registered");
452             }
453             try {
454                 mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
455                 mOpDatasetCallbackMap.remove(callback);
456             } catch (RemoteException e) {
457                 e.rethrowFromSystemServer();
458             }
459         }
460     }
461 
462     /**
463      * Joins to a Thread network with given Active Operational Dataset.
464      *
465      * <p>This method does nothing if this device has already joined to the same network specified
466      * by {@code activeDataset}. If this device has already joined to a different network, this
467      * device will first leave from that network and then join the new network. This method changes
468      * only this device and all other connected devices will stay in the old network. To change the
469      * network for all connected devices together, use {@link #scheduleMigration}.
470      *
471      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
472      * will be persisted on this device; this device will try to attach to the Thread network and
473      * the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
474      * OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
475      *
476      * <ul>
477      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
478      *       specifies a channel which is not supported in the current country or region; the {@code
479      *       activeDataset} is rejected and not persisted so this device won't auto re-join the next
480      *       time
481      *   <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
482      *       {@code join} or {@code leave} operation
483      * </ul>
484      *
485      * @param activeDataset the Active Operational Dataset represents the Thread network to join
486      * @param executor the executor to execute {@code receiver}
487      * @param receiver the receiver to receive result of this operation
488      */
489     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
join( @onNull ActiveOperationalDataset activeDataset, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)490     public void join(
491             @NonNull ActiveOperationalDataset activeDataset,
492             @NonNull @CallbackExecutor Executor executor,
493             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
494         requireNonNull(activeDataset, "activeDataset cannot be null");
495         requireNonNull(executor, "executor cannot be null");
496         requireNonNull(receiver, "receiver cannot be null");
497         try {
498             mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
499         } catch (RemoteException e) {
500             throw e.rethrowFromSystemServer();
501         }
502     }
503 
504     /**
505      * Schedules a network migration which moves all devices in the current connected network to a
506      * new network or updates parameters of the current connected network.
507      *
508      * <p>The migration doesn't happen immediately but is registered to the Leader device so that
509      * all devices in the current Thread network can be scheduled to apply the new dataset together.
510      *
511      * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
512      * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
513      * changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
514      * has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
515      * OutcomeReceiver#onError} will be called with a specific error:
516      *
517      * <ul>
518      *   <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
519      *       because this device is not attached
520      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
521      *       specifies a channel which is not supported in the current country or region; the {@code
522      *       pendingDataset} is rejected and not persisted
523      *   <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
524      *       by the Leader device
525      *   <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
526      *       being processed
527      *   <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
528      *       been received before deadline
529      * </ul>
530      *
531      * <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
532      * It's important to select a proper value to safely migrate all devices in the network without
533      * leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
534      * if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
535      * network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
536      *
537      * @param pendingDataset the Pending Operational Dataset
538      * @param executor the executor to execute {@code receiver}
539      * @param receiver the receiver to receive result of this operation
540      */
541     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
scheduleMigration( @onNull PendingOperationalDataset pendingDataset, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)542     public void scheduleMigration(
543             @NonNull PendingOperationalDataset pendingDataset,
544             @NonNull @CallbackExecutor Executor executor,
545             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
546         requireNonNull(pendingDataset, "pendingDataset cannot be null");
547         requireNonNull(executor, "executor cannot be null");
548         requireNonNull(receiver, "receiver cannot be null");
549         try {
550             mControllerService.scheduleMigration(
551                     pendingDataset, new OperationReceiverProxy(executor, receiver));
552         } catch (RemoteException e) {
553             throw e.rethrowFromSystemServer();
554         }
555     }
556 
557     /**
558      * Leaves from the Thread network.
559      *
560      * <p>This undoes a {@link join} operation. On success, this device is disconnected from the
561      * joined network and will not automatically join a network before {@link #join} is called
562      * again. Active and Pending Operational Dataset configured and persisted on this device will be
563      * removed too.
564      *
565      * @param executor the executor to execute {@code receiver}
566      * @param receiver the receiver to receive result of this operation
567      */
568     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
leave( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)569     public void leave(
570             @NonNull @CallbackExecutor Executor executor,
571             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
572         requireNonNull(executor, "executor cannot be null");
573         requireNonNull(receiver, "receiver cannot be null");
574         try {
575             mControllerService.leave(new OperationReceiverProxy(executor, receiver));
576         } catch (RemoteException e) {
577             throw e.rethrowFromSystemServer();
578         }
579     }
580 
581     /**
582      * Sets to use a specified test network as the upstream.
583      *
584      * @param testNetworkInterfaceName The name of the test network interface. When it's null,
585      *     forbids using test network as an upstream.
586      * @param executor the executor to execute {@code receiver}
587      * @param receiver the receiver to receive result of this operation
588      * @hide
589      */
590     @VisibleForTesting
591     @RequiresPermission(
592             allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
setTestNetworkAsUpstream( @ullable String testNetworkInterfaceName, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)593     public void setTestNetworkAsUpstream(
594             @Nullable String testNetworkInterfaceName,
595             @NonNull @CallbackExecutor Executor executor,
596             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
597         requireNonNull(executor, "executor cannot be null");
598         requireNonNull(receiver, "receiver cannot be null");
599         try {
600             mControllerService.setTestNetworkAsUpstream(
601                     testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
602         } catch (RemoteException e) {
603             throw e.rethrowFromSystemServer();
604         }
605     }
606 
607     /**
608      * Sets max power of each channel.
609      *
610      * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
611      * chip firmware.
612      *
613      * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
614      * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
615      * OutcomeReceiver#onError} will be called with a specific error:
616      *
617      * <ul>
618      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
619      *       supported by the platform.
620      * </ul>
621      *
622      * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
623      *     and corresponding max power. Valid channel values should be between {@link
624      *     ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
625      *     ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
626      *     power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
627      *     is set to INT16_MAX, the corresponding channel is not supported.
628      * @param executor the executor to execute {@code receiver}.
629      * @param receiver the receiver to receive the result of this operation.
630      * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
631      *     or invalid channel or max power is configured.
632      * @hide
633      */
634     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
setChannelMaxPowers( @onNull @izemin = 1) SparseIntArray channelMaxPowers, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)635     public final void setChannelMaxPowers(
636             @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
637             @NonNull @CallbackExecutor Executor executor,
638             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
639         requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null");
640         requireNonNull(executor, "executor cannot be null");
641         requireNonNull(receiver, "receiver cannot be null");
642 
643         if (channelMaxPowers.size() < 1) {
644             throw new IllegalArgumentException("channelMaxPowers cannot be empty");
645         }
646 
647         for (int i = 0; i < channelMaxPowers.size(); i++) {
648             int channel = channelMaxPowers.keyAt(i);
649             int maxPower = channelMaxPowers.get(channel);
650 
651             if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
652                     || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
653                 throw new IllegalArgumentException(
654                         "Channel "
655                                 + channel
656                                 + " exceeds allowed range ["
657                                 + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ
658                                 + ", "
659                                 + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
660                                 + "]");
661             }
662 
663             if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
664                 throw new IllegalArgumentException(
665                         "Channel power ({channel: "
666                                 + channel
667                                 + ", maxPower: "
668                                 + maxPower
669                                 + "}) exceeds allowed range ["
670                                 + POWER_LIMITATION_MIN
671                                 + ", "
672                                 + POWER_LIMITATION_MAX
673                                 + "]");
674             }
675         }
676 
677         try {
678             mControllerService.setChannelMaxPowers(
679                     toChannelMaxPowerArray(channelMaxPowers),
680                     new OperationReceiverProxy(executor, receiver));
681         } catch (RemoteException e) {
682             throw e.rethrowFromSystemServer();
683         }
684     }
685 
toChannelMaxPowerArray( @onNull SparseIntArray channelMaxPowers)686     private static ChannelMaxPower[] toChannelMaxPowerArray(
687             @NonNull SparseIntArray channelMaxPowers) {
688         final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()];
689 
690         for (int i = 0; i < channelMaxPowers.size(); i++) {
691             powerArray[i] = new ChannelMaxPower();
692             powerArray[i].channel = channelMaxPowers.keyAt(i);
693             powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel);
694         }
695 
696         return powerArray;
697     }
698 
propagateError( Executor executor, OutcomeReceiver<T, ThreadNetworkException> receiver, int errorCode, String errorMsg)699     private static <T> void propagateError(
700             Executor executor,
701             OutcomeReceiver<T, ThreadNetworkException> receiver,
702             int errorCode,
703             String errorMsg) {
704         final long identity = Binder.clearCallingIdentity();
705         try {
706             executor.execute(
707                     () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
708         } finally {
709             Binder.restoreCallingIdentity(identity);
710         }
711     }
712 
713     private static final class ActiveDatasetReceiverProxy
714             extends IActiveOperationalDatasetReceiver.Stub {
715         final Executor mExecutor;
716         final OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> mResultReceiver;
717 
ActiveDatasetReceiverProxy( @allbackExecutor Executor executor, OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver)718         ActiveDatasetReceiverProxy(
719                 @CallbackExecutor Executor executor,
720                 OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver) {
721             this.mExecutor = executor;
722             this.mResultReceiver = resultReceiver;
723         }
724 
725         @Override
onSuccess(ActiveOperationalDataset dataset)726         public void onSuccess(ActiveOperationalDataset dataset) {
727             final long identity = Binder.clearCallingIdentity();
728             try {
729                 mExecutor.execute(() -> mResultReceiver.onResult(dataset));
730             } finally {
731                 Binder.restoreCallingIdentity(identity);
732             }
733         }
734 
735         @Override
onError(int errorCode, String errorMessage)736         public void onError(int errorCode, String errorMessage) {
737             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
738         }
739     }
740 
741     private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
742         final Executor mExecutor;
743         final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
744 
OperationReceiverProxy( @allbackExecutor Executor executor, OutcomeReceiver<Void, ThreadNetworkException> resultReceiver)745         OperationReceiverProxy(
746                 @CallbackExecutor Executor executor,
747                 OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
748             this.mExecutor = executor;
749             this.mResultReceiver = resultReceiver;
750         }
751 
752         @Override
onSuccess()753         public void onSuccess() {
754             final long identity = Binder.clearCallingIdentity();
755             try {
756                 mExecutor.execute(() -> mResultReceiver.onResult(null));
757             } finally {
758                 Binder.restoreCallingIdentity(identity);
759             }
760         }
761 
762         @Override
onError(int errorCode, String errorMessage)763         public void onError(int errorCode, String errorMessage) {
764             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
765         }
766     }
767 }
768