• 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 com.android.threadnetwork.demoapp;
18 
19 import static com.google.common.io.BaseEncoding.base16;
20 
21 import android.net.ConnectivityManager;
22 import android.net.LinkAddress;
23 import android.net.LinkProperties;
24 import android.net.Network;
25 import android.net.NetworkCapabilities;
26 import android.net.NetworkRequest;
27 import android.net.RouteInfo;
28 import android.net.thread.ActiveOperationalDataset;
29 import android.net.thread.OperationalDatasetTimestamp;
30 import android.net.thread.PendingOperationalDataset;
31 import android.net.thread.ThreadConfiguration;
32 import android.net.thread.ThreadNetworkController;
33 import android.net.thread.ThreadNetworkException;
34 import android.net.thread.ThreadNetworkManager;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.OutcomeReceiver;
39 import android.util.Log;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.widget.Button;
44 import android.widget.TextView;
45 
46 import androidx.core.content.ContextCompat;
47 import androidx.fragment.app.Fragment;
48 
49 import com.google.android.material.switchmaterial.SwitchMaterial;
50 
51 import java.time.Duration;
52 import java.time.Instant;
53 import java.time.temporal.ChronoUnit;
54 import java.util.Timer;
55 import java.util.TimerTask;
56 import java.util.concurrent.Executor;
57 
58 public final class ThreadNetworkSettingsFragment extends Fragment {
59     private static final String TAG = "ThreadNetworkSettings";
60 
61     // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
62     private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
63 
64     private ThreadNetworkController mThreadController;
65     private TextView mTextState;
66     private TextView mTextNetworkInfo;
67     private TextView mMigrateNetworkState;
68     private TextView mEphemeralKeyStateText;
69     private SwitchMaterial mNat64Switch;
70     private Executor mMainExecutor;
71 
72     private int mDeviceRole;
73     private long mPartitionId;
74     private ActiveOperationalDataset mActiveDataset;
75     private int mEphemeralKeyState;
76     private String mEphemeralKey;
77     private Instant mEphemeralKeyExpiry;
78     private Timer mEphemeralKeyLifetimeTimer;
79     private ThreadConfiguration mThreadConfiguration;
80 
81     private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
82             base16().lowerCase()
83                     .decode(
84                             "0e080000000000010000000300001235060004001fffe00208dae21bccb8c321c40708fdc376ead74396bb0510c52f56cd2d38a9eb7a716954f8efd939030f4f70656e5468726561642d646231390102db190410fcb737e6fd6bb1b0fed524a4496363110c0402a0f7f8");
85     private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
86             ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
87 
deviceRoleToString(int mDeviceRole)88     private static String deviceRoleToString(int mDeviceRole) {
89         switch (mDeviceRole) {
90             case ThreadNetworkController.DEVICE_ROLE_STOPPED:
91                 return "Stopped";
92             case ThreadNetworkController.DEVICE_ROLE_DETACHED:
93                 return "Detached";
94             case ThreadNetworkController.DEVICE_ROLE_CHILD:
95                 return "Child";
96             case ThreadNetworkController.DEVICE_ROLE_ROUTER:
97                 return "Router";
98             case ThreadNetworkController.DEVICE_ROLE_LEADER:
99                 return "Leader";
100             default:
101                 return "Unknown";
102         }
103     }
104 
ephemeralKeyStateToString(int ephemeralKeyState)105     private static String ephemeralKeyStateToString(int ephemeralKeyState) {
106         switch (ephemeralKeyState) {
107             case ThreadNetworkController.EPHEMERAL_KEY_DISABLED:
108                 return "Disabled";
109             case ThreadNetworkController.EPHEMERAL_KEY_ENABLED:
110                 return "Enabled";
111             case ThreadNetworkController.EPHEMERAL_KEY_IN_USE:
112                 return "Connected";
113             default:
114                 return "Unknown";
115         }
116     }
117 
booleanToEnabledOrDisabled(boolean enabled)118     private static String booleanToEnabledOrDisabled(boolean enabled) {
119         return enabled ? "Enabled" : "Disabled";
120     }
121 
122     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)123     public View onCreateView(
124             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
125         return inflater.inflate(R.layout.thread_network_settings_fragment, container, false);
126     }
127 
128     @Override
onViewCreated(View view, Bundle savedInstanceState)129     public void onViewCreated(View view, Bundle savedInstanceState) {
130         super.onViewCreated(view, savedInstanceState);
131 
132         ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
133         cm.registerNetworkCallback(
134                 new NetworkRequest.Builder()
135                         .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
136                         .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
137                         .build(),
138                 new ConnectivityManager.NetworkCallback() {
139                     @Override
140                     public void onAvailable(Network network) {
141                         Log.i(TAG, "New Thread network is available");
142                     }
143 
144                     @Override
145                     public void onLinkPropertiesChanged(
146                             Network network, LinkProperties linkProperties) {
147                         updateNetworkInfo(linkProperties);
148                     }
149 
150                     @Override
151                     public void onLost(Network network) {
152                         Log.i(TAG, "Thread network " + network + " is lost");
153                         updateNetworkInfo(null /* linkProperties */);
154                     }
155                 },
156                 new Handler(Looper.myLooper()));
157 
158         mMainExecutor = ContextCompat.getMainExecutor(getActivity());
159         ThreadNetworkManager threadManager =
160                 getActivity().getSystemService(ThreadNetworkManager.class);
161         if (threadManager != null) {
162             mThreadController = threadManager.getAllThreadNetworkControllers().get(0);
163             mThreadController.registerStateCallback(
164                     mMainExecutor,
165                     new ThreadNetworkController.StateCallback() {
166                         @Override
167                         public void onDeviceRoleChanged(int mDeviceRole) {
168                             ThreadNetworkSettingsFragment.this.mDeviceRole = mDeviceRole;
169                             updateState();
170                         }
171 
172                         @Override
173                         public void onPartitionIdChanged(long mPartitionId) {
174                             ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId;
175                             updateState();
176                         }
177 
178                         @Override
179                         public void onEphemeralKeyStateChanged(
180                                 int state, String ephemeralKey, Instant expiry) {
181                             ThreadNetworkSettingsFragment.this.mEphemeralKeyState = state;
182                             ThreadNetworkSettingsFragment.this.mEphemeralKey = ephemeralKey;
183                             ThreadNetworkSettingsFragment.this.mEphemeralKeyExpiry = expiry;
184                             updateState();
185                         }
186                     });
187             mThreadController.registerOperationalDatasetCallback(
188                     mMainExecutor,
189                     newActiveDataset -> {
190                         this.mActiveDataset = newActiveDataset;
191                         updateState();
192                     });
193             mThreadController.registerConfigurationCallback(
194                     mMainExecutor, this::updateConfiguration);
195         }
196 
197         mTextState = (TextView) view.findViewById(R.id.text_state);
198         mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info);
199         mEphemeralKeyStateText = (TextView) view.findViewById(R.id.text_ephemeral_key_state);
200         mNat64Switch = (SwitchMaterial) view.findViewById(R.id.switch_nat64);
201         mNat64Switch.setOnCheckedChangeListener(
202                 (buttonView, isChecked) -> doSetNat64Enabled(isChecked));
203 
204         if (mThreadController == null) {
205             mTextState.setText("Thread not supported!");
206             return;
207         }
208 
209         ((Button) view.findViewById(R.id.button_join_network)).setOnClickListener(v -> doJoin());
210         ((Button) view.findViewById(R.id.button_leave_network)).setOnClickListener(v -> doLeave());
211 
212         mMigrateNetworkState = view.findViewById(R.id.text_migrate_network_state);
213         ((Button) view.findViewById(R.id.button_migrate_network))
214                 .setOnClickListener(v -> doMigration());
215 
216         ((Button) view.findViewById(R.id.button_activate_ephemeral_key_mode))
217                 .setOnClickListener(v -> doActivateEphemeralKeyMode());
218         ((Button) view.findViewById(R.id.button_deactivate_ephemeral_key_mode))
219                 .setOnClickListener(v -> doDeactivateEphemeralKeyMode());
220 
221         updateState();
222     }
223 
doJoin()224     private void doJoin() {
225         mThreadController.join(
226                 DEFAULT_ACTIVE_DATASET,
227                 mMainExecutor,
228                 new OutcomeReceiver<Void, ThreadNetworkException>() {
229                     @Override
230                     public void onError(ThreadNetworkException error) {
231                         Log.e(TAG, "Failed to join network " + DEFAULT_ACTIVE_DATASET, error);
232                     }
233 
234                     @Override
235                     public void onResult(Void v) {
236                         Log.i(TAG, "Successfully Joined");
237                     }
238                 });
239     }
240 
doLeave()241     private void doLeave() {
242         mThreadController.leave(
243                 mMainExecutor,
244                 new OutcomeReceiver<>() {
245                     @Override
246                     public void onError(ThreadNetworkException error) {
247                         Log.e(TAG, "Failed to leave network " + DEFAULT_ACTIVE_DATASET, error);
248                     }
249 
250                     @Override
251                     public void onResult(Void v) {
252                         Log.i(TAG, "Successfully Left");
253                     }
254                 });
255     }
256 
doMigration()257     private void doMigration() {
258         var newActiveDataset =
259                 new ActiveOperationalDataset.Builder(DEFAULT_ACTIVE_DATASET)
260                         .setNetworkName("NewThreadNet")
261                         .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
262                         .build();
263         var pendingDataset =
264                 new PendingOperationalDataset(
265                         newActiveDataset,
266                         OperationalDatasetTimestamp.fromInstant(Instant.now()),
267                         Duration.ofSeconds(30));
268         mThreadController.scheduleMigration(
269                 pendingDataset,
270                 mMainExecutor,
271                 new OutcomeReceiver<Void, ThreadNetworkException>() {
272                     @Override
273                     public void onResult(Void v) {
274                         mMigrateNetworkState.setText(
275                                 "Scheduled migration to network \"NewThreadNet\" in 30s");
276                         // TODO: update Pending Dataset state
277                     }
278 
279                     @Override
280                     public void onError(ThreadNetworkException e) {
281                         mMigrateNetworkState.setText(
282                                 "Failed to schedule migration: " + e.getMessage());
283                     }
284                 });
285     }
286 
doActivateEphemeralKeyMode()287     private void doActivateEphemeralKeyMode() {
288         mThreadController.activateEphemeralKeyMode(
289                 Duration.ofMinutes(2),
290                 mMainExecutor,
291                 new OutcomeReceiver<>() {
292                     @Override
293                     public void onError(ThreadNetworkException error) {
294                         Log.e(TAG, "Failed to activate ephemeral key", error);
295                     }
296 
297                     @Override
298                     public void onResult(Void v) {
299                         Log.i(TAG, "Successfully activated ephemeral key mode");
300                     }
301                 });
302     }
303 
doDeactivateEphemeralKeyMode()304     private void doDeactivateEphemeralKeyMode() {
305         mThreadController.deactivateEphemeralKeyMode(
306                 mMainExecutor,
307                 new OutcomeReceiver<>() {
308                     @Override
309                     public void onError(ThreadNetworkException error) {
310                         Log.e(TAG, "Failed to deactivate ephemeral key", error);
311                     }
312 
313                     @Override
314                     public void onResult(Void v) {
315                         Log.i(TAG, "Successfully deactivated ephemeral key mode");
316                     }
317                 });
318     }
319 
doSetNat64Enabled(boolean enabled)320     private void doSetNat64Enabled(boolean enabled) {
321         if (mThreadConfiguration == null) {
322             Log.e(TAG, "Thread configuration is not available");
323             return;
324         }
325         final ThreadConfiguration config =
326                 new ThreadConfiguration.Builder(mThreadConfiguration)
327                         .setNat64Enabled(enabled)
328                         .build();
329         mThreadController.setConfiguration(
330                 config,
331                 mMainExecutor,
332                 new OutcomeReceiver<>() {
333                     @Override
334                     public void onError(ThreadNetworkException error) {
335                         Log.e(
336                                 TAG,
337                                 "Failed to set NAT64 " + booleanToEnabledOrDisabled(enabled),
338                                 error);
339                     }
340 
341                     @Override
342                     public void onResult(Void v) {
343                         Log.i(TAG, "Successfully set NAT64 " + booleanToEnabledOrDisabled(enabled));
344                     }
345                 });
346     }
347 
updateState()348     private void updateState() {
349         Log.i(
350                 TAG,
351                 String.format(
352                         "Updating Thread states (mDeviceRole: %s, mEphemeralKeyState: %s)",
353                         deviceRoleToString(mDeviceRole),
354                         ephemeralKeyStateToString(mEphemeralKeyState)));
355 
356         String state =
357                 String.format(
358                         "Role             %s\n"
359                                 + "Partition ID     %d\n"
360                                 + "Network Name     %s\n"
361                                 + "Extended PAN ID  %s",
362                         deviceRoleToString(mDeviceRole),
363                         mPartitionId,
364                         mActiveDataset != null ? mActiveDataset.getNetworkName() : null,
365                         mActiveDataset != null
366                                 ? base16().encode(mActiveDataset.getExtendedPanId())
367                                 : null);
368         mTextState.setText(state);
369 
370         updateEphemeralKeyStatus();
371     }
372 
updateEphemeralKeyStatus()373     private void updateEphemeralKeyStatus() {
374         StringBuilder sb = new StringBuilder();
375         sb.append(ephemeralKeyStateToString(mEphemeralKeyState));
376         if (mEphemeralKeyState != ThreadNetworkController.EPHEMERAL_KEY_DISABLED) {
377             sb.append("\nPasscode: ");
378             sb.append(mEphemeralKey);
379             sb.append("\nRemaining lifetime: ");
380             sb.append(Instant.now().until(mEphemeralKeyExpiry, ChronoUnit.SECONDS));
381             sb.append(" seconds");
382             mEphemeralKeyLifetimeTimer = new Timer();
383             mEphemeralKeyLifetimeTimer.schedule(
384                     new TimerTask() {
385                         @Override
386                         public void run() {
387                             mMainExecutor.execute(() -> updateEphemeralKeyStatus());
388                         }
389                     },
390                     1000L /* delay in millis */);
391         }
392         mEphemeralKeyStateText.setText(sb.toString());
393     }
394 
updateNetworkInfo(LinkProperties linProperties)395     private void updateNetworkInfo(LinkProperties linProperties) {
396         if (linProperties == null) {
397             mTextNetworkInfo.setText("");
398             return;
399         }
400 
401         StringBuilder sb = new StringBuilder("Interface name:\n");
402         sb.append(linProperties.getInterfaceName() + "\n");
403         sb.append("Addresses:\n");
404         for (LinkAddress la : linProperties.getLinkAddresses()) {
405             sb.append(la + "\n");
406         }
407         sb.append("Routes:\n");
408         for (RouteInfo route : linProperties.getRoutes()) {
409             sb.append(route + "\n");
410         }
411         mTextNetworkInfo.setText(sb.toString());
412     }
413 
updateConfiguration(ThreadConfiguration config)414     private void updateConfiguration(ThreadConfiguration config) {
415         Log.i(TAG, "Updating configuration: " + config);
416 
417         mThreadConfiguration = config;
418         mNat64Switch.setChecked(config.isNat64Enabled());
419     }
420 }
421