Configure TREL feature from feature flag.

Bug: 354868035
Bug: 359089751

Test: atest ThreadNetworkIntegrationTests:ServiceDiscoveryTest#trel_servicePublished

Change-Id: Ib545df6bf5ce20f35914b4695dbd2e3c1e1232e4
diff --git a/thread/service/java/com/android/server/thread/FeatureFlags.java b/thread/service/java/com/android/server/thread/FeatureFlags.java
new file mode 100644
index 0000000..29bcedd
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/FeatureFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.server.thread;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+public class FeatureFlags {
+    // The namespace for Thread Network feature flags
+    private static final String NAMESPACE_THREAD_NETWORK = "thread_network";
+
+    // The prefix for TREL feature flags
+    private static final String TREL_FEATURE_PREFIX = "TrelFeature__";
+
+    // The feature flag for TREL enabled state
+    private static final String TREL_ENABLED_FLAG = TREL_FEATURE_PREFIX + "enabled";
+
+    private static boolean isFeatureEnabled(String flag, boolean defaultValue) {
+        return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+                NAMESPACE_THREAD_NETWORK, flag, defaultValue);
+    }
+
+    public static boolean isTrelEnabled() {
+        return isFeatureEnabled(TREL_ENABLED_FLAG, false);
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 8747b44..30d5a02 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -357,7 +357,8 @@
                 mNsdPublisher,
                 getMeshcopTxtAttributes(mResources.get()),
                 mOtDaemonCallbackProxy,
-                mCountryCodeSupplier.get());
+                mCountryCodeSupplier.get(),
+                FeatureFlags.isTrelEnabled());
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
         mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
@@ -1406,7 +1407,7 @@
 
     private void setInfraLinkState(InfraLinkState newInfraLinkState) {
         if (Objects.equals(mInfraLinkState, newInfraLinkState)) {
-            return ;
+            return;
         }
         LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
         setInfraLinkInterfaceName(newInfraLinkState.interfaceName);
@@ -1417,7 +1418,7 @@
 
     private void setInfraLinkInterfaceName(String newInfraLinkInterfaceName) {
         if (Objects.equals(mInfraLinkState.interfaceName, newInfraLinkInterfaceName)) {
-            return ;
+            return;
         }
         ParcelFileDescriptor infraIcmp6Socket = null;
         if (newInfraLinkInterfaceName != null) {
@@ -1440,7 +1441,7 @@
 
     private void setInfraLinkNat64Prefix(@Nullable String newNat64Prefix) {
         if (Objects.equals(newNat64Prefix, mInfraLinkState.nat64Prefix)) {
-            return ;
+            return;
         }
         try {
             getOtDaemon()
@@ -1453,7 +1454,7 @@
 
     private void setInfraLinkDnsServers(List<String> newDnsServers) {
         if (Objects.equals(newDnsServers, mInfraLinkState.dnsServers)) {
-            return ;
+            return;
         }
         try {
             getOtDaemon()
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index 8f98941..08409b4 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -48,4 +48,10 @@
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.thread.tests.integration" />
     </test>
+
+    <!-- Enable TREL for integration tests -->
+    <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+        <option name="flag-value"
+                value="thread_network/TrelFeature__enabled=true"/>
+    </target_preparer>
 </configuration>
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 2afca5f..15259c8 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -50,6 +50,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.HexDump;
+
 import com.google.common.truth.Correspondence;
 
 import org.junit.After;
@@ -64,6 +66,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.CompletableFuture;
@@ -441,6 +444,25 @@
                 .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
     }
 
+    @Test
+    // TODO: move this case out to BorderRoutingTest when the service discovery utilities
+    // are decoupled from this test.
+    public void trelFeatureFlagEnabled_trelServicePublished() throws Exception {
+        NsdServiceInfo discoveredService = discoverService(mNsdManager, "_trel._udp");
+        assertThat(discoveredService).isNotNull();
+        // Resolve service with the current TREL port, otherwise it may return stale service from
+        // a previous infra link setup.
+        NsdServiceInfo trelService =
+                resolveServiceUntil(
+                        mNsdManager, discoveredService, s -> s.getPort() == mOtCtl.getTrelPort());
+
+        Map<String, byte[]> txtMap = trelService.getAttributes();
+        assertThat(HexDump.toHexString(txtMap.get("xa")).toLowerCase(Locale.ROOT))
+                .isEqualTo(mOtCtl.getExtendedAddr().toLowerCase(Locale.ROOT));
+        assertThat(HexDump.toHexString(txtMap.get("xp")).toLowerCase(Locale.ROOT))
+                .isEqualTo(mOtCtl.getExtendedPanId().toLowerCase(Locale.ROOT));
+    }
+
     private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
             throws InterruptedException, ExecutionException, TimeoutException {
         mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, listener);
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 afb0fc7..9fbfa45 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -144,6 +144,18 @@
         executeCommand("netdata register");
     }
 
+    public int getTrelPort() {
+        return Integer.parseInt(executeCommandAndParse("trel port").get(0));
+    }
+
+    public String getExtendedAddr() {
+        return executeCommandAndParse("extaddr").get(0);
+    }
+
+    public String getExtendedPanId() {
+        return executeCommandAndParse("extpanid").get(0);
+    }
+
     public String executeCommand(String cmd) {
         return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
     }
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index c6a24ea..53b1eca 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -35,6 +35,7 @@
     static_libs: [
         "androidx.test.rules",
         "frameworks-base-testutils",
+        "framework-configinfrastructure.stubs.module_lib",
         "framework-connectivity-pre-jarjar",
         "framework-connectivity-t-pre-jarjar",
         "framework-location.stubs.module_lib",
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index e188491..9af0b53 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -17,6 +17,7 @@
 package com.android.server.thread;
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -34,6 +35,7 @@
 
 import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
@@ -85,6 +87,7 @@
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.os.test.TestLooper;
+import android.provider.DeviceConfig;
 import android.util.AtomicFile;
 
 import androidx.test.annotation.UiThreadTest;
@@ -102,6 +105,7 @@
 import com.android.server.thread.openthread.OtDaemonConfiguration;
 import com.android.server.thread.openthread.testing.FakeOtDaemon;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -258,6 +262,13 @@
         mService.setTestNetworkAgent(mMockNetworkAgent);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        runAsShell(
+                WRITE_DEVICE_CONFIG,
+                () -> DeviceConfig.deleteProperty("thread_network", "TrelFeature__enabled"));
+    }
+
     @Test
     public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
         when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
@@ -324,6 +335,33 @@
     }
 
     @Test
+    public void initialize_trelFeatureDisabled_trelDisabledAtOtDaemon() throws Exception {
+        runAsShell(
+                WRITE_DEVICE_CONFIG,
+                () ->
+                        DeviceConfig.setProperty(
+                                "thread_network", "TrelFeature__enabled", "false", false));
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.isTrelEnabled()).isFalse();
+    }
+
+    @Test
+    public void initialize_trelFeatureEnabled_setTrelEnabledAtOtDamon() throws Exception {
+        runAsShell(
+                WRITE_DEVICE_CONFIG,
+                () ->
+                        DeviceConfig.setProperty(
+                                "thread_network", "TrelFeature__enabled", "true", false));
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.isTrelEnabled()).isTrue();
+    }
+
+    @Test
     public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
         when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");