• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.net.thread.utils;
17 
18 import static android.net.DnsResolver.TYPE_A;
19 import static android.net.DnsResolver.TYPE_AAAA;
20 import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
21 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
22 
23 import static com.google.common.io.BaseEncoding.base16;
24 
25 import static java.util.concurrent.TimeUnit.SECONDS;
26 
27 import android.net.InetAddresses;
28 import android.net.IpPrefix;
29 import android.net.nsd.NsdServiceInfo;
30 import android.net.thread.ActiveOperationalDataset;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 
34 import com.google.errorprone.annotations.FormatMethod;
35 
36 import java.io.BufferedReader;
37 import java.io.BufferedWriter;
38 import java.io.IOException;
39 import java.io.InputStreamReader;
40 import java.io.OutputStreamWriter;
41 import java.net.Inet6Address;
42 import java.net.InetAddress;
43 import java.nio.charset.StandardCharsets;
44 import java.time.Duration;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.concurrent.CompletableFuture;
49 import java.util.concurrent.ExecutionException;
50 import java.util.concurrent.TimeoutException;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
53 
54 /**
55  * A class that launches and controls a simulation Full Thread Device (FTD).
56  *
57  * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
58  * and output. See <a
59  * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
60  * available commands.
61  */
62 public final class FullThreadDevice {
63     private static final int HOP_LIMIT = 64;
64     private static final int PING_INTERVAL = 1;
65     private static final int PING_SIZE = 100;
66     // There may not be a response for the ping command, using a short timeout to keep the tests
67     // short.
68     private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
69     // 1 second timeout should be used when response is expected.
70     private static final float PING_TIMEOUT_1_SECOND = 1f;
71     private static final int READ_LINE_TIMEOUT_SECONDS = 5;
72 
73     private final Process mProcess;
74     private final BufferedReader mReader;
75     private final BufferedWriter mWriter;
76     private final HandlerThread mReaderHandlerThread;
77     private final Handler mReaderHandler;
78 
79     private ActiveOperationalDataset mActiveOperationalDataset;
80 
81     /**
82      * Constructs a {@link FullThreadDevice} for the given node ID.
83      *
84      * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
85      * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
86      * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
87      *
88      * @param nodeId the node ID for the simulation Full Thread Device.
89      * @throws IllegalStateException the node ID is already occupied by another simulation Thread
90      *     device.
91      */
FullThreadDevice(int nodeId)92     public FullThreadDevice(int nodeId) {
93         try {
94             mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd -Leth1 " + nodeId);
95         } catch (IOException e) {
96             throw new IllegalStateException(
97                     "Failed to start ot-cli-ftd -Leth1 (id=" + nodeId + ")", e);
98         }
99         mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
100         mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
101         mReaderHandlerThread = new HandlerThread("FullThreadDeviceReader");
102         mReaderHandlerThread.start();
103         mReaderHandler = new Handler(mReaderHandlerThread.getLooper());
104         mActiveOperationalDataset = null;
105     }
106 
destroy()107     public void destroy() {
108         mProcess.destroy();
109         mReaderHandlerThread.quit();
110     }
111 
112     /**
113      * Returns an OMR (Off-Mesh-Routable) address on this device if any.
114      *
115      * <p>This methods goes through all unicast addresses on the device and returns the first
116      * address which is neither link-local nor mesh-local.
117      */
getOmrAddress()118     public Inet6Address getOmrAddress() {
119         List<String> addresses = executeCommand("ipaddr");
120         IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
121         for (String address : addresses) {
122             if (address.startsWith("fe80:")) {
123                 continue;
124             }
125             Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
126             if (!meshLocalPrefix.contains(addr)) {
127                 return addr;
128             }
129         }
130         return null;
131     }
132 
133     /** Returns the Mesh-local EID address on this device if any. */
getMlEid()134     public Inet6Address getMlEid() {
135         List<String> addresses = executeCommand("ipaddr mleid");
136         return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
137     }
138 
139     /**
140      * Returns the link-local address of the device.
141      *
142      * <p>This methods goes through all unicast addresses on the device and returns the address that
143      * begins with fe80.
144      */
getLinkLocalAddress()145     public Inet6Address getLinkLocalAddress() {
146         List<String> output = executeCommand("ipaddr linklocal");
147         if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
148             return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
149         }
150         return null;
151     }
152 
153     /**
154      * Returns the mesh-local addresses of the device.
155      *
156      * <p>This methods goes through all unicast addresses on the device and returns the address that
157      * begins with mesh-local prefix.
158      */
getMeshLocalAddresses()159     public List<Inet6Address> getMeshLocalAddresses() {
160         List<String> addresses = executeCommand("ipaddr");
161         List<Inet6Address> meshLocalAddresses = new ArrayList<>();
162         IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
163         for (String address : addresses) {
164             Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
165             if (meshLocalPrefix.contains(addr)) {
166                 meshLocalAddresses.add(addr);
167             }
168         }
169         return meshLocalAddresses;
170     }
171 
172     /**
173      * Joins the Thread network using the given {@link ActiveOperationalDataset}.
174      *
175      * @param dataset the Active Operational Dataset
176      */
joinNetwork(ActiveOperationalDataset dataset)177     public void joinNetwork(ActiveOperationalDataset dataset) {
178         mActiveOperationalDataset = dataset;
179         executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
180         executeCommand("ifconfig up");
181         executeCommand("thread start");
182     }
183 
184     /** Stops the Thread network radio. */
stopThreadRadio()185     public void stopThreadRadio() {
186         executeCommand("thread stop");
187         executeCommand("ifconfig down");
188     }
189 
190     /**
191      * Waits for the Thread device to enter the any state of the given {@link List<String>}.
192      *
193      * @param states the list of states to wait for. Valid states are "disabled", "detached",
194      *     "child", "router" and "leader".
195      * @param timeout the time to wait for the expected state before throwing
196      */
waitForStateAnyOf(List<String> states, Duration timeout)197     public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
198         waitFor(() -> states.contains(getState()), timeout);
199     }
200 
201     /**
202      * Gets the state of the Thread device.
203      *
204      * @return a string representing the state.
205      */
getState()206     public String getState() {
207         return executeCommand("state").get(0);
208     }
209 
210     /** Closes the UDP socket. */
udpClose()211     public void udpClose() {
212         executeCommand("udp close");
213     }
214 
215     /** Opens the UDP socket. */
udpOpen()216     public void udpOpen() {
217         executeCommand("udp open");
218     }
219 
220     /** Opens the UDP socket and binds it to a specific address and port. */
udpBind(Inet6Address address, int port)221     public void udpBind(Inet6Address address, int port) {
222         udpClose();
223         udpOpen();
224         executeCommand("udp bind %s %d", address.getHostAddress(), port);
225     }
226 
227     /** Returns the message received on the UDP socket. */
udpReceive()228     public String udpReceive() throws IOException {
229         Pattern pattern =
230                 Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
231         Matcher matcher = pattern.matcher(readLine());
232         matcher.matches();
233 
234         return matcher.group(4);
235     }
236 
237     /** Sends a UDP message to given IP address and port. */
udpSend(String message, InetAddress serverAddr, int serverPort)238     public void udpSend(String message, InetAddress serverAddr, int serverPort) {
239         executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
240     }
241 
242     /** Sets `true` to enable SRP server on this device. */
setSrpServerEnabled(boolean enabled)243     public void setSrpServerEnabled(boolean enabled) {
244         String cmd = enabled ? "enable" : "disable";
245         executeCommand("srp server " + cmd);
246     }
247 
248     /** Enables the SRP client and run in autostart mode. */
autoStartSrpClient()249     public void autoStartSrpClient() {
250         executeCommand("srp client autostart enable");
251     }
252 
253     /** Sets the hostname (e.g. "MyHost") for the SRP client. */
setSrpHostname(String hostname)254     public void setSrpHostname(String hostname) {
255         executeCommand("srp client host name " + hostname);
256     }
257 
258     /** Sets the host addresses for the SRP client. */
setSrpHostAddresses(List<Inet6Address> addresses)259     public void setSrpHostAddresses(List<Inet6Address> addresses) {
260         executeCommand(
261                 "srp client host address "
262                         + String.join(
263                                 " ",
264                                 addresses.stream().map(Inet6Address::getHostAddress).toList()));
265     }
266 
267     /** Removes the SRP host */
removeSrpHost()268     public void removeSrpHost() {
269         executeCommand("srp client host remove 1 1");
270     }
271 
272     /**
273      * Adds an SRP service for the SRP client and wait for the registration to complete.
274      *
275      * @param serviceName the service name like "MyService"
276      * @param serviceType the service type like "_test._tcp"
277      * @param subtypes the service subtypes like "_sub1"
278      * @param port the port number in range [1, 65535]
279      * @param txtMap the map of TXT names and values
280      * @throws TimeoutException if the service isn't registered within timeout
281      */
addSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)282     public void addSrpService(
283             String serviceName,
284             String serviceType,
285             List<String> subtypes,
286             int port,
287             Map<String, byte[]> txtMap)
288             throws TimeoutException {
289         StringBuilder fullServiceType = new StringBuilder(serviceType);
290         for (String subtype : subtypes) {
291             fullServiceType.append(",").append(subtype);
292         }
293         waitForSrpServer();
294         executeCommand(
295                 "srp client service add %s %s %d %d %d %s",
296                 serviceName,
297                 fullServiceType,
298                 port,
299                 0 /* priority */,
300                 0 /* weight */,
301                 txtMapToHexString(txtMap));
302         waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
303     }
304 
305     /**
306      * Removes an SRP service for the SRP client.
307      *
308      * @param serviceName the service name like "MyService"
309      * @param serviceType the service type like "_test._tcp"
310      * @param notifyServer whether to notify SRP server about the removal
311      */
removeSrpService(String serviceName, String serviceType, boolean notifyServer)312     public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
313         String verb = notifyServer ? "remove" : "clear";
314         executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
315     }
316 
317     /**
318      * Updates an existing SRP service for the SRP client.
319      *
320      * <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
321      *
322      * @param serviceName the service name like "MyService"
323      * @param serviceType the service type like "_test._tcp"
324      * @param subtypes the service subtypes like "_sub1"
325      * @param port the port number in range [1, 65535]
326      * @param txtMap the map of TXT names and values
327      * @throws TimeoutException if the service isn't updated within timeout
328      */
updateSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)329     public void updateSrpService(
330             String serviceName,
331             String serviceType,
332             List<String> subtypes,
333             int port,
334             Map<String, byte[]> txtMap)
335             throws TimeoutException {
336         removeSrpService(serviceName, serviceType, false /* notifyServer */);
337         addSrpService(serviceName, serviceType, subtypes, port, txtMap);
338     }
339 
340     /** Checks if an SRP service is registered. */
isSrpServiceRegistered(String serviceName, String serviceType)341     public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
342         List<String> lines = executeCommand("srp client service");
343         for (String line : lines) {
344             if (line.contains(serviceName) && line.contains(serviceType)) {
345                 return line.contains("Registered");
346             }
347         }
348         return false;
349     }
350 
351     /** Checks if an SRP host is registered. */
isSrpHostRegistered()352     public boolean isSrpHostRegistered() {
353         List<String> lines = executeCommand("srp client host");
354         for (String line : lines) {
355             return line.contains("Registered");
356         }
357         return false;
358     }
359 
360     /** Sets the DNS server address. */
setDnsServerAddress(String address)361     public void setDnsServerAddress(String address) {
362         executeCommand("dns config " + address);
363     }
364 
365     /** Resolves the {@code queryType} record of the {@code hostname} via DNS. */
resolveHost(String hostname, int queryType)366     public List<InetAddress> resolveHost(String hostname, int queryType) {
367         // CLI output:
368         // DNS response for hostname.com. - fd12::abc1 TTL:50 fd12::abc2 TTL:50 fd12::abc3 TTL:50
369 
370         String command;
371         switch (queryType) {
372             case TYPE_A -> command = "resolve4";
373             case TYPE_AAAA -> command = "resolve";
374             default -> throw new IllegalArgumentException("Invalid query type: " + queryType);
375         }
376         final List<InetAddress> addresses = new ArrayList<>();
377         String line;
378         try {
379             line = executeCommand("dns " + command + " " + hostname).get(0);
380         } catch (IllegalStateException e) {
381             return addresses;
382         }
383         final String[] addressTtlPairs = line.split("-")[1].strip().split(" ");
384         for (int i = 0; i < addressTtlPairs.length; i += 2) {
385             addresses.add(InetAddresses.parseNumericAddress(addressTtlPairs[i]));
386         }
387         return addresses;
388     }
389 
390     /** Returns the first browsed service instance of {@code serviceType}. */
browseService(String serviceType)391     public NsdServiceInfo browseService(String serviceType) {
392         // CLI output:
393         // DNS browse response for _testservice._tcp.default.service.arpa.
394         // test-service
395         //    Port:12345, Priority:0, Weight:0, TTL:10
396         //    Host:testhost.default.service.arpa.
397         //    HostAddress:2001:0:0:0:0:0:0:1 TTL:10
398         //    TXT:[key1=0102, key2=03] TTL:10
399 
400         List<String> lines = executeCommand("dns browse " + serviceType);
401         NsdServiceInfo info = new NsdServiceInfo();
402         info.setServiceName(lines.get(1));
403         info.setServiceType(serviceType);
404         info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2)));
405         info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3)));
406         info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4))));
407         DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info);
408 
409         return info;
410     }
411 
412     /** Returns the resolved service instance. */
resolveService(String serviceName, String serviceType)413     public NsdServiceInfo resolveService(String serviceName, String serviceType) {
414         // CLI output:
415         // DNS service resolution response for test-service for service
416         // _test._tcp.default.service.arpa.
417         // Port:12345, Priority:0, Weight:0, TTL:10
418         // Host:Android.default.service.arpa.
419         // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
420         // TXT:[key1=0102, key2=03] TTL:10
421 
422         List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType);
423         NsdServiceInfo info = new NsdServiceInfo();
424         info.setServiceName(serviceName);
425         info.setServiceType(serviceType);
426         info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1)));
427         info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2)));
428         info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3))));
429         DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info);
430 
431         return info;
432     }
433 
434     /** Runs the "factoryreset" command on the device. */
factoryReset()435     public void factoryReset() {
436         try {
437             mWriter.write("factoryreset\n");
438             mWriter.flush();
439             // fill the input buffer to avoid truncating next command
440             for (int i = 0; i < 1000; ++i) {
441                 mWriter.write("\n");
442             }
443             mWriter.flush();
444         } catch (IOException e) {
445             throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
446         }
447     }
448 
subscribeMulticastAddress(Inet6Address address)449     public void subscribeMulticastAddress(Inet6Address address) {
450         executeCommand("ipmaddr add " + address.getHostAddress());
451     }
452 
ping(InetAddress address, Inet6Address source)453     public void ping(InetAddress address, Inet6Address source) {
454         ping(
455                 address,
456                 source,
457                 PING_SIZE,
458                 1 /* count */,
459                 PING_INTERVAL,
460                 HOP_LIMIT,
461                 PING_TIMEOUT_0_1_SECOND);
462     }
463 
ping(InetAddress address)464     public void ping(InetAddress address) {
465         ping(
466                 address,
467                 null,
468                 PING_SIZE,
469                 1 /* count */,
470                 PING_INTERVAL,
471                 HOP_LIMIT,
472                 PING_TIMEOUT_0_1_SECOND);
473     }
474 
475     /** Returns the number of ping reply packets received. */
ping(InetAddress address, int count)476     public int ping(InetAddress address, int count) {
477         List<String> output =
478                 ping(
479                         address,
480                         null,
481                         PING_SIZE,
482                         count,
483                         PING_INTERVAL,
484                         HOP_LIMIT,
485                         PING_TIMEOUT_1_SECOND);
486         return getReceivedPacketsCount(output);
487     }
488 
ping( InetAddress address, Inet6Address source, int size, int count, int interval, int hopLimit, float timeout)489     private List<String> ping(
490             InetAddress address,
491             Inet6Address source,
492             int size,
493             int count,
494             int interval,
495             int hopLimit,
496             float timeout) {
497         String cmd =
498                 "ping"
499                         + ((source == null) ? "" : (" -I " + source.getHostAddress()))
500                         + " "
501                         + address.getHostAddress()
502                         + " "
503                         + size
504                         + " "
505                         + count
506                         + " "
507                         + interval
508                         + " "
509                         + hopLimit
510                         + " "
511                         + timeout;
512         return executeCommand(cmd);
513     }
514 
getReceivedPacketsCount(List<String> stringList)515     private int getReceivedPacketsCount(List<String> stringList) {
516         Pattern pattern = Pattern.compile("([\\d]+) packets received");
517 
518         for (String message : stringList) {
519             Matcher matcher = pattern.matcher(message);
520             if (matcher.find()) {
521                 String packetCountStr = matcher.group(1);
522                 return Integer.parseInt(packetCountStr);
523             }
524         }
525         // No match found
526         return -1;
527     }
528 
529     /** Waits for an SRP server to be present in Network Data */
waitForSrpServer()530     public void waitForSrpServer() throws TimeoutException {
531         // CLI output:
532         // > srp client server
533         // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
534         // Done
535         waitFor(
536                 () -> {
537                     final String serverAddr = executeCommand("srp client server").get(0);
538                     final int lastColonIndex = serverAddr.lastIndexOf(':');
539                     final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
540                     return port > 0;
541                 },
542                 SERVICE_DISCOVERY_TIMEOUT);
543     }
544 
545     @FormatMethod
executeCommand(String commandFormat, Object... args)546     private List<String> executeCommand(String commandFormat, Object... args) {
547         return executeCommand(String.format(commandFormat, args));
548     }
549 
executeCommand(String command)550     private List<String> executeCommand(String command) {
551         try {
552             mWriter.write(command + "\n");
553             mWriter.flush();
554         } catch (IOException e) {
555             throw new IllegalStateException(
556                     "Failed to write the command " + command + " to ot-cli-ftd", e);
557         }
558         try {
559             return readUntilDone();
560         } catch (IOException e) {
561             throw new IllegalStateException(
562                     "Failed to read the ot-cli-ftd output of command: " + command, e);
563         }
564     }
565 
readLine()566     private String readLine() throws IOException {
567         final CompletableFuture<String> future = new CompletableFuture<>();
568         mReaderHandler.post(
569                 () -> {
570                     try {
571                         future.complete(mReader.readLine());
572                     } catch (IOException e) {
573                         future.completeExceptionally(e);
574                     }
575                 });
576         try {
577             return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS);
578         } catch (InterruptedException | ExecutionException | TimeoutException e) {
579             throw new IOException("Failed to read a line from ot-cli-ftd");
580         }
581     }
582 
readUntilDone()583     private List<String> readUntilDone() throws IOException {
584         ArrayList<String> result = new ArrayList<>();
585         String line;
586         while ((line = readLine()) != null) {
587             if (line.equals("Done")) {
588                 break;
589             }
590             if (line.startsWith("Error")) {
591                 throw new IOException("ot-cli-ftd reported an error: " + line);
592             }
593             if (!line.startsWith("> ")) {
594                 result.add(line);
595             }
596         }
597         return result;
598     }
599 
txtMapToHexString(Map<String, byte[]> txtMap)600     private static String txtMapToHexString(Map<String, byte[]> txtMap) {
601         if (txtMap == null) {
602             return "";
603         }
604         StringBuilder sb = new StringBuilder();
605         for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
606             int length = entry.getKey().length() + entry.getValue().length + 1;
607             sb.append(String.format("%02x", length));
608             sb.append(toHexString(entry.getKey()));
609             sb.append(toHexString("="));
610             sb.append(toHexString(entry.getValue()));
611         }
612         return sb.toString();
613     }
614 
toHexString(String s)615     private static String toHexString(String s) {
616         return toHexString(s.getBytes(StandardCharsets.UTF_8));
617     }
618 
toHexString(byte[] bytes)619     private static String toHexString(byte[] bytes) {
620         return base16().encode(bytes);
621     }
622 
623     private static final class DnsServiceCliOutputParser {
624         /** Returns the first match in the input of a given regex pattern. */
firstMatchOf(String input, String regex)625         private static Matcher firstMatchOf(String input, String regex) {
626             Matcher matcher = Pattern.compile(regex).matcher(input);
627             matcher.find();
628             return matcher;
629         }
630 
631         // Example: "Port:12345"
parsePort(String line)632         private static int parsePort(String line) {
633             return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1));
634         }
635 
636         // Example: "Host:Android.default.service.arpa."
parseHostname(String line)637         private static String parseHostname(String line) {
638             return firstMatchOf(line, "Host:(.+)").group(1);
639         }
640 
641         // Example: "HostAddress:2001:0:0:0:0:0:0:1"
parseHostAddress(String line)642         private static InetAddress parseHostAddress(String line) {
643             return InetAddresses.parseNumericAddress(
644                     firstMatchOf(line, "HostAddress:([^ ]+)").group(1));
645         }
646 
647         // Example: "TXT:[key1=0102, key2=03]"
parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo)648         private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) {
649             String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1);
650             for (String txtEntry : txtString.split(",")) {
651                 String[] nameAndValue = txtEntry.trim().split("=");
652                 String name = nameAndValue[0];
653                 String value = nameAndValue[1];
654                 byte[] bytes = new byte[value.length() / 2];
655                 for (int i = 0; i < value.length(); i += 2) {
656                     byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0'));
657                     bytes[i / 2] = b;
658                 }
659                 serviceInfo.setAttribute(name, bytes);
660             }
661         }
662     }
663 }
664