Skip to content
Snippets Groups Projects
Commit a39db118 authored by Kangping Dong's avatar Kangping Dong Committed by Gerrit Code Review
Browse files

Merge "[Thread] add demoapp for Thread network APIs" into main

parents cc2cfd25 138b4b1c
No related branches found
No related tags found
No related merge requests found
Showing
with 1270 additions and 0 deletions
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
name: "ThreadNetworkDemoApp",
srcs: ["java/**/*.java"],
min_sdk_version: "34",
resource_dirs: ["res"],
static_libs: [
"androidx-constraintlayout_constraintlayout",
"androidx.appcompat_appcompat",
"androidx.navigation_navigation-common",
"androidx.navigation_navigation-fragment",
"androidx.navigation_navigation-ui",
"com.google.android.material_material",
"guava",
],
libs: [
"framework-connectivity-t",
],
certificate: "platform",
privileged: true,
platform_apis: true,
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.threadnetwork.demoapp">
<uses-sdk android:minSdkVersion="34" android:targetSdkVersion="35"/>
<uses-feature android:name="android.hardware.threadnetwork" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED" />
<application
android:label="ThreadNetworkDemoApp"
android:theme="@style/Theme.ThreadNetworkDemoApp"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:testOnly="true">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.threadnetwork.demoapp;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.android.threadnetwork.demoapp.concurrent.BackgroundExecutorProvider;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputEditText;
import com.google.common.io.CharStreams;
import com.google.common.net.InetAddresses;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
public final class ConnectivityToolsFragment extends Fragment {
private static final String TAG = "ConnectivityTools";
// This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
private static final Duration PING_TIMEOUT = Duration.ofSeconds(10L);
private static final Duration UDP_TIMEOUT = Duration.ofSeconds(10L);
private final ListeningScheduledExecutorService mBackgroundExecutor =
BackgroundExecutorProvider.getBackgroundExecutor();
private final ArrayList<String> mServerIpCandidates = new ArrayList<>();
private final ArrayList<String> mServerPortCandidates = new ArrayList<>();
private Executor mMainExecutor;
private ListenableFuture<String> mPingFuture;
private ListenableFuture<String> mUdpFuture;
private ArrayAdapter<String> mPingServerIpAdapter;
private ArrayAdapter<String> mUdpServerIpAdapter;
private ArrayAdapter<String> mUdpServerPortAdapter;
private Network mThreadNetwork;
private boolean mBindThreadNetwork = false;
private void subscribeToThreadNetwork() {
ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
cm.registerNetworkCallback(
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
mThreadNetwork = network;
}
@Override
public void onLost(Network network) {
mThreadNetwork = network;
}
},
new Handler(Looper.myLooper()));
}
private static String getPingCommand(String serverIp) {
try {
InetAddress serverAddress = InetAddresses.forString(serverIp);
return (serverAddress instanceof Inet6Address)
? "/system/bin/ping6"
: "/system/bin/ping";
} catch (IllegalArgumentException e) {
// The ping command can handle the illegal argument and output error message
return "/system/bin/ping6";
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.connectivity_tools_fragment, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mMainExecutor = ContextCompat.getMainExecutor(getActivity());
subscribeToThreadNetwork();
AutoCompleteTextView pingServerIpText = view.findViewById(R.id.ping_server_ip_address_text);
mPingServerIpAdapter =
new ArrayAdapter<String>(
getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
pingServerIpText.setAdapter(mPingServerIpAdapter);
TextView pingOutputText = view.findViewById(R.id.ping_output_text);
Button pingButton = view.findViewById(R.id.ping_button);
pingButton.setOnClickListener(
v -> {
if (mPingFuture != null) {
mPingFuture.cancel(/* mayInterruptIfRunning= */ true);
mPingFuture = null;
}
String serverIp = pingServerIpText.getText().toString().strip();
updateServerIpCandidates(serverIp);
pingOutputText.setText("Sending ping message to " + serverIp + "\n");
mPingFuture = sendPing(serverIp);
Futures.addCallback(
mPingFuture,
new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
pingOutputText.append(result + "\n");
}
@Override
public void onFailure(Throwable t) {
if (t instanceof CancellationException) {
// Ignore the cancellation error
return;
}
pingOutputText.append("Failed: " + t.getMessage() + "\n");
}
},
mMainExecutor);
});
AutoCompleteTextView udpServerIpText = view.findViewById(R.id.udp_server_ip_address_text);
mUdpServerIpAdapter =
new ArrayAdapter<String>(
getActivity(), R.layout.list_server_ip_address_view, mServerIpCandidates);
udpServerIpText.setAdapter(mUdpServerIpAdapter);
AutoCompleteTextView udpServerPortText = view.findViewById(R.id.udp_server_port_text);
mUdpServerPortAdapter =
new ArrayAdapter<String>(
getActivity(), R.layout.list_server_port_view, mServerPortCandidates);
udpServerPortText.setAdapter(mUdpServerPortAdapter);
TextInputEditText udpMsgText = view.findViewById(R.id.udp_message_text);
TextView udpOutputText = view.findViewById(R.id.udp_output_text);
SwitchMaterial switchBindThreadNetwork = view.findViewById(R.id.switch_bind_thread_network);
switchBindThreadNetwork.setChecked(mBindThreadNetwork);
switchBindThreadNetwork.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
Log.i(TAG, "Binding to the Thread network");
if (mThreadNetwork == null) {
Log.e(TAG, "Thread network is not available");
Toast.makeText(
getActivity().getApplicationContext(),
"Thread network is not available",
Toast.LENGTH_LONG);
switchBindThreadNetwork.setChecked(false);
} else {
mBindThreadNetwork = true;
}
} else {
mBindThreadNetwork = false;
}
});
Button sendUdpButton = view.findViewById(R.id.send_udp_button);
sendUdpButton.setOnClickListener(
v -> {
if (mUdpFuture != null) {
mUdpFuture.cancel(/* mayInterruptIfRunning= */ true);
mUdpFuture = null;
}
String serverIp = udpServerIpText.getText().toString().strip();
String serverPort = udpServerPortText.getText().toString().strip();
String udpMsg = udpMsgText.getText().toString().strip();
updateServerIpCandidates(serverIp);
updateServerPortCandidates(serverPort);
udpOutputText.setText(
String.format(
"Sending UDP message \"%s\" to [%s]:%s",
udpMsg, serverIp, serverPort));
mUdpFuture = sendUdpMessage(serverIp, serverPort, udpMsg);
Futures.addCallback(
mUdpFuture,
new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
udpOutputText.append("\n" + result);
}
@Override
public void onFailure(Throwable t) {
if (t instanceof CancellationException) {
// Ignore the cancellation error
return;
}
udpOutputText.append("\nFailed: " + t.getMessage());
}
},
mMainExecutor);
});
}
private void updateServerIpCandidates(String newServerIp) {
if (!mServerIpCandidates.contains(newServerIp)) {
mServerIpCandidates.add(0, newServerIp);
mPingServerIpAdapter.notifyDataSetChanged();
mUdpServerIpAdapter.notifyDataSetChanged();
}
}
private void updateServerPortCandidates(String newServerPort) {
if (!mServerPortCandidates.contains(newServerPort)) {
mServerPortCandidates.add(0, newServerPort);
mUdpServerPortAdapter.notifyDataSetChanged();
}
}
private ListenableFuture<String> sendPing(String serverIp) {
return FluentFuture.from(Futures.submit(() -> doSendPing(serverIp), mBackgroundExecutor))
.withTimeout(PING_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
}
private String doSendPing(String serverIp) throws IOException {
String pingCommand = getPingCommand(serverIp);
Process process =
new ProcessBuilder()
.command(pingCommand, "-c 1", serverIp)
.redirectErrorStream(true)
.start();
return CharStreams.toString(new InputStreamReader(process.getInputStream()));
}
private ListenableFuture<String> sendUdpMessage(
String serverIp, String serverPort, String msg) {
return FluentFuture.from(
Futures.submit(
() -> doSendUdpMessage(serverIp, serverPort, msg),
mBackgroundExecutor))
.withTimeout(UDP_TIMEOUT.getSeconds(), TimeUnit.SECONDS, mBackgroundExecutor);
}
private String doSendUdpMessage(String serverIp, String serverPort, String msg)
throws IOException {
SocketAddress serverAddr = new InetSocketAddress(serverIp, Integer.parseInt(serverPort));
try (DatagramSocket socket = new DatagramSocket()) {
if (mBindThreadNetwork && mThreadNetwork != null) {
mThreadNetwork.bindSocket(socket);
Log.i(TAG, "Successfully bind the socket to the Thread network");
}
socket.connect(serverAddr);
Log.d(TAG, "connected " + serverAddr);
byte[] msgBytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
Log.d(TAG, String.format("Sending message to server %s: %s", serverAddr, msg));
socket.send(packet);
Log.d(TAG, "Send done");
Log.d(TAG, "Waiting for server reply");
socket.receive(packet);
return new String(packet.getData(), packet.getOffset(), packet.getLength(), UTF_8);
}
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.threadnetwork.demoapp;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.navigation.NavigationView;
public final class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
NavHostFragment navHostFragment =
(NavHostFragment)
getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
Toolbar topAppBar = findViewById(R.id.top_app_bar);
AppBarConfiguration appBarConfig =
new AppBarConfiguration.Builder(navController.getGraph())
.setOpenableLayout(drawerLayout)
.build();
NavigationUI.setupWithNavController(topAppBar, navController, appBarConfig);
NavigationView navView = findViewById(R.id.nav_view);
NavigationUI.setupWithNavController(navView, navController);
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.threadnetwork.demoapp;
import static com.google.common.io.BaseEncoding.base16;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.RouteInfo;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executor;
public final class ThreadNetworkSettingsFragment extends Fragment {
private static final String TAG = "ThreadNetworkSettings";
// This is a mirror of NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK which is @hide for now
private static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
private ThreadNetworkController mThreadController;
private TextView mTextState;
private TextView mTextNetworkInfo;
private TextView mMigrateNetworkState;
private Executor mMainExecutor;
private int mDeviceRole;
private long mPartitionId;
private ActiveOperationalDataset mActiveDataset;
private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
base16().lowerCase()
.decode(
"0e080000000000010000000300001235060004001fffe00208dae21bccb8c321c40708fdc376ead74396bb0510c52f56cd2d38a9eb7a716954f8efd939030f4f70656e5468726561642d646231390102db190410fcb737e6fd6bb1b0fed524a4496363110c0402a0f7f8");
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
private static String deviceRoleToString(int mDeviceRole) {
switch (mDeviceRole) {
case ThreadNetworkController.DEVICE_ROLE_STOPPED:
return "Stopped";
case ThreadNetworkController.DEVICE_ROLE_DETACHED:
return "Detached";
case ThreadNetworkController.DEVICE_ROLE_CHILD:
return "Child";
case ThreadNetworkController.DEVICE_ROLE_ROUTER:
return "Router";
case ThreadNetworkController.DEVICE_ROLE_LEADER:
return "Leader";
default:
return "Unknown";
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.thread_network_settings_fragment, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ConnectivityManager cm = getActivity().getSystemService(ConnectivityManager.class);
cm.registerNetworkCallback(
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
Log.i(TAG, "New Thread network is available");
}
@Override
public void onLinkPropertiesChanged(
Network network, LinkProperties linkProperties) {
updateNetworkInfo(linkProperties);
}
@Override
public void onLost(Network network) {
Log.i(TAG, "Thread network " + network + " is lost");
updateNetworkInfo(null /* linkProperties */);
}
},
new Handler(Looper.myLooper()));
mMainExecutor = ContextCompat.getMainExecutor(getActivity());
ThreadNetworkManager threadManager =
getActivity().getSystemService(ThreadNetworkManager.class);
if (threadManager != null) {
mThreadController = threadManager.getAllThreadNetworkControllers().get(0);
mThreadController.registerStateCallback(
mMainExecutor,
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int mDeviceRole) {
ThreadNetworkSettingsFragment.this.mDeviceRole = mDeviceRole;
updateState();
}
@Override
public void onPartitionIdChanged(long mPartitionId) {
ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId;
updateState();
}
});
mThreadController.registerOperationalDatasetCallback(
mMainExecutor,
newActiveDataset -> {
this.mActiveDataset = newActiveDataset;
updateState();
});
}
mTextState = (TextView) view.findViewById(R.id.text_state);
mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info);
if (mThreadController == null) {
mTextState.setText("Thread not supported!");
return;
}
((Button) view.findViewById(R.id.button_join_network)).setOnClickListener(v -> doJoin());
((Button) view.findViewById(R.id.button_leave_network)).setOnClickListener(v -> doLeave());
mMigrateNetworkState = view.findViewById(R.id.text_migrate_network_state);
((Button) view.findViewById(R.id.button_migrate_network))
.setOnClickListener(v -> doMigration());
updateState();
}
private void doJoin() {
mThreadController.join(
DEFAULT_ACTIVE_DATASET,
mMainExecutor,
new OutcomeReceiver<Void, ThreadNetworkException>() {
@Override
public void onError(ThreadNetworkException error) {
Log.e(TAG, "Failed to join network " + DEFAULT_ACTIVE_DATASET, error);
}
@Override
public void onResult(Void v) {
Log.i(TAG, "Successfully Joined");
}
});
}
private void doLeave() {
mThreadController.leave(
mMainExecutor,
new OutcomeReceiver<>() {
@Override
public void onError(ThreadNetworkException error) {
Log.e(TAG, "Failed to leave network " + DEFAULT_ACTIVE_DATASET, error);
}
@Override
public void onResult(Void v) {
Log.i(TAG, "Successfully Left");
}
});
}
private void doMigration() {
var newActiveDataset =
new ActiveOperationalDataset.Builder(DEFAULT_ACTIVE_DATASET)
.setNetworkName("NewThreadNet")
.setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
.build();
var pendingDataset =
new PendingOperationalDataset(
newActiveDataset,
OperationalDatasetTimestamp.fromInstant(Instant.now()),
Duration.ofSeconds(30));
mThreadController.scheduleMigration(
pendingDataset,
mMainExecutor,
new OutcomeReceiver<Void, ThreadNetworkException>() {
@Override
public void onResult(Void v) {
mMigrateNetworkState.setText(
"Scheduled migration to network \"NewThreadNet\" in 30s");
// TODO: update Pending Dataset state
}
@Override
public void onError(ThreadNetworkException e) {
mMigrateNetworkState.setText(
"Failed to schedule migration: " + e.getMessage());
}
});
}
private void updateState() {
Log.i(
TAG,
String.format(
"Updating Thread states (mDeviceRole: %s)",
deviceRoleToString(mDeviceRole)));
String state =
String.format(
"Role %s\n"
+ "Partition ID %d\n"
+ "Network Name %s\n"
+ "Extended PAN ID %s",
deviceRoleToString(mDeviceRole),
mPartitionId,
mActiveDataset != null ? mActiveDataset.getNetworkName() : null,
mActiveDataset != null
? base16().encode(mActiveDataset.getExtendedPanId())
: null);
mTextState.setText(state);
}
private void updateNetworkInfo(LinkProperties linProperties) {
if (linProperties == null) {
mTextNetworkInfo.setText("");
return;
}
StringBuilder sb = new StringBuilder("Interface name:\n");
sb.append(linProperties.getInterfaceName() + "\n");
sb.append("Addresses:\n");
for (LinkAddress la : linProperties.getLinkAddresses()) {
sb.append(la + "\n");
}
sb.append("Routes:\n");
for (RouteInfo route : linProperties.getRoutes()) {
sb.append(route + "\n");
}
mTextNetworkInfo.setText(sb.toString());
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.threadnetwork.demoapp.concurrent;
import androidx.annotation.GuardedBy;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.Executors;
/** Provides executors for executing tasks in background. */
public final class BackgroundExecutorProvider {
private static final int CONCURRENCY = 4;
@GuardedBy("BackgroundExecutorProvider.class")
private static ListeningScheduledExecutorService backgroundExecutor;
private BackgroundExecutorProvider() {}
public static synchronized ListeningScheduledExecutorService getBackgroundExecutor() {
if (backgroundExecutor == null) {
backgroundExecutor =
MoreExecutors.listeningDecorator(
Executors.newScheduledThreadPool(/* maxConcurrency= */ CONCURRENCY));
}
return backgroundExecutor;
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.0612"
android:scaleY="0.0612"
android:translateX="23.4"
android:translateY="23.683332">
<path
android:pathData="M0,0h1000v1000h-1000z"
android:fillColor="#00FFCEC7"/>
<path
android:pathData="m630.6,954.5l-113.5,0l0,-567.2l-170.5,0c-50.6,0 -92,41.2 -92,91.9c0,50.6 41.4,91.8 92,91.8l0,113.5c-113.3,0 -205.5,-92.1 -205.5,-205.4c0,-113.3 92.2,-205.5 205.5,-205.5l170.5,0l0,-57.5c0,-94.2 76.7,-171 171.1,-171c94.2,0 170.8,76.7 170.8,171c0,94.2 -76.6,171 -170.8,171l-57.6,0l0,567.2zM630.6,273.9l57.6,0c31.7,0 57.3,-25.8 57.3,-57.5c0,-31.7 -25.7,-57.5 -57.3,-57.5c-31.8,0 -57.6,25.8 -57.6,57.5l0,57.5z"
android:strokeLineJoin="miter"
android:strokeWidth="0"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"
android:strokeLineCap="butt"/>
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dp"
android:height="24.0dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3.0,18.0l18.0,0.0l0.0,-2.0L3.0,16.0l0.0,2.0zm0.0,-5.0l18.0,0.0l0.0,-2.0L3.0,11.0l0.0,2.0zm0.0,-7.0l0.0,2.0l18.0,0.0L21.0,6.0L3.0,6.0z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="167dp"
android:height="31dp"
android:viewportWidth="167"
android:viewportHeight="31">
<path
android:pathData="m32.413,7.977 l3.806,0 0,9.561 11.48,0 0,-9.561 3.837,0 0,22.957 -3.837,0 0,-9.558 -11.48,0 0,9.558 -3.806,0 0,-22.957z"
android:fillColor="#ffffff"/>
<path
android:pathData="m76.761,30.934 l-4.432,-7.641 -6.483,0 0,7.641 -3.807,0 0,-22.957 11.48,0c2.095,0 3.894,0.75 5.392,2.246 1.501,1.504 2.249,3.298 2.249,5.392 0,1.591 -0.453,3.034 -1.356,4.335 -0.885,1.279 -2.006,2.193 -3.376,2.747l4.732,8.236 -4.4,0zM73.519,11.812l-7.673,0 0,7.645 7.673,0c1.034,0 1.928,-0.379 2.678,-1.124 0.75,-0.752 1.126,-1.657 1.126,-2.717 0,-1.034 -0.376,-1.926 -1.126,-2.678C75.448,12.188 74.554,11.812 73.519,11.812Z"
android:fillColor="#ffffff"/>
<path
android:pathData="m106.945,7.977 l0,3.835 -11.478,0 0,5.757 11.478,0 0,3.807 -11.478,0 0,5.722 11.478,0 0,3.836 -15.277,0 0,-22.957 15.277,0z"
android:fillColor="#ffffff"/>
<path
android:pathData="m132.325,27.08 l-10.586,0 -1.958,3.854 -4.283,0 11.517,-23.519 11.522,23.519 -4.283,0 -1.928,-3.854zM123.627,23.267 L130.404,23.267 127.014,16.013 123.627,23.267z"
android:fillColor="#ffffff"/>
<path
android:pathData="m146.606,7.977 l7.638,0c1.569,0 3.044,0.304 4.436,0.907 1.387,0.609 2.608,1.437 3.656,2.485 1.047,1.047 1.869,2.266 2.479,3.653 0.609,1.391 0.909,2.866 0.909,4.435 0,1.563 -0.299,3.041 -0.909,4.432 -0.61,1.391 -1.425,2.608 -2.464,3.654 -1.037,1.05 -2.256,1.874 -3.656,2.48 -1.401,0.607 -2.882,0.91 -4.451,0.91l-7.638,0 0,-22.956zM154.244,27.098c1.06,0 2.054,-0.199 2.978,-0.599 0.925,-0.394 1.737,-0.945 2.432,-1.654 0.696,-0.702 1.241,-1.521 1.638,-2.446 0.397,-0.925 0.597,-1.907 0.597,-2.942 0,-1.037 -0.201,-2.02 -0.597,-2.948 -0.397,-0.925 -0.946,-1.737 -1.651,-2.447 -0.709,-0.703 -1.524,-1.256 -2.45,-1.653 -0.925,-0.397 -1.907,-0.597 -2.948,-0.597l-3.834,0 0,15.286 3.834,0z"
android:fillColor="#ffffff"/>
<path
android:pathData="m16.491,30.934 l-3.828,0 0,-19.128 -5.749,0c-1.705,0 -3.102,1.391 -3.102,3.1 0,1.706 1.397,3.097 3.102,3.097l0,3.83c-3.821,0 -6.931,-3.106 -6.931,-6.926 0,-3.822 3.111,-6.929 6.931,-6.929l5.749,0 0,-1.938c0,-3.179 2.587,-5.766 5.77,-5.766 3.175,0 5.76,2.588 5.76,5.766 0,3.179 -2.584,5.766 -5.76,5.766l-1.942,0 0,19.128zM16.491,7.977 L18.433,7.977c1.069,0 1.934,-0.869 1.934,-1.938 0,-1.069 -0.865,-1.938 -1.934,-1.938 -1.072,0 -1.942,0.869 -1.942,1.938l0,1.938z"
android:fillColor="#ffffff"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ConnectivityToolsFragment" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ping_server_ip_address_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Server IP Address">
<AutoCompleteTextView
android:id="@+id/ping_server_ip_address_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="fdde:ad00:beef::ff:fe00:7400"
android:textSize="14sp"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/ping_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Ping"
android:textSize="20dp"/>
<TextView
android:id="@+id/ping_output_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:scrollbars="vertical"
android:textIsSelectable="true"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal" >
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/udp_server_ip_address_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Server IP Address">
<AutoCompleteTextView
android:id="@+id/udp_server_ip_address_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="fdde:ad00:beef::ff:fe00:7400"
android:textSize="14sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/udp_server_port_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:hint="Server Port">
<AutoCompleteTextView
android:id="@+id/udp_server_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="number"
android:text="12345"
android:textSize="14sp"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/udp_message_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="UDP Message">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/udp_message_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Thread!"
android:textSize="14sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_bind_thread_network"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="Bind to Thread network" />
<Button
android:id="@+id/send_udp_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Send UDP Message"
android:textSize="20dp"/>
<TextView
android:id="@+id/udp_output_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:scrollbars="vertical"
android:textIsSelectable="true"/>
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_menu_24dp" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</LinearLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="16dp"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"
android:orientation="vertical" >
<ImageView
android:id="@+id/nav_header_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:src="@drawable/ic_thread_wordmark" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:orientation="vertical"
tools:context=".ThreadNetworkSettingsFragment" >
<Button android:id="@+id/button_join_network"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Join Network" />
<Button android:id="@+id/button_leave_network"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Leave Network" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16dp"
android:textStyle="bold"
android:text="State" />
<TextView
android:id="@+id/text_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="16dp"
android:textStyle="bold"
android:text="Network Info" />
<TextView
android:id="@+id/text_network_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp" />
<Button android:id="@+id/button_migrate_network"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Migrate Network" />
<TextView
android:id="@+id/text_migrate_network_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/thread_network_settings"
android:title="Thread Network Settings" />
<item
android:id="@+id/connectivity_tools"
android:title="Connectivity Tools" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
thread/demoapp/res/mipmap-hdpi/ic_launcher.png

1.29 KiB

thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png

2.48 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment