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