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