Merge "[XFRM_MSG_GETSA] Support XFRM_MSG_GETSA message" into main
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 50d6c4b..5e9bbcb 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,6 +18,7 @@
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
@@ -38,12 +39,21 @@
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.FdEventsReader;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.LlaOption;
@@ -103,6 +113,11 @@
private static final int DAY_IN_SECONDS = 86_400;
+ // Commands for IpServer to control RouterAdvertisementDaemon
+ private static final int CMD_START = 1;
+ private static final int CMD_STOP = 2;
+ private static final int CMD_BUILD_NEW_RA = 3;
+
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -120,9 +135,13 @@
@GuardedBy("mLock")
private RaParams mRaParams;
+ // To be accessed only from RaMessageHandler
+ private RsPacketListener mRsPacketListener;
+
private volatile FileDescriptor mSocket;
private volatile MulticastTransmitter mMulticastTransmitter;
- private volatile UnicastResponder mUnicastResponder;
+ private volatile RaMessageHandler mRaMessageHandler;
+ private volatile HandlerThread mRaHandlerThread;
/** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
public static class RaParams {
@@ -244,6 +263,94 @@
}
}
+ private class RaMessageHandler extends Handler {
+ RaMessageHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_START:
+ mRsPacketListener = new RsPacketListener(this);
+ mRsPacketListener.start();
+ break;
+ case CMD_STOP:
+ if (mRsPacketListener != null) {
+ mRsPacketListener.stop();
+ mRsPacketListener = null;
+ }
+ break;
+ case CMD_BUILD_NEW_RA:
+ synchronized (mLock) {
+ // raInfo.first is deprecatedParams and raInfo.second is newParams.
+ final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
+ if (raInfo.first != null) {
+ mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
+ mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
+ }
+
+ if (raInfo.second != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
+ mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
+ }
+ mRaParams = raInfo.second;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
+ break;
+ default:
+ Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
+ break;
+ }
+ }
+ }
+
+ private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
+ private static final class RecvBuffer {
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ final byte[] mBytes = new byte[IPV6_MIN_MTU];
+ final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
+ }
+
+ RsPacketListener(@NonNull Handler handler) {
+ super(handler, new RecvBuffer());
+ }
+
+ @Override
+ protected int recvBufSize(@NonNull RecvBuffer buffer) {
+ return buffer.mBytes.length;
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mSocket;
+ }
+
+ @Override
+ protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
+ throws Exception {
+ return Os.recvfrom(
+ fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
+ }
+
+ @Override
+ protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
+ // Do the least possible amount of validations.
+ if (buffer.mSrcAddr == null
+ || length <= 0
+ || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ return;
+ }
+
+ maybeSendRA(buffer.mSrcAddr);
+ }
+ }
+
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -252,48 +359,43 @@
/** Build new RA.*/
public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
- synchronized (mLock) {
- if (deprecatedParams != null) {
- mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
- mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
- }
-
- if (newParams != null) {
- // Process information that is no longer deprecated.
- mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
- mDeprecatedInfoTracker.removeDnses(newParams.dnses);
- }
-
- mRaParams = newParams;
- assembleRaLocked();
- }
-
- maybeNotifyMulticastTransmitter();
+ final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
+ sendMessage(CMD_BUILD_NEW_RA, raInfo);
}
/** Start router advertisement daemon. */
public boolean start() {
if (!createSocket()) {
+ Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
return false;
}
mMulticastTransmitter = new MulticastTransmitter();
mMulticastTransmitter.start();
- mUnicastResponder = new UnicastResponder();
- mUnicastResponder.start();
+ mRaHandlerThread = new HandlerThread(TAG);
+ mRaHandlerThread.start();
+ mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
- return true;
+ return sendMessage(CMD_START);
}
/** Stop router advertisement daemon. */
public void stop() {
+ if (!sendMessage(CMD_STOP)) {
+ Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
+ return;
+ }
+
+ mRaHandlerThread.quitSafely();
+ mRaHandlerThread = null;
+ mRaMessageHandler = null;
+
closeSocket();
// Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
// the thread's termination.
maybeNotifyMulticastTransmitter();
mMulticastTransmitter = null;
- mUnicastResponder = null;
}
@GuardedBy("mLock")
@@ -503,7 +605,7 @@
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
- mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -565,34 +667,17 @@
}
}
- private final class UnicastResponder extends Thread {
- private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
- // The recycled buffer for receiving Router Solicitations from clients.
- // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
- // This is fine since currently only byte 0 is examined anyway.
- private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
+ private boolean sendMessage(int cmd) {
+ return sendMessage(cmd, null);
+ }
- @Override
- public void run() {
- while (isSocketValid()) {
- try {
- // Blocking receive.
- final int rval = Os.recvfrom(
- mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
- // Do the least possible amount of validation.
- if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
- continue;
- }
- } catch (ErrnoException | SocketException e) {
- if (isSocketValid()) {
- Log.e(TAG, "recvfrom error: " + e);
- }
- continue;
- }
-
- maybeSendRA(mSolicitor);
- }
+ private boolean sendMessage(int cmd, @Nullable Object obj) {
+ if (mRaMessageHandler == null) {
+ return false;
}
+
+ return mRaMessageHandler.sendMessage(
+ Message.obtain(mRaMessageHandler, cmd, obj));
}
// TODO: Consider moving this to run on a provided Looper as a Handler,
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 6c0ca82..6f3e865 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -79,6 +79,7 @@
private final TetheringConfiguration mConfig;
// keyed by downstream type(TetheringManager.TETHERING_*).
private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
+ private final Random mRandom;
public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
mDownstreams = new ArraySet<>();
@@ -95,6 +96,7 @@
mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
+ mRandom = new Random();
}
/**
@@ -263,12 +265,13 @@
// is less than 127.0.0.0 = 0x7f000000 = 2130706432.
//
// Additionally, it makes debug output easier to read by making the numbers smaller.
- final int randomPrefixStart = getRandomInt() & ~prefixRangeMask & prefixMask;
+ final int randomInt = getRandomInt();
+ final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
// A random offset within the prefix. Used to determine the local address once the prefix
// is selected. It does not result in an IPv4 address ending in .0, .1, or .255
- // For a PREFIX_LENGTH of 255, this is a number between 2 and 254.
- final int subAddress = getSanitizedSubAddr(~prefixMask);
+ // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
+ final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
// Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
// such that the prefix does not conflict with any upstream.
@@ -310,12 +313,12 @@
/** Get random int which could be used to generate random address. */
@VisibleForTesting
public int getRandomInt() {
- return (new Random()).nextInt();
+ return mRandom.nextInt();
}
/** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
- private int getSanitizedSubAddr(final int subAddrMask) {
- final int randomSubAddr = getRandomInt() & subAddrMask;
+ private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
+ final int randomSubAddr = randomInt & subAddrMask;
// If prefix length > 30, the selecting speace would be less than 4 which may be hard to
// avoid 3 consecutive address.
if (PREFIX_LENGTH > 30) return randomSubAddr;
diff --git a/common/flags.aconfig b/common/flags.aconfig
index ebfa13a..ad78d62 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -15,13 +15,6 @@
}
flag {
- name: "nsd_expired_services_removal"
- namespace: "android_core_networking"
- description: "Remove expired services from MdnsServiceCache"
- bug: "304649384"
-}
-
-flag {
name: "set_data_saver_via_cm"
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
diff --git a/service/ServiceConnectivityResources/res/values/strings.xml b/service/ServiceConnectivityResources/res/values/strings.xml
index b2fa5f5..246155e 100644
--- a/service/ServiceConnectivityResources/res/values/strings.xml
+++ b/service/ServiceConnectivityResources/res/values/strings.xml
@@ -29,6 +29,15 @@
<!-- A notification is shown when a captive portal network is detected. This is the notification's message. -->
<string name="network_available_sign_in_detailed"><xliff:g id="network_ssid">%1$s</xliff:g></string>
+ <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription). This is the notification's title. -->
+ <string name="mobile_network_available_no_internet">No internet</string>
+
+ <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription). This is the notification's message. -->
+ <string name="mobile_network_available_no_internet_detailed">You may be out of data from <xliff:g id="network_carrier" example="Android Mobile">%1$s</xliff:g>. Tap for options.</string>
+
+ <!-- A notification is shown when the system detected no internet access on a mobile network, possibly because the user is out of data, and a webpage is available to get Internet access (possibly by topping up or getting a subscription). This is the notification's message when the carrier is unknown. -->
+ <string name="mobile_network_available_no_internet_detailed_unknown_carrier">You may be out of data. Tap for options.</string>
+
<!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's title. -->
<string name="wifi_no_internet"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has no internet access</string>
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index bc13592..7707122 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -243,7 +243,7 @@
details = r.getString(R.string.network_available_sign_in_detailed, name);
break;
case TRANSPORT_CELLULAR:
- title = r.getString(R.string.network_available_sign_in, 0);
+ title = r.getString(R.string.mobile_network_available_no_internet);
// TODO: Change this to pull from NetworkInfo once a printable
// name has been added to it
NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
@@ -252,8 +252,16 @@
subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
}
- details = mTelephonyManager.createForSubscriptionId(subId)
+ final String operatorName = mTelephonyManager.createForSubscriptionId(subId)
.getNetworkOperatorName();
+ if (TextUtils.isEmpty(operatorName)) {
+ details = r.getString(R.string
+ .mobile_network_available_no_internet_detailed_unknown_carrier);
+ } else {
+ details = r.getString(
+ R.string.mobile_network_available_no_internet_detailed,
+ operatorName);
+ }
break;
default:
title = r.getString(R.string.network_available_sign_in, 0);
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index c473444..d94c8dc 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -17,8 +17,6 @@
package com.android.server.connectivity;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -223,19 +221,6 @@
}
/**
- * Returns whether the scorable has any of the PRIORITIZE_* capabilities.
- *
- * These capabilities code for customer slices, and a network that has one is a customer slice.
- */
- private boolean hasPrioritizedCapability(@NonNull final Scoreable nai) {
- final NetworkCapabilities caps = nai.getCapsNoCopy();
- final long anyPrioritizeCapability =
- (1L << NET_CAPABILITY_PRIORITIZE_LATENCY)
- | (1L << NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
- return 0 != (caps.getCapabilitiesInternal() & anyPrioritizeCapability);
- }
-
- /**
* Get the best network among a list of candidates according to policy.
* @param candidates the candidates
* @param currentSatisfier the current satisfier, or null if none
@@ -339,12 +324,6 @@
// change from the previous result. If there were, it's guaranteed candidates.size() > 0
// because accepted.size() > 0 above.
- // If any network is not a slice with prioritized bandwidth or latency, don't choose one
- // that is.
- partitionInto(candidates, nai -> !hasPrioritizedCapability(nai), accepted, rejected);
- if (accepted.size() == 1) return accepted.get(0);
- if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
-
// If some of the networks have a better transport than others, keep only the ones with
// the best transports.
for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index 90b7875..0ffe81e 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -36,6 +36,7 @@
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
<option name="force-skip-system-props" value="true" />
<option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+ <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
</target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 308aead..9ff0f2f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -860,4 +860,9 @@
assertEquals(DnsResolver.ERROR_SYSTEM, e.code);
}
}
+
+ @Test
+ public void testNoRawBinderAccess() {
+ assertNull(mContext.getSystemService("dnsresolver"));
+ }
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 9b082a4..496d163 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -255,7 +255,12 @@
na.connect()
testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
- assertEquals(2, nsInstrumentation.getRequestUrls().size)
+ val requestedSize = nsInstrumentation.getRequestUrls().size
+ if (requestedSize == 2 || (requestedSize == 1 &&
+ nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)) {
+ return
+ }
+ fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index b319c30..7121ed4 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -54,6 +54,7 @@
import android.content.res.Resources;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
+import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
@@ -107,12 +108,16 @@
private static final long TEST_TIMEOUT_MS = 10_000L;
private static final long UI_AUTOMATOR_WAIT_TIME_MILLIS = TEST_TIMEOUT_MS;
- static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
- static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
- static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+ private static final int TEST_SUB_ID = 43;
+ private static final String TEST_OPERATOR_NAME = "Test Operator";
+ private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
+ private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
+ private static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
static {
CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ CELL_CAPABILITIES.setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+ .setSubscriptionId(TEST_SUB_ID).build());
WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
@@ -149,6 +154,7 @@
@Mock DisplayMetrics mDisplayMetrics;
@Mock PackageManager mPm;
@Mock TelephonyManager mTelephonyManager;
+ @Mock TelephonyManager mTestSubIdTelephonyManager;
@Mock NotificationManager mNotificationManager;
@Mock NetworkAgentInfo mWifiNai;
@Mock NetworkAgentInfo mCellNai;
@@ -170,18 +176,21 @@
mVpnNai.networkInfo = mNetworkInfo;
mDisplayMetrics.density = 2.275f;
doReturn(true).when(mVpnNai).isVPN();
- when(mCtx.getResources()).thenReturn(mResources);
- when(mCtx.getPackageManager()).thenReturn(mPm);
- when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+ doReturn(mResources).when(mCtx).getResources();
+ doReturn(mPm).when(mCtx).getPackageManager();
+ doReturn(new ApplicationInfo()).when(mCtx).getApplicationInfo();
final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx));
doReturn(UserHandle.ALL).when(asUserCtx).getUser();
- when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
- when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
- .thenReturn(mNotificationManager);
- when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO);
+ doReturn(asUserCtx).when(mCtx).createContextAsUser(eq(UserHandle.ALL), anyInt());
+ doReturn(mNotificationManager).when(mCtx)
+ .getSystemService(eq(Context.NOTIFICATION_SERVICE));
+ doReturn(TEST_EXTRA_INFO).when(mNetworkInfo).getExtraInfo();
ConnectivityResources.setResourcesContextForTest(mCtx);
- when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
- when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
+ doReturn(0xFF607D8B).when(mResources).getColor(anyInt(), any());
+ doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
+ doReturn(mTestSubIdTelephonyManager).when(mTelephonyManager)
+ .createForSubscriptionId(TEST_SUB_ID);
+ doReturn(TEST_OPERATOR_NAME).when(mTestSubIdTelephonyManager).getNetworkOperatorName();
// Come up with some credible-looking transport names. The actual values do not matter.
String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1];
@@ -532,4 +541,44 @@
R.string.wifi_no_internet, TEST_EXTRA_INFO,
R.string.wifi_no_internet_detailed);
}
+
+ private void runTelephonySignInNotificationTest(String testTitle, String testContents) {
+ final int id = 101;
+ final String tag = NetworkNotificationManager.tagFor(id);
+ mManager.showNotification(id, SIGN_IN, mCellNai, null, null, false);
+
+ final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
+ verify(mNotificationManager).notify(eq(tag), eq(SIGN_IN.eventId), noteCaptor.capture());
+ final Bundle noteExtras = noteCaptor.getValue().extras;
+ assertEquals(testTitle, noteExtras.getString(Notification.EXTRA_TITLE));
+ assertEquals(testContents, noteExtras.getString(Notification.EXTRA_TEXT));
+ }
+
+ @Test
+ public void testTelephonySignInNotification() {
+ final String testTitle = "Telephony no internet title";
+ final String testContents = "Add data for " + TEST_OPERATOR_NAME;
+ // The test does not use real resources as they are in the ConnectivityResources package,
+ // which is tricky to use (requires resolving the package, QUERY_ALL_PACKAGES permission).
+ doReturn(testTitle).when(mResources).getString(
+ R.string.mobile_network_available_no_internet);
+ doReturn(testContents).when(mResources).getString(
+ R.string.mobile_network_available_no_internet_detailed, TEST_OPERATOR_NAME);
+
+ runTelephonySignInNotificationTest(testTitle, testContents);
+ }
+
+ @Test
+ public void testTelephonySignInNotification_NoOperator() {
+ doReturn("").when(mTestSubIdTelephonyManager).getNetworkOperatorName();
+
+ final String testTitle = "Telephony no internet title";
+ final String testContents = "Add data";
+ doReturn(testTitle).when(mResources).getString(
+ R.string.mobile_network_available_no_internet);
+ doReturn(testContents).when(mResources).getString(
+ R.string.mobile_network_available_no_internet_detailed_unknown_carrier);
+
+ runTelephonySignInNotificationTest(testTitle, testContents);
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 87f7369..1e3f389 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -18,12 +18,9 @@
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL as NET_CAP_PORTAL
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET as NET_CAP_INTERNET
-import android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH as NET_CAP_PRIO_BW
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkScore.KEEP_CONNECTED_NONE
-import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.net.NetworkScore.POLICY_EXITING as EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY as PRIMARY
import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI as YIELD_TO_BAD_WIFI
@@ -53,8 +50,8 @@
class NetworkRankerTest(private val activelyPreferBadWifi: Boolean) {
private val mRanker = NetworkRanker(NetworkRanker.Configuration(activelyPreferBadWifi))
- private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities) :
- NetworkRanker.Scoreable {
+ private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
+ : NetworkRanker.Scoreable {
override fun getScore() = sc
override fun getCapsNoCopy(): NetworkCapabilities = nc
}
@@ -199,41 +196,4 @@
val badExitingWifi = TestScore(score(EVER_EVALUATED, EVER_VALIDATED, EXITING), CAPS_WIFI)
assertEquals(cell, rank(cell, badExitingWifi))
}
-
- @Test
- fun testValidatedPolicyStrongerThanSlice() {
- val unvalidatedNonslice = TestScore(score(EVER_EVALUATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
- val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
- assertEquals(slice, rank(slice, unvalidatedNonslice))
- }
-
- @Test
- fun testPrimaryPolicyStrongerThanSlice() {
- val nonslice = TestScore(score(EVER_EVALUATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
- val primarySlice = TestScore(score(EVER_EVALUATED, POLICY_TRANSPORT_PRIMARY),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
- assertEquals(primarySlice, rank(nonslice, primarySlice))
- }
-
- @Test
- fun testPreferNonSlices() {
- // Slices lose to non-slices for general ranking
- val nonslice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
- val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
- assertEquals(nonslice, rank(slice, nonslice))
- }
-
- @Test
- fun testSlicePolicyStrongerThanTransport() {
- val nonSliceCell = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
- caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
- val sliceWifi = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
- caps(TRANSPORT_WIFI, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
- assertEquals(nonSliceCell, rank(nonSliceCell, sliceWifi))
- }
}
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
new file mode 100644
index 0000000..da7a5f8
--- /dev/null
+++ b/thread/demoapp/Android.bp
@@ -0,0 +1,39 @@
+// 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,
+}
diff --git a/thread/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
new file mode 100644
index 0000000..c31bb71
--- /dev/null
+++ b/thread/demoapp/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?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>
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
new file mode 100644
index 0000000..6f616eb
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ConnectivityToolsFragment.java
@@ -0,0 +1,317 @@
+/*
+ * 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);
+ }
+ }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
new file mode 100644
index 0000000..ef97a6c
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/MainActivity.java
@@ -0,0 +1,61 @@
+/*
+ * 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);
+ }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
new file mode 100644
index 0000000..e95feaf
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
@@ -0,0 +1,277 @@
+/*
+ * 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());
+ }
+}
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
new file mode 100644
index 0000000..d05ba73
--- /dev/null
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/concurrent/BackgroundExecutorProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+ }
+}
diff --git a/thread/demoapp/res/drawable/ic_launcher_foreground.xml b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..4dd8163
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,40 @@
+<?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>
diff --git a/thread/demoapp/res/drawable/ic_menu_24dp.xml b/thread/demoapp/res/drawable/ic_menu_24dp.xml
new file mode 100644
index 0000000..8a4cf80
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_menu_24dp.xml
@@ -0,0 +1,26 @@
+<?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>
diff --git a/thread/demoapp/res/drawable/ic_thread_wordmark.xml b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
new file mode 100644
index 0000000..babaf54
--- /dev/null
+++ b/thread/demoapp/res/drawable/ic_thread_wordmark.xml
@@ -0,0 +1,40 @@
+<?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>
diff --git a/thread/demoapp/res/layout/connectivity_tools_fragment.xml b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
new file mode 100644
index 0000000..a1aa0d4
--- /dev/null
+++ b/thread/demoapp/res/layout/connectivity_tools_fragment.xml
@@ -0,0 +1,137 @@
+<?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>
diff --git a/thread/demoapp/res/layout/list_server_ip_address_view.xml b/thread/demoapp/res/layout/list_server_ip_address_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_ip_address_view.xml
@@ -0,0 +1,25 @@
+<?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"
+ />
diff --git a/thread/demoapp/res/layout/list_server_port_view.xml b/thread/demoapp/res/layout/list_server_port_view.xml
new file mode 100644
index 0000000..1a8f02e
--- /dev/null
+++ b/thread/demoapp/res/layout/list_server_port_view.xml
@@ -0,0 +1,25 @@
+<?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"
+ />
diff --git a/thread/demoapp/res/layout/main_activity.xml b/thread/demoapp/res/layout/main_activity.xml
new file mode 100644
index 0000000..12072e5
--- /dev/null
+++ b/thread/demoapp/res/layout/main_activity.xml
@@ -0,0 +1,63 @@
+<?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>
diff --git a/thread/demoapp/res/layout/nav_header.xml b/thread/demoapp/res/layout/nav_header.xml
new file mode 100644
index 0000000..b91fb9c
--- /dev/null
+++ b/thread/demoapp/res/layout/nav_header.xml
@@ -0,0 +1,30 @@
+<?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>
diff --git a/thread/demoapp/res/layout/thread_network_settings_fragment.xml b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
new file mode 100644
index 0000000..cae46a3
--- /dev/null
+++ b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
@@ -0,0 +1,71 @@
+<?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>
diff --git a/thread/demoapp/res/menu/nav_menu.xml b/thread/demoapp/res/menu/nav_menu.xml
new file mode 100644
index 0000000..8d036c2
--- /dev/null
+++ b/thread/demoapp/res/menu/nav_menu.xml
@@ -0,0 +1,24 @@
+<?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>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?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>
diff --git a/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..b111e91
--- /dev/null
+++ b/thread/demoapp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+<?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>
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..94e778f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..074a671
--- /dev/null
+++ b/thread/demoapp/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..674e51f
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4e35c29
--- /dev/null
+++ b/thread/demoapp/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2ee5d92
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..78a3b7d
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ffb6261
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..80fa037
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5ca1bfe
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2fd92e3
--- /dev/null
+++ b/thread/demoapp/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/thread/demoapp/res/navigation/nav_graph.xml b/thread/demoapp/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..472d1bb
--- /dev/null
+++ b/thread/demoapp/res/navigation/nav_graph.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/nav_graph"
+ app:startDestination="@+id/thread_network_settings" >
+ <fragment
+ android:id="@+id/thread_network_settings"
+ android:name=".ThreadNetworkSettingsFragment"
+ android:label="Thread Network Settings"
+ tools:layout="@layout/thread_network_settings_fragment">
+ </fragment>
+
+ <fragment
+ android:id="@+id/connectivity_tools"
+ android:name=".ConnectivityToolsFragment"
+ android:label="Connectivity Tools"
+ tools:layout="@layout/connectivity_tools_fragment">
+ </fragment>
+</navigation>
diff --git a/thread/demoapp/res/values/colors.xml b/thread/demoapp/res/values/colors.xml
new file mode 100644
index 0000000..6a65937
--- /dev/null
+++ b/thread/demoapp/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
diff --git a/thread/demoapp/res/values/dimens.xml b/thread/demoapp/res/values/dimens.xml
new file mode 100644
index 0000000..5165951
--- /dev/null
+++ b/thread/demoapp/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="nav_header_vertical_spacing">8dp</dimen>
+ <dimen name="nav_header_height">176dp</dimen>
+ <dimen name="fab_margin">16dp</dimen>
+</resources>
diff --git a/thread/demoapp/res/values/themes.xml b/thread/demoapp/res/values/themes.xml
new file mode 100644
index 0000000..9cb3403
--- /dev/null
+++ b/thread/demoapp/res/values/themes.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.ThreadNetworkDemoApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_500</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/white</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_700</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>