Merge "[Thread] add config_thread_mdns_vendor_specific_txts" into main

This commit is contained in:
Treehugger Robot 2024-07-10 11:03:52 +00:00 • committed by Gerrit Code Review
commit 9365e728e5
5 changed files with 114 additions and 33 deletions

View file

@ -54,9 +54,21 @@
-->
<string translatable="false" name="config_thread_model_name">Thread Border Router</string>
<!-- Whether the Thread network will be managed by the Google Home ecosystem. When this value
is set, a TXT entry "vgh=0" or "vgh=1" will be added to the "_mehscop._udp" mDNS service
respectively (The TXT value is a string).
<!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
example, the key and value of each TXT entry MUST be separated with "=". If the value length is
0, the trailing "=" may be omitted. Additionally, the TXT keys MUST start with "v" and be at
least 2 characters.
Note, do not include credentials in any of the TXT entries - they will be advertised on Wi-Fi
or Ethernet link.
An example config can be:
<string-array name="config_thread_mdns_vendor_specific_txts">
<item>vab=123</item>
<item>vcd</item>
</string-array>
-->
<bool name="config_thread_managed_by_google_home">false</bool>
<string-array name="config_thread_mdns_vendor_specific_txts">
</string-array>
</resources>

View file

@ -51,6 +51,7 @@
<item type="string" name="config_thread_vendor_name" />
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
<item type="array" name="config_thread_mdns_vendor_specific_txts" />
</policy>
</overlayable>
</resources>

View file

@ -139,6 +139,7 @@ import java.security.SecureRandom;
import java.time.Clock;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -344,8 +345,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
final String modelName = resources.getString(R.string.config_thread_model_name);
final String vendorName = resources.getString(R.string.config_thread_vendor_name);
final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
final boolean managedByGoogle =
resources.getBoolean(R.bool.config_thread_managed_by_google_home);
final String[] vendorSpecificTxts =
resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
if (!modelName.isEmpty()) {
if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
@ -375,19 +376,44 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
meshcopTxts.modelName = modelName;
meshcopTxts.vendorName = vendorName;
meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
meshcopTxts.nonStandardTxtEntries = List.of(makeManagedByGoogleTxtAttr(managedByGoogle));
meshcopTxts.nonStandardTxtEntries = makeVendorSpecificTxtAttrs(vendorSpecificTxts);
return meshcopTxts;
}
/**
* Creates a DNS-SD TXT entry for indicating whether Thread on this device is managed by Google.
* Parses vendor-specific TXT entries from "=" separated strings into list of {@link
* DnsTxtAttribute}.
*
* @return TXT entry "vgh=1" if {@code managedByGoogle} is {@code true}; otherwise, "vgh=0"
* @throws IllegalArgumentsException if invalid TXT entries are found in {@code vendorTxts}
*/
private static DnsTxtAttribute makeManagedByGoogleTxtAttr(boolean managedByGoogle) {
final byte[] value = (managedByGoogle ? "1" : "0").getBytes(UTF_8);
return new DnsTxtAttribute("vgh", value);
@VisibleForTesting
static List<DnsTxtAttribute> makeVendorSpecificTxtAttrs(String[] vendorTxts) {
List<DnsTxtAttribute> txts = new ArrayList<>();
for (String txt : vendorTxts) {
String[] kv = txt.split("=", 2 /* limit */); // Split with only the first '='
if (kv.length < 1) {
throw new IllegalArgumentException(
"Invalid vendor-specific TXT is found in resources: " + txt);
}
if (kv[0].length() < 2) {
throw new IllegalArgumentException(
"Invalid vendor-specific TXT key \""
+ kv[0]
+ "\": it must contain at least 2 characters");
}
if (!kv[0].startsWith("v")) {
throw new IllegalArgumentException(
"Invalid vendor-specific TXT key \""
+ kv[0]
+ "\": it doesn't start with \"v\"");
}
txts.add(new DnsTxtAttribute(kv[0], (kv.length >= 2 ? kv[1] : "").getBytes(UTF_8)));
}
return txts;
}
private void onOtDaemonDied() {

View file

@ -858,7 +858,6 @@ public class ThreadNetworkControllerTest {
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
}
@Test
@ -885,7 +884,6 @@ public class ThreadNetworkControllerTest {
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(txtMap.get("id").length).isEqualTo(16);
assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
}
@Test

View file

@ -49,6 +49,8 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -96,11 +98,11 @@ import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
@ -150,7 +152,6 @@ public final class ThreadNetworkControllerServiceTest {
private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
private static final String TEST_VENDOR_NAME = "test vendor";
private static final String TEST_MODEL_NAME = "test model";
private static final boolean TEST_VGH_VALUE = false;
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@ -203,8 +204,8 @@ public final class ThreadNetworkControllerServiceTest {
.thenReturn(TEST_VENDOR_OUI);
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
.thenReturn(TEST_VGH_VALUE);
when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
.thenReturn(new String[] {});
final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
@ -247,8 +248,8 @@ public final class ThreadNetworkControllerServiceTest {
.thenReturn(TEST_VENDOR_OUI);
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
.thenReturn(true);
when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
.thenReturn(new String[] {"vt=test"});
mService.initialize();
mTestLooper.dispatchAll();
@ -258,19 +259,7 @@ public final class ThreadNetworkControllerServiceTest {
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
assertThat(meshcopTxts.nonStandardTxtEntries)
.containsExactly(new DnsTxtAttribute("vgh", "1".getBytes(StandardCharsets.UTF_8)));
}
@Test
public void getMeshcopTxtAttributes_managedByGoogleIsFalse_vghIsZero() {
when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
.thenReturn(false);
MeshcopTxtAttributes meshcopTxts =
ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
assertThat(meshcopTxts.nonStandardTxtEntries)
.containsExactly(new DnsTxtAttribute("vgh", "0".getBytes(StandardCharsets.UTF_8)));
.containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
}
@Test
@ -342,6 +331,61 @@ public final class ThreadNetworkControllerServiceTest {
return ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources).vendorOui;
}
@Test
public void makeVendorSpecificTxtAttrs_validTxts_returnsParsedTxtAttrs() {
String[] txts = new String[] {"va=123", "vb=", "vc"};
List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
assertThat(attrs)
.containsExactly(
new DnsTxtAttribute("va", "123".getBytes(UTF_8)),
new DnsTxtAttribute("vb", new byte[] {}),
new DnsTxtAttribute("vc", new byte[] {}));
}
@Test
public void makeVendorSpecificTxtAttrs_txtKeyNotStartWithV_throwsIllegalArgument() {
String[] txts = new String[] {"abc=123"};
assertThrows(
IllegalArgumentException.class, () -> mService.makeVendorSpecificTxtAttrs(txts));
}
@Test
public void makeVendorSpecificTxtAttrs_txtIsTooShort_throwsIllegalArgument() {
String[] txtEmptyKey = new String[] {"=123"};
String[] txtSingleCharKey = new String[] {"v=456"};
assertThrows(
IllegalArgumentException.class,
() -> mService.makeVendorSpecificTxtAttrs(txtEmptyKey));
assertThrows(
IllegalArgumentException.class,
() -> mService.makeVendorSpecificTxtAttrs(txtSingleCharKey));
}
@Test
public void makeVendorSpecificTxtAttrs_txtValueIsEmpty_parseSuccess() {
String[] txts = new String[] {"va=", "vb"};
List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
assertThat(attrs)
.containsExactly(
new DnsTxtAttribute("va", new byte[] {}),
new DnsTxtAttribute("vb", new byte[] {}));
}
@Test
public void makeVendorSpecificTxtAttrs_multipleEquals_splittedByTheFirstEqual() {
String[] txts = new String[] {"va=abc=def=123"};
List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
assertThat(attrs).containsExactly(new DnsTxtAttribute("va", "abc=def=123".getBytes(UTF_8)));
}
@Test
public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
mService.initialize();