• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.SuppressLint;
30 import android.annotation.SystemApi;
31 import android.os.Binder;
32 import android.os.OutcomeReceiver;
33 import android.os.RemoteException;
34 import android.util.SparseIntArray;
35 
36 import com.android.internal.annotations.GuardedBy;
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.net.thread.flags.Flags;
39 
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.time.Duration;
43 import java.time.Instant;
44 import java.util.HashMap;
45 import java.util.Map;
46 import java.util.concurrent.Executor;
47 import java.util.function.Consumer;
48 
49 /**
50  * Provides the primary APIs for controlling all aspects of a Thread network.
51  *
52  * <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
53  * migrate an existing network.
54  *
55  * @hide
56  */
57 @FlaggedApi(Flags.FLAG_THREAD_ENABLED)
58 @SystemApi
59 public final class ThreadNetworkController {
60     private static final String TAG = "ThreadNetworkController";
61 
62     /** The Thread stack is stopped. */
63     public static final int DEVICE_ROLE_STOPPED = 0;
64 
65     /** The device is not currently participating in a Thread network/partition. */
66     public static final int DEVICE_ROLE_DETACHED = 1;
67 
68     /** The device is a Thread Child. */
69     public static final int DEVICE_ROLE_CHILD = 2;
70 
71     /** The device is a Thread Router. */
72     public static final int DEVICE_ROLE_ROUTER = 3;
73 
74     /** The device is a Thread Leader. */
75     public static final int DEVICE_ROLE_LEADER = 4;
76 
77     /** The Thread radio is disabled. */
78     public static final int STATE_DISABLED = 0;
79 
80     /** The Thread radio is enabled. */
81     public static final int STATE_ENABLED = 1;
82 
83     /** The Thread radio is being disabled. */
84     public static final int STATE_DISABLING = 2;
85 
86     /** The ephemeral key mode is disabled. */
87     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
88     public static final int EPHEMERAL_KEY_DISABLED = 0;
89 
90     /**
91      * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral
92      * key to connect to this device and get Thread credential shared.
93      */
94     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
95     public static final int EPHEMERAL_KEY_ENABLED = 1;
96 
97     /**
98      * The ephemeral key is in use. This state means there is already an active secure session
99      * connected to this device with the ephemeral key, it's not possible to use the ephemeral key
100      * for new connections in this state.
101      */
102     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
103     public static final int EPHEMERAL_KEY_IN_USE = 2;
104 
105     /** @hide */
106     @Retention(RetentionPolicy.SOURCE)
107     @IntDef({
108         DEVICE_ROLE_STOPPED,
109         DEVICE_ROLE_DETACHED,
110         DEVICE_ROLE_CHILD,
111         DEVICE_ROLE_ROUTER,
112         DEVICE_ROLE_LEADER
113     })
114     public @interface DeviceRole {}
115 
116     /** @hide */
117     @Retention(RetentionPolicy.SOURCE)
118     @IntDef(
119             prefix = {"STATE_"},
120             value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
121     public @interface EnabledState {}
122 
123     /** @hide */
124     @Retention(RetentionPolicy.SOURCE)
125     @IntDef(
126             prefix = {"EPHEMERAL_KEY_"},
127             value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE})
128     public @interface EphemeralKeyState {}
129 
130     /** Thread standard version 1.3. */
131     public static final int THREAD_VERSION_1_3 = 4;
132 
133     /** The value of max power to disable the Thread channel. */
134     // This constant can never change. It has "max" in the name not because it indicates
135     // maximum power, but because it's passed to an API that sets the maximum power to
136     // disabled the Thread channel.
137     @SuppressLint("MinMaxConstant")
138     public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
139 
140     /** The maximum lifetime of an ephemeral key. @hide */
141     @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10);
142 
143     /** @hide */
144     @Retention(RetentionPolicy.SOURCE)
145     @IntDef({THREAD_VERSION_1_3})
146     public @interface ThreadVersion {}
147 
148     private final IThreadNetworkController mControllerService;
149 
150     private final Object mStateCallbackMapLock = new Object();
151 
152     @GuardedBy("mStateCallbackMapLock")
153     private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
154 
155     private final Object mOpDatasetCallbackMapLock = new Object();
156 
157     @GuardedBy("mOpDatasetCallbackMapLock")
158     private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
159             mOpDatasetCallbackMap = new HashMap<>();
160 
161     private final Object mConfigurationCallbackMapLock = new Object();
162 
163     @GuardedBy("mConfigurationCallbackMapLock")
164     private final Map<Consumer<ThreadConfiguration>, ConfigurationCallbackProxy>
165             mConfigurationCallbackMap = new HashMap<>();
166 
167     /** @hide */
ThreadNetworkController(@onNull IThreadNetworkController controllerService)168     public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
169         requireNonNull(controllerService, "controllerService cannot be null");
170         mControllerService = controllerService;
171     }
172 
173     /**
174      * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
175      * be persistent and survives device reboots.
176      *
177      * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
178      * require the Thread radio will fail with error code {@link
179      * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
180      * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
181      * with error code {@link ThreadNetworkException#ERROR_BUSY}.
182      *
183      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
184      * the operation has completed. But there maybe subsequent calls to update the enabled state,
185      * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
186      * enabled state changes.
187      *
188      * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
189      * specific error in {@link ThreadNetworkException#ERROR_}.
190      *
191      * @param enabled {@code true} for enabling Thread
192      * @param executor the executor to execute {@code receiver}
193      * @param receiver the receiver to receive result of this operation
194      */
195     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
setEnabled( boolean enabled, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)196     public void setEnabled(
197             boolean enabled,
198             @NonNull @CallbackExecutor Executor executor,
199             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
200         try {
201             mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
202         } catch (RemoteException e) {
203             throw e.rethrowFromSystemServer();
204         }
205     }
206 
207     /** Returns the maximum lifetime allowed when activating ephemeral key mode. */
208     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
209     @NonNull
getMaxEphemeralKeyLifetime()210     public Duration getMaxEphemeralKeyLifetime() {
211         return EPHEMERAL_KEY_LIFETIME_MAX;
212     }
213 
214     /**
215      * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary,
216      * single-use numeric code that is used for Thread Administration Sharing. After activation, the
217      * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral
218      * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key
219      * state changes.
220      *
221      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral
222      * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically
223      * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be
224      * notified with a call to {@link #onEphemeralKeyStateChanged}.
225      *
226      * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
227      * specific error:
228      *
229      * <ul>
230      *   <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not a
231      *       Border Router or not attached to Thread network
232      *   <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
233      *       on the device, caller can recover from this error when the ephemeral key mode gets
234      *       deactivated
235      * </ul>
236      *
237      * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link
238      *     Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}.
239      * @param executor the executor on which to execute {@code receiver}
240      * @param receiver the receiver to receive the result of this operation
241      * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range
242      */
243     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
244     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
activateEphemeralKeyMode( @onNull Duration lifetime, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)245     public void activateEphemeralKeyMode(
246             @NonNull Duration lifetime,
247             @NonNull @CallbackExecutor Executor executor,
248             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
249         if (lifetime.compareTo(Duration.ZERO) <= 0
250                 || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) {
251             throw new IllegalArgumentException(
252                     "Invalid ephemeral key lifetime: the value must be in range of (0, "
253                             + EPHEMERAL_KEY_LIFETIME_MAX
254                             + "]");
255         }
256         long lifetimeMillis = lifetime.toMillis();
257         try {
258             mControllerService.activateEphemeralKeyMode(
259                     lifetimeMillis, new OperationReceiverProxy(executor, receiver));
260         } catch (RemoteException e) {
261             throw e.rethrowFromSystemServer();
262         }
263     }
264 
265     /**
266      * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the
267      * connection will be terminated.
268      *
269      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
270      * always succeed if the device is not in ephemeral key mode. It returns an error {@link
271      * ThreadNetworkException#ERROR_FAILED_PRECONDITION} if this device is not a Border Router.
272      *
273      * @param executor the executor to execute {@code receiver}
274      * @param receiver the receiver to receive the result of this operation
275      */
276     @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
277     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
deactivateEphemeralKeyMode( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)278     public void deactivateEphemeralKeyMode(
279             @NonNull @CallbackExecutor Executor executor,
280             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
281         try {
282             mControllerService.deactivateEphemeralKeyMode(
283                     new OperationReceiverProxy(executor, receiver));
284         } catch (RemoteException e) {
285             throw e.rethrowFromSystemServer();
286         }
287     }
288 
289     /** Returns the Thread version this device is operating on. */
290     @ThreadVersion
getThreadVersion()291     public int getThreadVersion() {
292         try {
293             return mControllerService.getThreadVersion();
294         } catch (RemoteException e) {
295             throw e.rethrowFromSystemServer();
296         }
297     }
298 
299     /**
300      * Creates a new Active Operational Dataset with randomized parameters.
301      *
302      * <p>This method is the recommended way to create a randomized dataset which can be used with
303      * {@link #join} to securely join this device to the specified network . It's highly discouraged
304      * to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
305      * the security of a Thread network.
306      *
307      * @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
308      *     isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
309      *     #LENGTH_MAX_NETWORK_NAME_BYTES}]
310      */
createRandomizedDataset( @onNull String networkName, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver)311     public void createRandomizedDataset(
312             @NonNull String networkName,
313             @NonNull @CallbackExecutor Executor executor,
314             @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver) {
315         ActiveOperationalDataset.checkNetworkName(networkName);
316         requireNonNull(executor, "executor cannot be null");
317         requireNonNull(receiver, "receiver cannot be null");
318         try {
319             mControllerService.createRandomizedDataset(
320                     networkName, new ActiveDatasetReceiverProxy(executor, receiver));
321         } catch (RemoteException e) {
322             e.rethrowFromSystemServer();
323         }
324     }
325 
326     /** Returns {@code true} if {@code deviceRole} indicates an attached state. */
isAttached(@eviceRole int deviceRole)327     public static boolean isAttached(@DeviceRole int deviceRole) {
328         return deviceRole == DEVICE_ROLE_CHILD
329                 || deviceRole == DEVICE_ROLE_ROUTER
330                 || deviceRole == DEVICE_ROLE_LEADER;
331     }
332 
333     /**
334      * Callback to receive notifications when the Thread network states are changed.
335      *
336      * <p>Applications which are interested in monitoring Thread network states should implement
337      * this interface and register the callback with {@link #registerStateCallback}.
338      */
339     public interface StateCallback {
340         /**
341          * The Thread device role has changed.
342          *
343          * @param deviceRole the new Thread device role
344          */
onDeviceRoleChanged(@eviceRole int deviceRole)345         void onDeviceRoleChanged(@DeviceRole int deviceRole);
346 
347         /**
348          * The Thread network partition ID has changed.
349          *
350          * @param partitionId the new Thread partition ID
351          */
onPartitionIdChanged(long partitionId)352         default void onPartitionIdChanged(long partitionId) {}
353 
354         /**
355          * The Thread enabled state has changed.
356          *
357          * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
358          * airplane mode or admin control.
359          *
360          * @param enabledState the new Thread enabled state
361          */
onThreadEnableStateChanged(@nabledState int enabledState)362         default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
363 
364         /**
365          * The ephemeral key state has changed.
366          *
367          * @param ephemeralKeyState the ephemeral key state
368          * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits
369          *     0-9 of user-input friendly length (typically 9), or {@code null} if {@code
370          *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
371          *     permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
372          * @param expiry a timestamp of when the ephemeral key will expire or {@code null} if {@code
373          *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
374          */
375         @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
376         @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
onEphemeralKeyStateChanged( @phemeralKeyState int ephemeralKeyState, @Nullable String ephemeralKey, @Nullable Instant expiry)377         default void onEphemeralKeyStateChanged(
378                 @EphemeralKeyState int ephemeralKeyState,
379                 @Nullable String ephemeralKey,
380                 @Nullable Instant expiry) {}
381     }
382 
383     private static final class StateCallbackProxy extends IStateCallback.Stub {
384         private final Executor mExecutor;
385         private final StateCallback mCallback;
386 
StateCallbackProxy(@allbackExecutor Executor executor, StateCallback callback)387         StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
388             mExecutor = executor;
389             mCallback = callback;
390         }
391 
392         @Override
onDeviceRoleChanged(@eviceRole int deviceRole)393         public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
394             final long identity = Binder.clearCallingIdentity();
395             try {
396                 mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
397             } finally {
398                 Binder.restoreCallingIdentity(identity);
399             }
400         }
401 
402         @Override
onPartitionIdChanged(long partitionId)403         public void onPartitionIdChanged(long partitionId) {
404             final long identity = Binder.clearCallingIdentity();
405             try {
406                 mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
407             } finally {
408                 Binder.restoreCallingIdentity(identity);
409             }
410         }
411 
412         @Override
onThreadEnableStateChanged(@nabledState int enabled)413         public void onThreadEnableStateChanged(@EnabledState int enabled) {
414             final long identity = Binder.clearCallingIdentity();
415             try {
416                 mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
417             } finally {
418                 Binder.restoreCallingIdentity(identity);
419             }
420         }
421 
422         @Override
onEphemeralKeyStateChanged( @phemeralKeyState int ephemeralKeyState, String ephemeralKey, long lifetimeMillis)423         public void onEphemeralKeyStateChanged(
424                 @EphemeralKeyState int ephemeralKeyState,
425                 String ephemeralKey,
426                 long lifetimeMillis) {
427             final long identity = Binder.clearCallingIdentity();
428             final Instant expiry =
429                     ephemeralKeyState == EPHEMERAL_KEY_DISABLED
430                             ? null
431                             : Instant.now().plusMillis(lifetimeMillis);
432 
433             try {
434                 mExecutor.execute(
435                         () ->
436                                 mCallback.onEphemeralKeyStateChanged(
437                                         ephemeralKeyState, ephemeralKey, expiry));
438             } finally {
439                 Binder.restoreCallingIdentity(identity);
440             }
441         }
442     }
443 
444     /**
445      * Registers a callback to be called when Thread network states are changed.
446      *
447      * <p>Upon return of this method, all methods of {@code callback} will be invoked immediately
448      * with existing states. The order of the invoked callbacks is not guaranteed.
449      *
450      * @param executor the executor to execute the {@code callback}
451      * @param callback the callback to receive Thread network state changes
452      * @throws IllegalArgumentException if {@code callback} has already been registered
453      */
454     @RequiresPermission(permission.ACCESS_NETWORK_STATE)
registerStateCallback( @onNull @allbackExecutor Executor executor, @NonNull StateCallback callback)455     public void registerStateCallback(
456             @NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
457         requireNonNull(executor, "executor cannot be null");
458         requireNonNull(callback, "callback cannot be null");
459         synchronized (mStateCallbackMapLock) {
460             if (mStateCallbackMap.containsKey(callback)) {
461                 throw new IllegalArgumentException("callback has already been registered");
462             }
463             StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
464             mStateCallbackMap.put(callback, callbackProxy);
465 
466             try {
467                 mControllerService.registerStateCallback(callbackProxy);
468             } catch (RemoteException e) {
469                 mStateCallbackMap.remove(callback);
470                 e.rethrowFromSystemServer();
471             }
472         }
473     }
474 
475     /**
476      * Unregisters the Thread state changed callback.
477      *
478      * @param callback the callback which has been registered with {@link #registerStateCallback}
479      * @throws IllegalArgumentException if {@code callback} hasn't been registered
480      */
481     @RequiresPermission(permission.ACCESS_NETWORK_STATE)
unregisterStateCallback(@onNull StateCallback callback)482     public void unregisterStateCallback(@NonNull StateCallback callback) {
483         requireNonNull(callback, "callback cannot be null");
484         synchronized (mStateCallbackMapLock) {
485             StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
486             if (callbackProxy == null) {
487                 throw new IllegalArgumentException("callback hasn't been registered");
488             }
489             try {
490                 mControllerService.unregisterStateCallback(callbackProxy);
491                 mStateCallbackMap.remove(callback);
492             } catch (RemoteException e) {
493                 e.rethrowFromSystemServer();
494             }
495         }
496     }
497 
498     /**
499      * Callback to receive notifications when the Thread Operational Datasets are changed.
500      *
501      * <p>Applications which are interested in monitoring Thread network datasets should implement
502      * this interface and register the callback with {@link #registerOperationalDatasetCallback}.
503      */
504     public interface OperationalDatasetCallback {
505         /**
506          * Called when the Active Operational Dataset is changed.
507          *
508          * @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
509          *     absent
510          */
onActiveOperationalDatasetChanged(@ullable ActiveOperationalDataset activeDataset)511         void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
512 
513         /**
514          * Called when the Pending Operational Dataset is changed.
515          *
516          * @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
517          *     has been committed and removed
518          */
onPendingOperationalDatasetChanged( @ullable PendingOperationalDataset pendingDataset)519         default void onPendingOperationalDatasetChanged(
520                 @Nullable PendingOperationalDataset pendingDataset) {}
521     }
522 
523     private static final class OperationalDatasetCallbackProxy
524             extends IOperationalDatasetCallback.Stub {
525         private final Executor mExecutor;
526         private final OperationalDatasetCallback mCallback;
527 
OperationalDatasetCallbackProxy( @allbackExecutor Executor executor, OperationalDatasetCallback callback)528         OperationalDatasetCallbackProxy(
529                 @CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
530             mExecutor = executor;
531             mCallback = callback;
532         }
533 
534         @Override
onActiveOperationalDatasetChanged( @ullable ActiveOperationalDataset activeDataset)535         public void onActiveOperationalDatasetChanged(
536                 @Nullable ActiveOperationalDataset activeDataset) {
537             final long identity = Binder.clearCallingIdentity();
538             try {
539                 mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
540             } finally {
541                 Binder.restoreCallingIdentity(identity);
542             }
543         }
544 
545         @Override
onPendingOperationalDatasetChanged( @ullable PendingOperationalDataset pendingDataset)546         public void onPendingOperationalDatasetChanged(
547                 @Nullable PendingOperationalDataset pendingDataset) {
548             final long identity = Binder.clearCallingIdentity();
549             try {
550                 mExecutor.execute(
551                         () -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
552             } finally {
553                 Binder.restoreCallingIdentity(identity);
554             }
555         }
556     }
557 
558     /**
559      * Registers a callback to be called when Thread Operational Datasets are changed.
560      *
561      * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
562      * existing Operational Datasets.
563      *
564      * @param executor the executor to execute {@code callback}
565      * @param callback the callback to receive Operational Dataset changes
566      * @throws IllegalArgumentException if {@code callback} has already been registered
567      */
568     @RequiresPermission(
569             allOf = {
570                 permission.ACCESS_NETWORK_STATE,
571                 "android.permission.THREAD_NETWORK_PRIVILEGED"
572             })
registerOperationalDatasetCallback( @onNull @allbackExecutor Executor executor, @NonNull OperationalDatasetCallback callback)573     public void registerOperationalDatasetCallback(
574             @NonNull @CallbackExecutor Executor executor,
575             @NonNull OperationalDatasetCallback callback) {
576         requireNonNull(executor, "executor cannot be null");
577         requireNonNull(callback, "callback cannot be null");
578         synchronized (mOpDatasetCallbackMapLock) {
579             if (mOpDatasetCallbackMap.containsKey(callback)) {
580                 throw new IllegalArgumentException("callback has already been registered");
581             }
582             OperationalDatasetCallbackProxy callbackProxy =
583                     new OperationalDatasetCallbackProxy(executor, callback);
584             mOpDatasetCallbackMap.put(callback, callbackProxy);
585 
586             try {
587                 mControllerService.registerOperationalDatasetCallback(callbackProxy);
588             } catch (RemoteException e) {
589                 mOpDatasetCallbackMap.remove(callback);
590                 e.rethrowFromSystemServer();
591             }
592         }
593     }
594 
595     /**
596      * Unregisters the Thread Operational Dataset callback.
597      *
598      * @param callback the callback which has been registered with {@link
599      *     #registerOperationalDatasetCallback}
600      * @throws IllegalArgumentException if {@code callback} hasn't been registered
601      */
602     @RequiresPermission(
603             allOf = {
604                 permission.ACCESS_NETWORK_STATE,
605                 "android.permission.THREAD_NETWORK_PRIVILEGED"
606             })
unregisterOperationalDatasetCallback(@onNull OperationalDatasetCallback callback)607     public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
608         requireNonNull(callback, "callback cannot be null");
609         synchronized (mOpDatasetCallbackMapLock) {
610             OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
611             if (callbackProxy == null) {
612                 throw new IllegalArgumentException("callback hasn't been registered");
613             }
614             try {
615                 mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
616                 mOpDatasetCallbackMap.remove(callback);
617             } catch (RemoteException e) {
618                 e.rethrowFromSystemServer();
619             }
620         }
621     }
622 
623     /**
624      * Joins to a Thread network with given Active Operational Dataset.
625      *
626      * <p>This method does nothing if this device has already joined to the same network specified
627      * by {@code activeDataset}. If this device has already joined to a different network, this
628      * device will first leave from that network and then join the new network. This method changes
629      * only this device and all other connected devices will stay in the old network. To change the
630      * network for all connected devices together, use {@link #scheduleMigration}.
631      *
632      * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
633      * will be persisted on this device; this device will try to attach to the Thread network and
634      * the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
635      * OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
636      *
637      * <ul>
638      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
639      *       specifies a channel which is not supported in the current country or region; the {@code
640      *       activeDataset} is rejected and not persisted so this device won't auto re-join the next
641      *       time
642      *   <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
643      *       {@code join} or {@code leave} operation
644      * </ul>
645      *
646      * @param activeDataset the Active Operational Dataset represents the Thread network to join
647      * @param executor the executor to execute {@code receiver}
648      * @param receiver the receiver to receive result of this operation
649      */
650     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
join( @onNull ActiveOperationalDataset activeDataset, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)651     public void join(
652             @NonNull ActiveOperationalDataset activeDataset,
653             @NonNull @CallbackExecutor Executor executor,
654             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
655         requireNonNull(activeDataset, "activeDataset cannot be null");
656         requireNonNull(executor, "executor cannot be null");
657         requireNonNull(receiver, "receiver cannot be null");
658         try {
659             mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
660         } catch (RemoteException e) {
661             throw e.rethrowFromSystemServer();
662         }
663     }
664 
665     /**
666      * Schedules a network migration which moves all devices in the current connected network to a
667      * new network or updates parameters of the current connected network.
668      *
669      * <p>The migration doesn't happen immediately but is registered to the Leader device so that
670      * all devices in the current Thread network can be scheduled to apply the new dataset together.
671      *
672      * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
673      * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
674      * changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
675      * has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
676      * OutcomeReceiver#onError} will be called with a specific error:
677      *
678      * <ul>
679      *   <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
680      *       because this device is not attached
681      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
682      *       specifies a channel which is not supported in the current country or region; the {@code
683      *       pendingDataset} is rejected and not persisted
684      *   <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
685      *       by the Leader device
686      *   <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
687      *       being processed
688      *   <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
689      *       been received before deadline
690      * </ul>
691      *
692      * <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
693      * It's important to select a proper value to safely migrate all devices in the network without
694      * leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
695      * if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
696      * network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
697      *
698      * @param pendingDataset the Pending Operational Dataset
699      * @param executor the executor to execute {@code receiver}
700      * @param receiver the receiver to receive result of this operation
701      */
702     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
scheduleMigration( @onNull PendingOperationalDataset pendingDataset, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)703     public void scheduleMigration(
704             @NonNull PendingOperationalDataset pendingDataset,
705             @NonNull @CallbackExecutor Executor executor,
706             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
707         requireNonNull(pendingDataset, "pendingDataset cannot be null");
708         requireNonNull(executor, "executor cannot be null");
709         requireNonNull(receiver, "receiver cannot be null");
710         try {
711             mControllerService.scheduleMigration(
712                     pendingDataset, new OperationReceiverProxy(executor, receiver));
713         } catch (RemoteException e) {
714             throw e.rethrowFromSystemServer();
715         }
716     }
717 
718     /**
719      * Leaves from the Thread network.
720      *
721      * <p>This undoes a {@link join} operation. On success, this device is disconnected from the
722      * joined network and will not automatically join a network before {@link #join} is called
723      * again. Active and Pending Operational Dataset configured and persisted on this device will be
724      * removed too.
725      *
726      * @param executor the executor to execute {@code receiver}
727      * @param receiver the receiver to receive result of this operation
728      */
729     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
leave( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)730     public void leave(
731             @NonNull @CallbackExecutor Executor executor,
732             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
733         requireNonNull(executor, "executor cannot be null");
734         requireNonNull(receiver, "receiver cannot be null");
735         try {
736             mControllerService.leave(new OperationReceiverProxy(executor, receiver));
737         } catch (RemoteException e) {
738             throw e.rethrowFromSystemServer();
739         }
740     }
741 
742     /**
743      * Configures the Thread features for this device.
744      *
745      * <p>This method sets the {@link ThreadConfiguration} for this device. On success, the {@link
746      * OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
747      * persisted to the device; the configuration changes can be observed by {@link
748      * #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
749      * receiver} will be invoked with a specific error:
750      *
751      * <ul>
752      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the configuration enables a
753      *       feature which is not supported by the platform.
754      * </ul>
755      *
756      * @param configuration the configuration to set
757      * @param executor the executor to execute {@code receiver}
758      * @param receiver the receiver to receive result of this operation
759      */
760     @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
761     @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
setConfiguration( @onNull ThreadConfiguration configuration, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)762     public void setConfiguration(
763             @NonNull ThreadConfiguration configuration,
764             @NonNull @CallbackExecutor Executor executor,
765             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
766         requireNonNull(configuration, "Configuration cannot be null");
767         requireNonNull(executor, "executor cannot be null");
768         requireNonNull(receiver, "receiver cannot be null");
769         try {
770             mControllerService.setConfiguration(
771                     configuration, new OperationReceiverProxy(executor, receiver));
772         } catch (RemoteException e) {
773             throw e.rethrowFromSystemServer();
774         }
775     }
776 
777     /**
778      * Registers a callback to be called when the configuration is changed.
779      *
780      * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
781      * {@link ThreadConfiguration}.
782      *
783      * @param executor the executor to execute the {@code callback}
784      * @param callback the callback to receive Thread configuration changes
785      * @throws IllegalArgumentException if {@code callback} has already been registered
786      */
787     @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
788     @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
registerConfigurationCallback( @onNull @allbackExecutor Executor executor, @NonNull Consumer<ThreadConfiguration> callback)789     public void registerConfigurationCallback(
790             @NonNull @CallbackExecutor Executor executor,
791             @NonNull Consumer<ThreadConfiguration> callback) {
792         requireNonNull(executor, "executor cannot be null");
793         requireNonNull(callback, "callback cannot be null");
794         synchronized (mConfigurationCallbackMapLock) {
795             if (mConfigurationCallbackMap.containsKey(callback)) {
796                 throw new IllegalArgumentException("callback has already been registered");
797             }
798             ConfigurationCallbackProxy callbackProxy =
799                     new ConfigurationCallbackProxy(executor, callback);
800             mConfigurationCallbackMap.put(callback, callbackProxy);
801             try {
802                 mControllerService.registerConfigurationCallback(callbackProxy);
803             } catch (RemoteException e) {
804                 mConfigurationCallbackMap.remove(callback);
805                 e.rethrowFromSystemServer();
806             }
807         }
808     }
809 
810     /**
811      * Unregisters the configuration callback.
812      *
813      * @param callback the callback which has been registered with {@link
814      *     #registerConfigurationCallback}
815      * @throws IllegalArgumentException if {@code callback} hasn't been registered
816      */
817     @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
818     @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
unregisterConfigurationCallback(@onNull Consumer<ThreadConfiguration> callback)819     public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
820         requireNonNull(callback, "callback cannot be null");
821         synchronized (mConfigurationCallbackMapLock) {
822             ConfigurationCallbackProxy callbackProxy = mConfigurationCallbackMap.get(callback);
823             if (callbackProxy == null) {
824                 throw new IllegalArgumentException("callback hasn't been registered");
825             }
826             try {
827                 mControllerService.unregisterConfigurationCallback(callbackProxy);
828                 mConfigurationCallbackMap.remove(callbackProxy.mConfigurationConsumer);
829             } catch (RemoteException e) {
830                 e.rethrowFromSystemServer();
831             }
832         }
833     }
834 
835     /**
836      * Sets to use a specified test network as the upstream.
837      *
838      * @param testNetworkInterfaceName The name of the test network interface. When it's null,
839      *     forbids using test network as an upstream.
840      * @param executor the executor to execute {@code receiver}
841      * @param receiver the receiver to receive result of this operation
842      * @hide
843      */
844     @VisibleForTesting
845     @RequiresPermission(
846             allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
setTestNetworkAsUpstream( @ullable String testNetworkInterfaceName, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)847     public void setTestNetworkAsUpstream(
848             @Nullable String testNetworkInterfaceName,
849             @NonNull @CallbackExecutor Executor executor,
850             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
851         requireNonNull(executor, "executor cannot be null");
852         requireNonNull(receiver, "receiver cannot be null");
853         try {
854             mControllerService.setTestNetworkAsUpstream(
855                     testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
856         } catch (RemoteException e) {
857             throw e.rethrowFromSystemServer();
858         }
859     }
860 
861     /**
862      * Sets max power of each channel.
863      *
864      * <p>This method sets the max power for the given channel. The platform sets the actual output
865      * power to be less than or equal to the {@code channelMaxPowers} and as close as possible to
866      * the {@code channelMaxPowers}.
867      *
868      * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
869      * chip firmware.
870      *
871      * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
872      * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
873      * OutcomeReceiver#onError} will be called with a specific error:
874      *
875      * <ul>
876      *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the feature is not supported
877      *       by the platform.
878      * </ul>
879      *
880      * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
881      *     and corresponding max power. Valid channel values should be between {@link
882      *     ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
883      *     ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. For
884      *     example, 1000 means 0.01W and 2000 means 0.1W. If the power value of {@code
885      *     channelMaxPowers} is lower than the minimum output power supported by the platform, the
886      *     output power will be set to the minimum output power supported by the platform. If the
887      *     power value of {@code channelMaxPowers} is higher than the maximum output power supported
888      *     by the platform, the output power will be set to the maximum output power supported by
889      *     the platform. If the power value of {@code channelMaxPowers} is set to {@link
890      *     #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
891      * @param executor the executor to execute {@code receiver}.
892      * @param receiver the receiver to receive the result of this operation.
893      * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
894      *     or invalid channel or max power is configured.
895      */
896     @FlaggedApi(Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED)
897     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
setChannelMaxPowers( @onNull @izemin = 1) SparseIntArray channelMaxPowers, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver)898     public final void setChannelMaxPowers(
899             @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
900             @NonNull @CallbackExecutor Executor executor,
901             @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
902         requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null");
903         requireNonNull(executor, "executor cannot be null");
904         requireNonNull(receiver, "receiver cannot be null");
905 
906         if (channelMaxPowers.size() < 1) {
907             throw new IllegalArgumentException("channelMaxPowers cannot be empty");
908         }
909 
910         for (int i = 0; i < channelMaxPowers.size(); i++) {
911             int channel = channelMaxPowers.keyAt(i);
912 
913             if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
914                     || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
915                 throw new IllegalArgumentException(
916                         "Channel "
917                                 + channel
918                                 + " exceeds allowed range ["
919                                 + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ
920                                 + ", "
921                                 + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
922                                 + "]");
923             }
924         }
925 
926         try {
927             mControllerService.setChannelMaxPowers(
928                     toChannelMaxPowerArray(channelMaxPowers),
929                     new OperationReceiverProxy(executor, receiver));
930         } catch (RemoteException e) {
931             throw e.rethrowFromSystemServer();
932         }
933     }
934 
toChannelMaxPowerArray( @onNull SparseIntArray channelMaxPowers)935     private static ChannelMaxPower[] toChannelMaxPowerArray(
936             @NonNull SparseIntArray channelMaxPowers) {
937         final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()];
938 
939         for (int i = 0; i < channelMaxPowers.size(); i++) {
940             powerArray[i] = new ChannelMaxPower();
941             powerArray[i].channel = channelMaxPowers.keyAt(i);
942             powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel);
943         }
944 
945         return powerArray;
946     }
947 
propagateError( Executor executor, OutcomeReceiver<T, ThreadNetworkException> receiver, int errorCode, String errorMsg)948     private static <T> void propagateError(
949             Executor executor,
950             OutcomeReceiver<T, ThreadNetworkException> receiver,
951             int errorCode,
952             String errorMsg) {
953         final long identity = Binder.clearCallingIdentity();
954         try {
955             executor.execute(
956                     () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
957         } finally {
958             Binder.restoreCallingIdentity(identity);
959         }
960     }
961 
962     private static final class ActiveDatasetReceiverProxy
963             extends IActiveOperationalDatasetReceiver.Stub {
964         final Executor mExecutor;
965         final OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> mResultReceiver;
966 
ActiveDatasetReceiverProxy( @allbackExecutor Executor executor, OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver)967         ActiveDatasetReceiverProxy(
968                 @CallbackExecutor Executor executor,
969                 OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver) {
970             this.mExecutor = executor;
971             this.mResultReceiver = resultReceiver;
972         }
973 
974         @Override
onSuccess(ActiveOperationalDataset dataset)975         public void onSuccess(ActiveOperationalDataset dataset) {
976             final long identity = Binder.clearCallingIdentity();
977             try {
978                 mExecutor.execute(() -> mResultReceiver.onResult(dataset));
979             } finally {
980                 Binder.restoreCallingIdentity(identity);
981             }
982         }
983 
984         @Override
onError(int errorCode, String errorMessage)985         public void onError(int errorCode, String errorMessage) {
986             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
987         }
988     }
989 
990     private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
991         final Executor mExecutor;
992         final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
993 
OperationReceiverProxy( @allbackExecutor Executor executor, OutcomeReceiver<Void, ThreadNetworkException> resultReceiver)994         OperationReceiverProxy(
995                 @CallbackExecutor Executor executor,
996                 OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
997             this.mExecutor = executor;
998             this.mResultReceiver = resultReceiver;
999         }
1000 
1001         @Override
onSuccess()1002         public void onSuccess() {
1003             final long identity = Binder.clearCallingIdentity();
1004             try {
1005                 mExecutor.execute(() -> mResultReceiver.onResult(null));
1006             } finally {
1007                 Binder.restoreCallingIdentity(identity);
1008             }
1009         }
1010 
1011         @Override
onError(int errorCode, String errorMessage)1012         public void onError(int errorCode, String errorMessage) {
1013             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
1014         }
1015     }
1016 
1017     private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub {
1018         final Executor mExecutor;
1019         final Consumer<ThreadConfiguration> mConfigurationConsumer;
1020 
ConfigurationCallbackProxy( @allbackExecutor Executor executor, Consumer<ThreadConfiguration> ConfigurationConsumer)1021         ConfigurationCallbackProxy(
1022                 @CallbackExecutor Executor executor,
1023                 Consumer<ThreadConfiguration> ConfigurationConsumer) {
1024             this.mExecutor = executor;
1025             this.mConfigurationConsumer = ConfigurationConsumer;
1026         }
1027 
1028         @Override
onConfigurationChanged(ThreadConfiguration configuration)1029         public void onConfigurationChanged(ThreadConfiguration configuration) {
1030             final long identity = Binder.clearCallingIdentity();
1031             try {
1032                 mExecutor.execute(() -> mConfigurationConsumer.accept(configuration));
1033             } finally {
1034                 Binder.restoreCallingIdentity(identity);
1035             }
1036         }
1037     }
1038 }
1039