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.ThreadNetworkController; 32 import android.net.thread.ThreadNetworkException; 33 import android.net.thread.ThreadNetworkManager; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.os.OutcomeReceiver; 38 import android.util.Log; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.Button; 43 import android.widget.TextView; 44 45 import androidx.core.content.ContextCompat; 46 import androidx.fragment.app.Fragment; 47 48 import java.time.Duration; 49 import java.time.Instant; 50 import java.util.concurrent.Executor; 51 52 public final class ThreadNetworkSettingsFragment extends Fragment { 53 private static final String TAG = "ThreadNetworkSettings"; 54 55 // This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now 56 private static final int NET_CAPABILITY_LOCAL_NETWORK = 36; 57 58 private ThreadNetworkController mThreadController; 59 private TextView mTextState; 60 private TextView mTextNetworkInfo; 61 private TextView mMigrateNetworkState; 62 private Executor mMainExecutor; 63 64 private int mDeviceRole; 65 private long mPartitionId; 66 private ActiveOperationalDataset mActiveDataset; 67 68 private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS = 69 base16().lowerCase() 70 .decode( 71 "0e080000000000010000000300001235060004001fffe00208dae21bccb8c321c40708fdc376ead74396bb0510c52f56cd2d38a9eb7a716954f8efd939030f4f70656e5468726561642d646231390102db190410fcb737e6fd6bb1b0fed524a4496363110c0402a0f7f8"); 72 private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET = 73 ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS); 74 deviceRoleToString(int mDeviceRole)75 private static String deviceRoleToString(int mDeviceRole) { 76 switch (mDeviceRole) { 77 case ThreadNetworkController.DEVICE_ROLE_STOPPED: 78 return "Stopped"; 79 case ThreadNetworkController.DEVICE_ROLE_DETACHED: 80 return "Detached"; 81 case ThreadNetworkController.DEVICE_ROLE_CHILD: 82 return "Child"; 83 case ThreadNetworkController.DEVICE_ROLE_ROUTER: 84 return "Router"; 85 case ThreadNetworkController.DEVICE_ROLE_LEADER: 86 return "Leader"; 87 default: 88 return "Unknown"; 89 } 90 } 91 92 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)93 public View onCreateView( 94 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 95 return inflater.inflate(R.layout.thread_network_settings_fragment, container, false); 96 } 97 98 @Override onViewCreated(View view, Bundle savedInstanceState)99 public void onViewCreated(View view, Bundle savedInstanceState) { 100 super.onViewCreated(view, savedInstanceState); 101 102 ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class); 103 cm.registerNetworkCallback( 104 new NetworkRequest.Builder() 105 .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) 106 .addCapability(NET_CAPABILITY_LOCAL_NETWORK) 107 .build(), 108 new ConnectivityManager.NetworkCallback() { 109 @Override 110 public void onAvailable(Network network) { 111 Log.i(TAG, "New Thread network is available"); 112 } 113 114 @Override 115 public void onLinkPropertiesChanged( 116 Network network, LinkProperties linkProperties) { 117 updateNetworkInfo(linkProperties); 118 } 119 120 @Override 121 public void onLost(Network network) { 122 Log.i(TAG, "Thread network " + network + " is lost"); 123 updateNetworkInfo(null /* linkProperties */); 124 } 125 }, 126 new Handler(Looper.myLooper())); 127 128 mMainExecutor = ContextCompat.getMainExecutor(getActivity()); 129 ThreadNetworkManager threadManager = 130 getActivity().getSystemService(ThreadNetworkManager.class); 131 if (threadManager != null) { 132 mThreadController = threadManager.getAllThreadNetworkControllers().get(0); 133 mThreadController.registerStateCallback( 134 mMainExecutor, 135 new ThreadNetworkController.StateCallback() { 136 @Override 137 public void onDeviceRoleChanged(int mDeviceRole) { 138 ThreadNetworkSettingsFragment.this.mDeviceRole = mDeviceRole; 139 updateState(); 140 } 141 142 @Override 143 public void onPartitionIdChanged(long mPartitionId) { 144 ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId; 145 updateState(); 146 } 147 }); 148 mThreadController.registerOperationalDatasetCallback( 149 mMainExecutor, 150 newActiveDataset -> { 151 this.mActiveDataset = newActiveDataset; 152 updateState(); 153 }); 154 } 155 156 mTextState = (TextView) view.findViewById(R.id.text_state); 157 mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info); 158 159 if (mThreadController == null) { 160 mTextState.setText("Thread not supported!"); 161 return; 162 } 163 164 ((Button) view.findViewById(R.id.button_join_network)).setOnClickListener(v -> doJoin()); 165 ((Button) view.findViewById(R.id.button_leave_network)).setOnClickListener(v -> doLeave()); 166 167 mMigrateNetworkState = view.findViewById(R.id.text_migrate_network_state); 168 ((Button) view.findViewById(R.id.button_migrate_network)) 169 .setOnClickListener(v -> doMigration()); 170 171 updateState(); 172 } 173 doJoin()174 private void doJoin() { 175 mThreadController.join( 176 DEFAULT_ACTIVE_DATASET, 177 mMainExecutor, 178 new OutcomeReceiver<Void, ThreadNetworkException>() { 179 @Override 180 public void onError(ThreadNetworkException error) { 181 Log.e(TAG, "Failed to join network " + DEFAULT_ACTIVE_DATASET, error); 182 } 183 184 @Override 185 public void onResult(Void v) { 186 Log.i(TAG, "Successfully Joined"); 187 } 188 }); 189 } 190 doLeave()191 private void doLeave() { 192 mThreadController.leave( 193 mMainExecutor, 194 new OutcomeReceiver<>() { 195 @Override 196 public void onError(ThreadNetworkException error) { 197 Log.e(TAG, "Failed to leave network " + DEFAULT_ACTIVE_DATASET, error); 198 } 199 200 @Override 201 public void onResult(Void v) { 202 Log.i(TAG, "Successfully Left"); 203 } 204 }); 205 } 206 doMigration()207 private void doMigration() { 208 var newActiveDataset = 209 new ActiveOperationalDataset.Builder(DEFAULT_ACTIVE_DATASET) 210 .setNetworkName("NewThreadNet") 211 .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now())) 212 .build(); 213 var pendingDataset = 214 new PendingOperationalDataset( 215 newActiveDataset, 216 OperationalDatasetTimestamp.fromInstant(Instant.now()), 217 Duration.ofSeconds(30)); 218 mThreadController.scheduleMigration( 219 pendingDataset, 220 mMainExecutor, 221 new OutcomeReceiver<Void, ThreadNetworkException>() { 222 @Override 223 public void onResult(Void v) { 224 mMigrateNetworkState.setText( 225 "Scheduled migration to network \"NewThreadNet\" in 30s"); 226 // TODO: update Pending Dataset state 227 } 228 229 @Override 230 public void onError(ThreadNetworkException e) { 231 mMigrateNetworkState.setText( 232 "Failed to schedule migration: " + e.getMessage()); 233 } 234 }); 235 } 236 updateState()237 private void updateState() { 238 Log.i( 239 TAG, 240 String.format( 241 "Updating Thread states (mDeviceRole: %s)", 242 deviceRoleToString(mDeviceRole))); 243 244 String state = 245 String.format( 246 "Role %s\n" 247 + "Partition ID %d\n" 248 + "Network Name %s\n" 249 + "Extended PAN ID %s", 250 deviceRoleToString(mDeviceRole), 251 mPartitionId, 252 mActiveDataset != null ? mActiveDataset.getNetworkName() : null, 253 mActiveDataset != null 254 ? base16().encode(mActiveDataset.getExtendedPanId()) 255 : null); 256 mTextState.setText(state); 257 } 258 updateNetworkInfo(LinkProperties linProperties)259 private void updateNetworkInfo(LinkProperties linProperties) { 260 if (linProperties == null) { 261 mTextNetworkInfo.setText(""); 262 return; 263 } 264 265 StringBuilder sb = new StringBuilder("Interface name:\n"); 266 sb.append(linProperties.getInterfaceName() + "\n"); 267 sb.append("Addresses:\n"); 268 for (LinkAddress la : linProperties.getLinkAddresses()) { 269 sb.append(la + "\n"); 270 } 271 sb.append("Routes:\n"); 272 for (RouteInfo route : linProperties.getRoutes()) { 273 sb.append(route + "\n"); 274 } 275 mTextNetworkInfo.setText(sb.toString()); 276 } 277 } 278