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