Merge "Add current clients to servicediscovery dumpsys" into main
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 9c8fd99..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -131,6 +131,10 @@
// returned when a tethered interface is requested; until then, it remains in client mode. Its
// current mode is reflected in mTetheringInterfaceMode.
private String mTetheringInterface;
+ // If the tethering interface is in server mode, it is not tracked by factory. The HW address
+ // must be maintained by the EthernetTracker. Its current mode is reflected in
+ // mTetheringInterfaceMode.
+ private String mTetheringInterfaceHwAddr;
private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
// Tracks whether clients were notified that the tethered interface is available
private boolean mTetheredInterfaceWasAvailable = false;
@@ -582,6 +586,7 @@
removeInterface(iface);
if (iface.equals(mTetheringInterface)) {
mTetheringInterface = null;
+ mTetheringInterfaceHwAddr = null;
}
broadcastInterfaceStateChange(iface);
}
@@ -610,13 +615,14 @@
return;
}
+ final String hwAddress = config.hwAddr;
+
if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
maybeUpdateServerModeInterfaceState(iface, true);
+ mTetheringInterfaceHwAddr = hwAddress;
return;
}
- final String hwAddress = config.hwAddr;
-
NetworkCapabilities nc = mNetworkCapabilities.get(iface);
if (nc == null) {
// Try to resolve using mac address
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index a2e4ab6..38f46db 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -27,16 +27,21 @@
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.system.OsConstants
import androidx.test.platform.app.InstrumentationRegistry
-import com.android.compatibility.common.util.PropertyUtil.isVendorApiLevelNewerThan
+import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.internal.util.HexDump
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.NetworkStackModuleTest
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.SkipPresubmit
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.runAsShell
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import com.google.common.truth.TruthJUnit.assume
+import kotlin.random.Random
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.Before
@@ -50,7 +55,6 @@
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
@RunWith(DevSdkIgnoreRunner::class)
@NetworkStackModuleTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
class ApfIntegrationTest {
companion object {
@BeforeClass
@@ -79,7 +83,10 @@
private lateinit var caps: ApfCapabilities
fun getApfCapabilities(): ApfCapabilities {
- val caps = runShellCommandOrThrow("cmd network_stack apf $ifname capabilities").trim()
+ val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
+ if (caps.isEmpty()) {
+ return ApfCapabilities(0, 0, 0)
+ }
val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
return ApfCapabilities(version, maxLen, packetFormat)
}
@@ -87,7 +94,6 @@
@Before
fun setUp() {
assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
- assume().that(isVendorApiLevelNewerThan(Build.VERSION_CODES.TIRAMISU)).isTrue()
networkCallback = TestableNetworkCallback()
cm.requestNetwork(
NetworkRequest.Builder()
@@ -100,7 +106,10 @@
ifname = assertNotNull(it.lp.interfaceName)
true
}
- runShellCommandOrThrow("cmd network_stack apf $ifname pause")
+ // It's possible the device does not support APF, in which case this command will not be
+ // successful. Ignore the error as testApfCapabilities() already asserts APF support on the
+ // respective VSR releases and all other tests are based on the capabilities indicated.
+ runShellCommand("cmd network_stack apf $ifname pause")
caps = getApfCapabilities()
}
@@ -110,17 +119,76 @@
cm.unregisterNetworkCallback(networkCallback)
}
if (::ifname.isInitialized) {
- runShellCommandOrThrow("cmd network_stack apf $ifname resume")
+ runShellCommand("cmd network_stack apf $ifname resume")
}
}
@Test
- fun testGetApfCapabilities() {
+ fun testApfCapabilities() {
+ // APF became mandatory in Android 14 VSR.
+ assume().that(getVsrApiLevel()).isAtLeast(34)
+
+ // ApfFilter does not support anything but ARPHRD_ETHER.
+ assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+
+ // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
+ // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ // - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ // TODO: check whether above text should be changed "34 or higher"
+ // This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
+ // currently no tests for APFv6 and there cannot be a valid implementation as the
+ // interpreter has yet to be finalized.
assertThat(caps.apfVersionSupported).isEqualTo(4)
assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
- if (isVendorApiLevelNewerThan(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
+
+ // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
+ // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
+ // - [GMS-VSR-5.3.12-009] MUST indicate at least 2000 bytes of usable memory from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ if (getVsrApiLevel() >= 202404) {
assertThat(caps.maximumApfProgramSize).isAtLeast(2000)
}
- assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+ }
+
+ // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
+ fun assumeApfVersionSupportAtLeast(version: Int) {
+ assume().that(caps.apfVersionSupported).isAtLeast(version)
+ }
+
+ fun installProgram(bytes: ByteArray) {
+ val prog = HexDump.toHexString(bytes, 0 /* offset */, bytes.size, false /* upperCase */)
+ val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
+ // runShellCommandOrThrow only throws on S+.
+ assertThat(result).isEqualTo("success")
+ }
+
+ fun readProgram(): ByteArray {
+ val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
+ // runShellCommandOrThrow only throws on S+.
+ assertThat(progHexString).isNotEmpty()
+ return HexDump.hexStringToByteArray(progHexString)
+ }
+
+ @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
+ // APF integration is mostly broken before V, only run the full read / write test on V+.
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testReadWriteProgram() {
+ assumeApfVersionSupportAtLeast(4)
+
+ // Only test down to 2 bytes. The first byte always stays PASS.
+ val program = ByteArray(caps.maximumApfProgramSize)
+ for (i in caps.maximumApfProgramSize downTo 2) {
+ // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
+ // Note that only the very first instruction (PASS) is valid APF bytecode.
+ Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
+ installProgram(program.sliceArray(0..<i))
+
+ // Compare entire memory region.
+ val readResult = readProgram()
+ assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
+ }
}
}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index c70f3af..d29e657 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -29,6 +29,7 @@
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
@@ -45,14 +46,25 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
@RequiresThreadFeature
@RunWith(AndroidJUnit4.class)
public class ThreadIntegrationTest {
+ // The byte[] buffer size for UDP tests
+ private static final int UDP_BUFFER_SIZE = 1024;
+
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -66,24 +78,32 @@
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+ private ExecutorService mExecutor;
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
private OtDaemonController mOtCtl;
+ private FullThreadDevice mFtd;
@Before
public void setUp() throws Exception {
+ mExecutor = Executors.newSingleThreadExecutor();
mOtCtl = new OtDaemonController();
mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl.factoryReset();
+
+ mFtd = new FullThreadDevice(10 /* nodeId */);
}
@After
public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
+
+ mFtd.destroy();
+ mExecutor.shutdownNow();
}
@Test
@@ -153,10 +173,57 @@
assertThat(mOtCtl.getCountryCode()).isEqualTo("CN");
}
- private static String runThreadCommand(String cmd) {
- return runShellCommandOrThrow("cmd thread_network " + cmd);
+ @Test
+ public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
+ // Topology:
+ // Test App ------ thread-wpan ------ End Device
+
+ mController.joinAndWait(DEFAULT_DATASET);
+ startFtdChild(mFtd, DEFAULT_DATASET);
+ final Inet6Address serverAddress = mOtCtl.getMeshLocalAddresses().get(0);
+ final int serverPort = 9527;
+
+ mExecutor.execute(() -> startUdpEchoServerAndWait(serverAddress, serverPort));
+ mFtd.udpOpen();
+ mFtd.udpSend("Hello,Thread", serverAddress, serverPort);
+ String udpReply = mFtd.udpReceive();
+
+ assertThat(udpReply).isEqualTo("Hello,Thread");
}
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
+
+ private static String runThreadCommand(String cmd) {
+ return runShellCommandOrThrow("cmd thread_network " + cmd);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+ }
+
+ /**
+ * Starts a UDP echo server and replies to the first UDP message.
+ *
+ * <p>This method exits when the first UDP message is received and the reply is sent
+ */
+ private void startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort) {
+ try (var udpServerSocket = new DatagramSocket(serverPort, serverAddress)) {
+ DatagramPacket recvPacket =
+ new DatagramPacket(new byte[UDP_BUFFER_SIZE], UDP_BUFFER_SIZE);
+ udpServerSocket.receive(recvPacket);
+ byte[] sendBuffer = Arrays.copyOf(recvPacket.getData(), recvPacket.getData().length);
+ udpServerSocket.send(
+ new DatagramPacket(
+ sendBuffer,
+ sendBuffer.length,
+ (Inet6Address) recvPacket.getAddress(),
+ recvPacket.getPort()));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index f7bb9ff..5e70f6c 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -20,8 +20,6 @@
import static com.google.common.io.BaseEncoding.base16;
-import static org.junit.Assert.fail;
-
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.nsd.NsdServiceInfo;
@@ -218,6 +216,11 @@
return matcher.group(4);
}
+ /** Sends a UDP message to given IPv6 address and port. */
+ public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
+ executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
+ }
+
/** Enables the SRP client and run in autostart mode. */
public void autoStartSrpClient() {
executeCommand("srp client autostart enable");
@@ -474,7 +477,7 @@
break;
}
if (line.startsWith("Error")) {
- fail("ot-cli-ftd reported an error: " + line);
+ throw new IOException("ot-cli-ftd reported an error: " + line);
}
if (!line.startsWith("> ")) {
result.add(line);
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index f39a064..b3175fd 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -16,13 +16,16 @@
package android.net.thread.utils;
+import android.annotation.Nullable;
import android.net.InetAddresses;
+import android.net.IpPrefix;
import android.os.SystemClock;
import com.android.compatibility.common.util.SystemUtil;
import java.net.Inet6Address;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
/**
@@ -53,10 +56,7 @@
/** Returns the list of IPv6 addresses on ot-daemon. */
public List<Inet6Address> getAddresses() {
- String output = executeCommand("ipaddr");
- return Arrays.asList(output.split("\n")).stream()
- .map(String::trim)
- .filter(str -> !str.equals("Done"))
+ return executeCommandAndParse("ipaddr").stream()
.map(addr -> InetAddresses.parseNumericAddress(addr))
.map(inetAddr -> (Inet6Address) inetAddr)
.toList();
@@ -70,17 +70,54 @@
/** Returns the ML-EID of the device. */
public Inet6Address getMlEid() {
- String addressStr = executeCommand("ipaddr mleid").split("\n")[0].trim();
+ String addressStr = executeCommandAndParse("ipaddr mleid").get(0);
return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
}
/** Returns the country code on ot-daemon. */
public String getCountryCode() {
- String countryCodeStr = executeCommand("region").split("\n")[0].trim();
- return countryCodeStr;
+ return executeCommandAndParse("region").get(0);
+ }
+
+ /**
+ * Returns the list of IPv6 Mesh-Local addresses on ot-daemon.
+ *
+ * <p>The return List can be empty if no Mesh-Local prefix exists.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ IpPrefix meshLocalPrefix = getMeshLocalPrefix();
+ if (meshLocalPrefix == null) {
+ return Collections.emptyList();
+ }
+ return getAddresses().stream().filter(addr -> meshLocalPrefix.contains(addr)).toList();
+ }
+
+ /**
+ * Returns the Mesh-Local prefix or {@code null} if none exists (e.g. the Active Dataset is not
+ * set).
+ */
+ @Nullable
+ public IpPrefix getMeshLocalPrefix() {
+ List<IpPrefix> prefixes =
+ executeCommandAndParse("prefix meshlocal").stream()
+ .map(prefix -> new IpPrefix(prefix))
+ .toList();
+ return prefixes.isEmpty() ? null : prefixes.get(0);
}
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
+
+ /**
+ * Executes a ot-ctl command and parse the output to a list of strings.
+ *
+ * <p>The trailing "Done" in the command output will be dropped.
+ */
+ public List<String> executeCommandAndParse(String cmd) {
+ return Arrays.asList(executeCommand(cmd).split("\n")).stream()
+ .map(String::trim)
+ .filter(str -> !str.equals("Done"))
+ .toList();
+ }
}