• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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 
17 package android.net.thread.utils
18 
19 import android.Manifest.permission.MANAGE_TEST_NETWORKS
20 import android.content.Context
21 import android.net.ConnectivityManager
22 import android.net.InetAddresses.parseNumericAddress
23 import android.net.IpPrefix
24 import android.net.LinkAddress
25 import android.net.LinkProperties
26 import android.net.MacAddress
27 import android.net.Network
28 import android.net.NetworkCapabilities
29 import android.net.NetworkRequest
30 import android.net.RouteInfo
31 import android.net.TestNetworkInterface
32 import android.net.nsd.NsdManager
33 import android.net.nsd.NsdServiceInfo
34 import android.net.thread.ActiveOperationalDataset
35 import android.net.thread.ThreadConfiguration
36 import android.net.thread.ThreadNetworkController
37 import android.os.Build
38 import android.os.Handler
39 import android.os.SystemClock
40 import android.system.OsConstants
41 import android.system.OsConstants.IPPROTO_ICMP
42 import android.util.Log
43 import androidx.test.core.app.ApplicationProvider
44 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
45 import com.android.net.module.util.IpUtils
46 import com.android.net.module.util.NetworkStackConstants
47 import com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET
48 import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
49 import com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN
50 import com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET
51 import com.android.net.module.util.Struct
52 import com.android.net.module.util.structs.Icmpv4Header
53 import com.android.net.module.util.structs.Icmpv6Header
54 import com.android.net.module.util.structs.Ipv4Header
55 import com.android.net.module.util.structs.Ipv6Header
56 import com.android.net.module.util.structs.PrefixInformationOption
57 import com.android.net.module.util.structs.RaHeader
58 import com.android.testutils.PollPacketReader
59 import com.android.testutils.TestNetworkTracker
60 import com.android.testutils.initTestNetwork
61 import com.android.testutils.runAsShell
62 import com.android.testutils.waitForIdle
63 import com.google.common.io.BaseEncoding
64 import com.google.common.util.concurrent.MoreExecutors
65 import com.google.common.util.concurrent.MoreExecutors.directExecutor
66 import com.google.common.util.concurrent.SettableFuture
67 import java.io.IOException
68 import java.lang.Byte.toUnsignedInt
69 import java.net.DatagramPacket
70 import java.net.DatagramSocket
71 import java.net.Inet4Address
72 import java.net.Inet6Address
73 import java.net.InetAddress
74 import java.net.InetSocketAddress
75 import java.net.SocketAddress
76 import java.nio.ByteBuffer
77 import java.time.Duration
78 import java.util.concurrent.CompletableFuture
79 import java.util.concurrent.ExecutionException
80 import java.util.concurrent.TimeUnit
81 import java.util.concurrent.TimeoutException
82 import java.util.function.Predicate
83 import java.util.function.Supplier
84 import org.junit.Assert
85 
86 /** Utilities for Thread integration tests. */
87 object IntegrationTestUtils {
88     private val TAG = IntegrationTestUtils::class.simpleName
89 
90     // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
91     // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
92     // seconds to be safe
93     @JvmField
94     val RESTART_JOIN_TIMEOUT: Duration = Duration.ofSeconds(40)
95 
96     @JvmField
97     val JOIN_TIMEOUT: Duration = Duration.ofSeconds(30)
98 
99     @JvmField
100     val LEAVE_TIMEOUT: Duration = Duration.ofSeconds(2)
101 
102     @JvmField
103     val CALLBACK_TIMEOUT: Duration = Duration.ofSeconds(1)
104 
105     @JvmField
106     val SERVICE_DISCOVERY_TIMEOUT: Duration = Duration.ofSeconds(20)
107 
108     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
109     private val DEFAULT_DATASET_TLVS: ByteArray = BaseEncoding.base16().decode(
110         ("0E080000000000010000000300001335060004001FFFE002"
111                 + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
112                 + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
113                 + "642D643961300102D9A00410A245479C836D551B9CA557F7"
114                 + "B9D351B40C0402A0FFF8")
115     )
116 
117     @JvmField
118     val DEFAULT_DATASET: ActiveOperationalDataset =
119         ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
120 
121     @JvmField
122     val DEFAULT_CONFIG = ThreadConfiguration.Builder().build()
123 
124     /**
125      * Waits for the given [Supplier] to be true until given timeout.
126      *
127      * @param condition the condition to check
128      * @param timeout the time to wait for the condition before throwing
129      * @throws TimeoutException if the condition is still not met when the timeout expires
130      */
131     @JvmStatic
132     @Throws(TimeoutException::class)
133     fun waitFor(condition: Supplier<Boolean>, timeout: Duration) {
134         val intervalMills: Long = 500
135         val timeoutMills = timeout.toMillis()
136 
137         var i: Long = 0
138         while (i < timeoutMills) {
139             if (condition.get()) {
140                 return
141             }
142             SystemClock.sleep(intervalMills)
143             i += intervalMills
144         }
145         if (condition.get()) {
146             return
147         }
148         throw TimeoutException("The condition failed to become true in $timeout")
149     }
150 
151     /**
152      * Creates a [PollPacketReader] given the [TestNetworkInterface] and [Handler].
153      *
154      * @param testNetworkInterface the TUN interface of the test network
155      * @param handler the handler to process the packets
156      * @return the [PollPacketReader]
157      */
158     @JvmStatic
159     fun newPacketReader(
160         testNetworkInterface: TestNetworkInterface, handler: Handler
161     ): PollPacketReader {
162         val fd = testNetworkInterface.fileDescriptor.fileDescriptor
163         val reader = PollPacketReader(handler, fd, testNetworkInterface.mtu)
164         handler.post { reader.start() }
165         handler.waitForIdle(timeoutMs = 5000)
166         return reader
167     }
168 
169     /**
170      * Waits for the Thread module to enter any state of the given `deviceRoles`.
171      *
172      * @param controller the [ThreadNetworkController]
173      * @param deviceRoles the desired device roles. See also [     ]
174      * @param timeout the time to wait for the expected state before throwing
175      * @return the [ThreadNetworkController.DeviceRole] after waiting
176      * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
177      * expires
178      */
179     @JvmStatic
180     @Throws(TimeoutException::class)
181     fun waitForStateAnyOf(
182         controller: ThreadNetworkController, deviceRoles: List<Int>, timeout: Duration
183     ): Int {
184         val future = SettableFuture.create<Int>()
185         val callback = ThreadNetworkController.StateCallback { newRole: Int ->
186             if (deviceRoles.contains(newRole)) {
187                 future.set(newRole)
188             }
189         }
190         controller.registerStateCallback(MoreExecutors.directExecutor(), callback)
191         try {
192             return future[timeout.toMillis(), TimeUnit.MILLISECONDS]
193         } catch (e: InterruptedException) {
194             throw TimeoutException(
195                 "The device didn't become an expected role in $timeout: $e.message"
196             )
197         } catch (e: ExecutionException) {
198             throw TimeoutException(
199                 "The device didn't become an expected role in $timeout: $e.message"
200             )
201         } finally {
202             controller.unregisterStateCallback(callback)
203         }
204     }
205 
206     /**
207      * Polls for a packet from a given [PollPacketReader] that satisfies the `filter`.
208      *
209      * @param packetReader a TUN packet reader
210      * @param filter the filter to be applied on the packet
211      * @return the first IPv6 packet that satisfies the `filter`. If it has waited for more
212      * than 3000ms to read the next packet, the method will return null
213      */
214     @JvmStatic
215     fun pollForPacket(packetReader: PollPacketReader, filter: Predicate<ByteArray>): ByteArray? {
216         var packet: ByteArray?
217         while ((packetReader.poll(3000 /* timeoutMs */, filter).also { packet = it }) != null) {
218             return packet
219         }
220         return null
221     }
222 
223     /** Returns `true` if `packet` is an ICMPv4 packet of given `type`.  */
224     @JvmStatic
225     fun isExpectedIcmpv4Packet(packet: ByteArray, type: Int): Boolean {
226         val buf = makeByteBuffer(packet)
227         val header = extractIpv4Header(buf) ?: return false
228         if (header.protocol != OsConstants.IPPROTO_ICMP.toByte()) {
229             return false
230         }
231         try {
232             return Struct.parse(Icmpv4Header::class.java, buf).type == type.toShort()
233         } catch (ignored: IllegalArgumentException) {
234             // It's fine that the passed in packet is malformed because it's could be sent
235             // by anybody.
236         }
237         return false
238     }
239 
240     /** Returns `true` if `packet` is an ICMPv6 packet of given `type`.  */
241     @JvmStatic
242     fun isExpectedIcmpv6Packet(packet: ByteArray, type: Int): Boolean {
243         val buf = makeByteBuffer(packet)
244         val header = extractIpv6Header(buf) ?: return false
245         if (header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
246             return false
247         }
248         try {
249             return Struct.parse(Icmpv6Header::class.java, buf).type == type.toShort()
250         } catch (ignored: IllegalArgumentException) {
251             // It's fine that the passed in packet is malformed because it's could be sent
252             // by anybody.
253         }
254         return false
255     }
256 
257     @JvmStatic
258     fun isFrom(packet: ByteArray, src: InetAddress): Boolean {
259         when (src) {
260             is Inet4Address -> return isFromIpv4Source(packet, src)
261             is Inet6Address -> return isFromIpv6Source(packet, src)
262             else -> return false
263         }
264     }
265 
266     @JvmStatic
267     fun isTo(packet: ByteArray, dest: InetAddress): Boolean {
268         when (dest) {
269             is Inet4Address -> return isToIpv4Destination(packet, dest)
270             is Inet6Address -> return isToIpv6Destination(packet, dest)
271             else -> return false
272         }
273     }
274 
275     private fun isFromIpv4Source(packet: ByteArray, src: Inet4Address): Boolean {
276         val header = extractIpv4Header(makeByteBuffer(packet))
277         return header?.srcIp == src
278     }
279 
280     private fun isFromIpv6Source(packet: ByteArray, src: Inet6Address): Boolean {
281         val header = extractIpv6Header(makeByteBuffer(packet))
282         return header?.srcIp == src
283     }
284 
285     private fun isToIpv4Destination(packet: ByteArray, dest: Inet4Address): Boolean {
286         val header = extractIpv4Header(makeByteBuffer(packet))
287         return header?.dstIp == dest
288     }
289 
290     private fun isToIpv6Destination(packet: ByteArray, dest: Inet6Address): Boolean {
291         val header = extractIpv6Header(makeByteBuffer(packet))
292         return header?.dstIp == dest
293     }
294 
295     private fun makeByteBuffer(packet: ByteArray): ByteBuffer {
296         return ByteBuffer.wrap(packet)
297     }
298 
299     private fun extractIpv4Header(buf: ByteBuffer): Ipv4Header? {
300         try {
301             return Struct.parse(Ipv4Header::class.java, buf)
302         } catch (ignored: IllegalArgumentException) {
303             // It's fine that the passed in packet is malformed because it's could be sent
304             // by anybody.
305         }
306         return null
307     }
308 
309     private fun extractIpv6Header(buf: ByteBuffer): Ipv6Header? {
310         try {
311             return Struct.parse(Ipv6Header::class.java, buf)
312         } catch (ignored: IllegalArgumentException) {
313             // It's fine that the passed in packet is malformed because it's could be sent
314             // by anybody.
315         }
316         return null
317     }
318 
319     /** Builds an ICMPv4 Echo Reply packet to respond to the given ICMPv4 Echo Request packet. */
320     @JvmStatic
321     fun buildIcmpv4EchoReply(request: ByteBuffer): ByteBuffer? {
322         val requestIpv4Header = Struct.parse(Ipv4Header::class.java, request) ?: return null
323         val requestIcmpv4Header = Struct.parse(Icmpv4Header::class.java, request) ?: return null
324 
325         val id = request.getShort()
326         val seq = request.getShort()
327 
328         val payload = ByteBuffer.allocate(4 + request.limit() - request.position())
329         payload.putShort(id)
330         payload.putShort(seq)
331         payload.put(request)
332         payload.rewind()
333 
334         val ipv4HeaderLen = Struct.getSize(Ipv4Header::class.java)
335         val Icmpv4HeaderLen = Struct.getSize(Icmpv4Header::class.java)
336         val payloadLen = payload.limit();
337 
338         val reply = ByteBuffer.allocate(ipv4HeaderLen + Icmpv4HeaderLen + payloadLen)
339 
340         // IPv4 header
341         val replyIpv4Header = Ipv4Header(
342             0 /* TYPE OF SERVICE */,
343             0.toShort().toInt()/* totalLength, calculate later */,
344             requestIpv4Header.id,
345             requestIpv4Header.flagsAndFragmentOffset,
346             0x40 /* ttl */,
347             IPPROTO_ICMP.toByte(),
348             0.toShort()/* checksum, calculate later */,
349             requestIpv4Header.dstIp /* srcIp */,
350             requestIpv4Header.srcIp /* dstIp */
351         )
352         replyIpv4Header.writeToByteBuffer(reply)
353 
354         // ICMPv4 header
355         val replyIcmpv4Header = Icmpv4Header(
356             0 /* type, ICMP_ECHOREPLY */,
357             requestIcmpv4Header.code,
358             0.toShort() /* checksum, calculate later */
359         )
360         replyIcmpv4Header.writeToByteBuffer(reply)
361 
362         // Payload
363         reply.put(payload)
364         reply.flip()
365 
366         // Populate the IPv4 totalLength field.
367         reply.putShort(
368             IPV4_LENGTH_OFFSET, (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen).toShort()
369         )
370 
371         // Populate the IPv4 header checksum field.
372         reply.putShort(
373             IPV4_CHECKSUM_OFFSET, IpUtils.ipChecksum(reply, 0 /* headerOffset */)
374         )
375 
376         // Populate the ICMP checksum field.
377         reply.putShort(
378             IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET, IpUtils.icmpChecksum(
379                 reply, IPV4_HEADER_MIN_LEN, Icmpv4HeaderLen + payloadLen
380             )
381         )
382 
383         return reply
384     }
385 
386     /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message.  */
387     @JvmStatic
388     fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
389         val pioList = ArrayList<PrefixInformationOption>()
390 
391         raMsg ?: return pioList
392 
393         val buf = ByteBuffer.wrap(raMsg)
394         val ipv6Header = try {
395             Struct.parse(Ipv6Header::class.java, buf)
396         } catch (e: IllegalArgumentException) {
397             // the packet is not IPv6
398             return pioList
399         }
400         if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
401             return pioList
402         }
403 
404         val icmpv6Header = Struct.parse(Icmpv6Header::class.java, buf)
405         if (icmpv6Header.type != NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT.toShort()) {
406             return pioList
407         }
408 
409         Struct.parse(RaHeader::class.java, buf)
410         while (buf.position() < raMsg.size) {
411             val currentPos = buf.position()
412             val type = toUnsignedInt(buf.get())
413             val length = toUnsignedInt(buf.get())
414             if (type == NetworkStackConstants.ICMPV6_ND_OPTION_PIO) {
415                 val pioBuf = ByteBuffer.wrap(
416                     buf.array(), currentPos, Struct.getSize(PrefixInformationOption::class.java)
417                 )
418                 val pio = Struct.parse(PrefixInformationOption::class.java, pioBuf)
419                 pioList.add(pio)
420 
421                 // Move ByteBuffer position to the next option.
422                 buf.position(
423                     currentPos + Struct.getSize(PrefixInformationOption::class.java)
424                 )
425             } else {
426                 // The length is in units of 8 octets.
427                 buf.position(currentPos + (length * 8))
428             }
429         }
430         return pioList
431     }
432 
433     /**
434      * Sends a UDP message to a destination.
435      *
436      * @param dstAddress the IP address of the destination
437      * @param dstPort the port of the destination
438      * @param message the message in UDP payload
439      * @throws IOException if failed to send the message
440      */
441     @JvmStatic
442     @Throws(IOException::class)
443     fun sendUdpMessage(dstAddress: InetAddress, dstPort: Int, message: String) {
444         val dstSockAddr: SocketAddress = InetSocketAddress(dstAddress, dstPort)
445 
446         DatagramSocket().use { socket ->
447             socket.connect(dstSockAddr)
448             val msgBytes = message.toByteArray()
449             val packet = DatagramPacket(msgBytes, msgBytes.size)
450             socket.send(packet)
451         }
452     }
453 
454     @JvmStatic
455     fun isInMulticastGroup(interfaceName: String, address: Inet6Address): Boolean {
456         val cmd = "ip -6 maddr show dev $interfaceName"
457         val output: String = runShellCommandOrThrow(cmd)
458         for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
459             if (line.contains(address.hostAddress!!)) {
460                 return true
461             }
462         }
463         return false
464     }
465 
466     @JvmStatic
467     fun getIpv6LinkAddresses(interfaceName: String): List<LinkAddress> {
468         val addresses: MutableList<LinkAddress> = ArrayList()
469         val cmd = " ip -6 addr show dev $interfaceName"
470         val output: String = runShellCommandOrThrow(cmd)
471 
472         for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
473             if (line.contains("inet6")) {
474                 addresses.add(parseAddressLine(line))
475             }
476         }
477 
478         return addresses
479     }
480 
481     /** Returns the list of [InetAddress] of the given network. */
482     @JvmStatic
483     fun getIpv6Addresses(interfaceName: String): List<InetAddress> {
484         return getIpv6LinkAddresses(interfaceName).map { it.address }
485     }
486 
487     /** Return the first discovered service of `serviceType`. */
488     @JvmStatic
489     @Throws(Exception::class)
490     fun discoverService(nsdManager: NsdManager, serviceType: String): NsdServiceInfo {
491         return discoverService(nsdManager, serviceType, null)
492     }
493 
494     /**
495      * Returns the service that matches `serviceType` and `serviceName`.
496      *
497      * If `serviceName` is null, returns the first discovered service. `serviceName` is not case
498      * sensitive.
499      */
500     @JvmStatic
501     @Throws(Exception::class)
502     fun discoverService(nsdManager: NsdManager, serviceType: String, serviceName: String?):
503             NsdServiceInfo {
504         val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
505         val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
506             override fun onServiceFound(serviceInfo: NsdServiceInfo) {
507                 Log.d(TAG, "onServiceFound: $serviceInfo")
508                 if (serviceName == null ||
509                         serviceInfo.getServiceName().equals(serviceName, true /* ignore case */)) {
510                     serviceInfoFuture.complete(serviceInfo)
511                 }
512             }
513         }
514         nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
515         try {
516             serviceInfoFuture[SERVICE_DISCOVERY_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS]
517         } finally {
518             nsdManager.stopServiceDiscovery(listener)
519         }
520 
521         return serviceInfoFuture.get()
522     }
523 
524     /**
525      * Returns the [NsdServiceInfo] when a service instance of `serviceType` gets lost.
526      */
527     @JvmStatic
528     fun discoverForServiceLost(
529         nsdManager: NsdManager,
530         serviceType: String?,
531         serviceInfoFuture: CompletableFuture<NsdServiceInfo?>
532     ): NsdManager.DiscoveryListener {
533         val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
534             override fun onServiceLost(serviceInfo: NsdServiceInfo): Unit {
535                 serviceInfoFuture.complete(serviceInfo)
536             }
537         }
538         nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
539         return listener
540     }
541 
542     /** Resolves the service.  */
543     @JvmStatic
544     @Throws(Exception::class)
545     fun resolveService(nsdManager: NsdManager, serviceInfo: NsdServiceInfo): NsdServiceInfo {
546         return resolveServiceUntil(nsdManager, serviceInfo) { true }
547     }
548 
549     /** Returns the first resolved service that satisfies the `predicate`.  */
550     @JvmStatic
551     @Throws(Exception::class)
552     fun resolveServiceUntil(
553         nsdManager: NsdManager, serviceInfo: NsdServiceInfo, predicate: Predicate<NsdServiceInfo>
554     ): NsdServiceInfo {
555         val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
556         val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
557             override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
558                 Log.d(TAG, "onServiceUpdated: $serviceInfo")
559                 if (predicate.test(serviceInfo)) {
560                     resolvedServiceInfoFuture.complete(serviceInfo)
561                 }
562             }
563         }
564         nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback)
565         try {
566             return resolvedServiceInfoFuture[
567                 SERVICE_DISCOVERY_TIMEOUT.toMillis(),
568                 TimeUnit.MILLISECONDS]
569         } finally {
570             nsdManager.unregisterServiceInfoCallback(callback)
571         }
572     }
573 
574     @JvmStatic
575     fun getPrefixesFromNetData(netData: String): String {
576         val startIdx = netData.indexOf("Prefixes:")
577         val endIdx = netData.indexOf("Routes:")
578         return netData.substring(startIdx, endIdx)
579     }
580 
581     @JvmStatic
582     @Throws(Exception::class)
583     fun getThreadNetwork(timeout: Duration): Network {
584         val networkFuture = CompletableFuture<Network>()
585         val cm =
586             ApplicationProvider.getApplicationContext<Context>()
587                 .getSystemService(ConnectivityManager::class.java)
588         val networkRequestBuilder =
589             NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
590         // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
591         // a Thread network.
592         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
593             networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
594         }
595         val networkRequest = networkRequestBuilder.build()
596         val networkCallback: ConnectivityManager.NetworkCallback =
597             object : ConnectivityManager.NetworkCallback() {
598                 override fun onAvailable(network: Network) {
599                     networkFuture.complete(network)
600                 }
601             }
602         cm.registerNetworkCallback(networkRequest, networkCallback)
603         return networkFuture[timeout.toSeconds(), TimeUnit.SECONDS]
604     }
605 
606     /**
607      * Let the FTD join the specified Thread network and wait for it becomes a Child or Router.
608      */
609     @JvmStatic
610     @Throws(Exception::class)
611     fun joinNetworkAndWait(ftd: FullThreadDevice, dataset: ActiveOperationalDataset) {
612         ftd.factoryReset()
613         ftd.joinNetwork(dataset)
614         ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
615     }
616 
617     /**
618      * Let the FTD join the specified Thread network and wait for border routing to be available.
619      *
620      * @return the OMR address
621      */
622     @JvmStatic
623     @Throws(Exception::class)
624     fun joinNetworkAndWaitForOmr(
625         ftd: FullThreadDevice, dataset: ActiveOperationalDataset
626     ): Inet6Address {
627         ftd.factoryReset()
628         ftd.joinNetwork(dataset)
629         ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
630         waitFor({ ftd.omrAddress != null }, Duration.ofSeconds(60))
631         Assert.assertNotNull(ftd.omrAddress)
632         return ftd.omrAddress
633     }
634 
635     /** Enables Thread and joins the specified Thread network. */
636     @JvmStatic
637     fun enableThreadAndJoinNetwork(dataset: ActiveOperationalDataset) {
638         val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
639         val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
640 
641         // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
642         controller.leaveAndWait();
643 
644         controller.setEnabledAndWait(true);
645         controller.joinAndWait(dataset);
646     }
647 
648     /** Enables Border Router and joins the specified Thread network. */
649     @JvmStatic
650     fun enableBorderRouterAndJoinNetwork(dataset: ActiveOperationalDataset) {
651         val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
652         val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
653 
654         // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
655         controller.leaveAndWait();
656 
657         controller.setEnabledAndWait(true);
658         val config = ThreadConfiguration.Builder().setBorderRouterEnabled(true).build();
659         controller.setConfigurationAndWait(config);
660         controller.joinAndWait(dataset);
661     }
662 
663     /** Leaves the Thread network and disables Thread. */
664     @JvmStatic
665     fun leaveNetworkAndDisableThread() {
666         val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
667         val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
668         controller.leaveAndWait();
669         controller.setEnabledAndWait(false);
670     }
671 
672     private open class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
673         override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
674         override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
675         override fun onDiscoveryStarted(serviceType: String) {}
676         override fun onDiscoveryStopped(serviceType: String) {}
677         override fun onServiceFound(serviceInfo: NsdServiceInfo) {}
678         override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
679     }
680 
681     private open class DefaultServiceInfoCallback : NsdManager.ServiceInfoCallback {
682         override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
683         override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {}
684         override fun onServiceLost(): Unit {}
685         override fun onServiceInfoCallbackUnregistered() {}
686     }
687 
688     /**
689      * Parses a line of output from "ip -6 addr show" into a [LinkAddress].
690      *
691      * Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
692      */
693     private fun parseAddressLine(line: String): LinkAddress {
694         val parts = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }.toTypedArray()
695         val addressString = parts[1]
696         val pieces = addressString.split("/".toRegex(), limit = 2).toTypedArray()
697         val prefixLength = pieces[1].toInt()
698         val address = parseNumericAddress(pieces[0])
699         val deprecationTimeMillis =
700             if (line.contains("deprecated")) SystemClock.elapsedRealtime()
701             else LinkAddress.LIFETIME_PERMANENT
702 
703         return LinkAddress(
704             address, prefixLength,
705             0 /* flags */, 0 /* scope */,
706             deprecationTimeMillis, LinkAddress.LIFETIME_PERMANENT /* expirationTime */
707         )
708     }
709 
710     /**
711      * Stop the ot-daemon by shell command.
712      */
713     @JvmStatic
714     fun stopOtDaemon() {
715         runShellCommandOrThrow("stop ot-daemon")
716     }
717 }
718