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