Merge "Store MAC address of tethering interface in EthernetTracker" into main
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index a2e4ab6..b91dee8 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,15 +21,13 @@
 import android.net.NetworkCapabilities
 import android.net.NetworkRequest
 import android.net.apf.ApfCapabilities
-import android.os.Build
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 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.SystemUtil.runShellCommandOrThrow
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.NetworkStackModuleTest
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
@@ -50,7 +48,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 +76,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 +87,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 +99,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 +112,36 @@
             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)
     }
 }
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();
+    }
 }