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