blob: 66e7713df5bb0234149cf96e2c27d659b31031e0 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.cts
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.app.compat.CompatChanges
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkRequest
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
import android.net.TestNetworkSpecifier
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StartDiscoveryFailed
import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StopDiscoveryFailed
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveStopped
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.StopResolutionFailed
import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.RegisterCallbackFailed
import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
import android.net.cts.util.CtsNetUtils
import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.DiscoveryListener
import android.net.nsd.NsdManager.RegistrationListener
import android.net.nsd.NsdManager.ResolveListener
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.Process.myTid
import android.platform.test.annotations.AppModeFull
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.TrackRecord
import com.android.networkstack.apishim.NsdShimImpl
import com.android.networkstack.apishim.common.NsdShim
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import com.android.testutils.waitForIdle
import java.io.File
import java.net.ServerSocket
import java.nio.charset.StandardCharsets
import java.util.Random
import java.util.concurrent.Executor
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
private const val TAG = "NsdManagerTest"
private const val TIMEOUT_MS = 2000L
private const val NO_CALLBACK_TIMEOUT_MS = 200L
// Registration may take a long time if there are devices with the same hostname on the network,
// as the device needs to try another name and probe again. This is especially true since when using
// mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
// tried sequentially
private const val REGISTRATION_TIMEOUT_MS = 10_000L
private const val DBG = false
private val nsdShim = NsdShimImpl.newInstance()
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(AndroidJUnit4::class)
class NsdManagerTest {
// Rule used to filter CtsNetTestCasesMaxTargetSdkXX
@get:Rule
val ignoreRule = DevSdkIgnoreRule()
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
private lateinit var testNetwork1: TestTapNetwork
private lateinit var testNetwork2: TestTapNetwork
private class TestTapNetwork(
val iface: TestNetworkInterface,
val requestCb: NetworkCallback,
val agent: TestableNetworkAgent,
val network: Network
) {
fun close(cm: ConnectivityManager) {
cm.unregisterNetworkCallback(requestCb)
agent.unregister()
iface.fileDescriptor.close()
agent.waitForIdle(TIMEOUT_MS)
}
}
private interface NsdEvent
private open class NsdRecord<T : NsdEvent> private constructor(
private val history: ArrayTrackRecord<T>,
private val expectedThreadId: Int? = null
) : TrackRecord<T> by history {
constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
val nextEvents = history.newReadHead()
override fun add(e: T): Boolean {
if (expectedThreadId != null) {
assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
}
return history.add(e)
}
inline fun <reified V : NsdEvent> expectCallbackEventually(
timeoutMs: Long = TIMEOUT_MS,
crossinline predicate: (V) -> Boolean = { true }
): V = nextEvents.poll(timeoutMs) { e -> e is V && predicate(e) } as V?
?: fail("Callback for ${V::class.java.simpleName} not seen after $timeoutMs ms")
inline fun <reified V : NsdEvent> expectCallback(timeoutMs: Long = TIMEOUT_MS): V {
val nextEvent = nextEvents.poll(timeoutMs)
assertNotNull(nextEvent, "No callback received after $timeoutMs ms, expected " +
"${V::class.java.simpleName}")
assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
nextEvent.javaClass.simpleName)
return nextEvent
}
inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
val cb = nextEvents.poll(timeoutMs)
assertNull(cb, "Expected no callback but got $cb")
}
}
private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
sealed class RegistrationEvent : NsdEvent {
abstract val serviceInfo: NsdServiceInfo
data class RegistrationFailed(
override val serviceInfo: NsdServiceInfo,
val errorCode: Int
) : RegistrationEvent()
data class UnregistrationFailed(
override val serviceInfo: NsdServiceInfo,
val errorCode: Int
) : RegistrationEvent()
data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
RegistrationEvent()
data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
RegistrationEvent()
}
override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
add(RegistrationFailed(si, err))
}
override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
add(UnregistrationFailed(si, err))
}
override fun onServiceRegistered(si: NsdServiceInfo) {
add(ServiceRegistered(si))
}
override fun onServiceUnregistered(si: NsdServiceInfo) {
add(ServiceUnregistered(si))
}
}
private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
sealed class DiscoveryEvent : NsdEvent {
data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
DiscoveryEvent()
data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
DiscoveryEvent()
data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
}
override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
add(StartDiscoveryFailed(serviceType, err))
}
override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
add(StopDiscoveryFailed(serviceType, err))
}
override fun onDiscoveryStarted(serviceType: String) {
add(DiscoveryStarted(serviceType))
}
override fun onDiscoveryStopped(serviceType: String) {
add(DiscoveryStopped(serviceType))
}
override fun onServiceFound(si: NsdServiceInfo) {
add(ServiceFound(si))
}
override fun onServiceLost(si: NsdServiceInfo) {
add(ServiceLost(si))
}
fun waitForServiceDiscovered(
serviceName: String,
expectedNetwork: Network? = null
): NsdServiceInfo {
return expectCallbackEventually<ServiceFound> {
it.serviceInfo.serviceName == serviceName &&
(expectedNetwork == null ||
expectedNetwork == nsdShim.getNetwork(it.serviceInfo))
}.serviceInfo
}
}
private class NsdResolveRecord : ResolveListener,
NsdRecord<NsdResolveRecord.ResolveEvent>() {
sealed class ResolveEvent : NsdEvent {
data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
ResolveEvent()
data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
data class ResolveStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
ResolveEvent()
}
override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
add(ResolveFailed(si, err))
}
override fun onServiceResolved(si: NsdServiceInfo) {
add(ServiceResolved(si))
}
override fun onResolveStopped(si: NsdServiceInfo) {
add(ResolveStopped(si))
}
override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
add(StopResolutionFailed(si, err))
}
}
private class NsdServiceInfoCallbackRecord : NsdShim.ServiceInfoCallbackShim,
NsdRecord<NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent>() {
sealed class ServiceInfoCallbackEvent : NsdEvent {
data class RegisterCallbackFailed(val errorCode: Int) : ServiceInfoCallbackEvent()
data class ServiceUpdated(val serviceInfo: NsdServiceInfo) : ServiceInfoCallbackEvent()
object ServiceUpdatedLost : ServiceInfoCallbackEvent()
object UnregisterCallbackSucceeded : ServiceInfoCallbackEvent()
}
override fun onServiceInfoCallbackRegistrationFailed(err: Int) {
add(RegisterCallbackFailed(err))
}
override fun onServiceUpdated(si: NsdServiceInfo) {
add(ServiceUpdated(si))
}
override fun onServiceLost() {
add(ServiceUpdatedLost)
}
override fun onServiceInfoCallbackUnregistered() {
add(UnregisterCallbackSucceeded)
}
}
@Before
fun setUp() {
handlerThread.start()
if (TestUtils.shouldTestTApis()) {
runAsShell(MANAGE_TEST_NETWORKS) {
testNetwork1 = createTestNetwork()
testNetwork2 = createTestNetwork()
}
}
}
private fun createTestNetwork(): TestTapNetwork {
val tnm = context.getSystemService(TestNetworkManager::class.java)
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(NetworkRequest.Builder()
.removeCapability(NET_CAPABILITY_TRUSTED)
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(testNetworkSpecifier)
.build(), cb)
val agent = registerTestNetworkAgent(iface.interfaceName)
val network = agent.network ?: fail("Registered agent should have a network")
// The network has no INTERNET capability, so will be marked validated immediately
cb.expectAvailableThenValidatedCallbacks(network, TIMEOUT_MS)
return TestTapNetwork(iface, cb, agent, network)
}
private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
val agent = TestableNetworkAgent(context, handlerThread.looper,
NetworkCapabilities().apply {
removeCapability(NET_CAPABILITY_TRUSTED)
addTransportType(TRANSPORT_TEST)
setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
},
LinkProperties().apply {
interfaceName = ifaceName
},
NetworkAgentConfig.Builder().build())
agent.register()
agent.markConnected()
return agent
}
@After
fun tearDown() {
if (TestUtils.shouldTestTApis()) {
runAsShell(MANAGE_TEST_NETWORKS) {
testNetwork1.close(cm)
testNetwork2.close(cm)
}
}
handlerThread.waitForIdle(TIMEOUT_MS)
handlerThread.quitSafely()
}
@Test
fun testNsdManager() {
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = serviceName
// Test binary data with various bytes
val testByteArray = byteArrayOf(-128, 127, 2, 1, 0, 1, 2)
// Test string data with 256 characters (25 blocks of 10 characters + 6)
val string256 = "1_________2_________3_________4_________5_________6_________" +
"7_________8_________9_________10________11________12________13________" +
"14________15________16________17________18________19________20________" +
"21________22________23________24________25________123456"
// Illegal attributes
listOf(
Triple(null, null, "null key"),
Triple("", null, "empty key"),
Triple(string256, null, "key with 256 characters"),
Triple("key", string256.substring(3),
"key+value combination with more than 255 characters"),
Triple("key", string256.substring(4), "key+value combination with 255 characters"),
Triple("\u0019", null, "key with invalid character"),
Triple("=", null, "key with invalid character"),
Triple("\u007f", null, "key with invalid character")
).forEach {
assertFailsWith<IllegalArgumentException>(
"Setting invalid ${it.third} unexpectedly succeeded") {
si.setAttribute(it.first, it.second)
}
}
// Allowed attributes
si.setAttribute("booleanAttr", null as String?)
si.setAttribute("keyValueAttr", "value")
si.setAttribute("keyEqualsAttr", "=")
si.setAttribute(" whiteSpaceKeyValueAttr ", " value ")
si.setAttribute("binaryDataAttr", testByteArray)
si.setAttribute("nullBinaryDataAttr", null as ByteArray?)
si.setAttribute("emptyBinaryDataAttr", byteArrayOf())
si.setAttribute("longkey", string256.substring(9))
val socket = ServerSocket(0)
val localPort = socket.localPort
si.port = localPort
if (DBG) Log.d(TAG, "Port = $localPort")
val registrationRecord = NsdRegistrationRecord()
// Test registering without an Executor
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
REGISTRATION_TIMEOUT_MS).serviceInfo
val discoveryRecord = NsdDiscoveryRecord()
// Test discovering without an Executor
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
// Expect discovery started
discoveryRecord.expectCallback<DiscoveryStarted>()
// Expect a service record to be discovered
val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
// Test resolving without an Executor
val resolveRecord = NsdResolveRecord()
nsdManager.resolveService(foundInfo, resolveRecord)
val resolvedService = resolveRecord.expectCallback<ServiceResolved>().serviceInfo
// Check Txt attributes
assertEquals(8, resolvedService.attributes.size)
assertTrue(resolvedService.attributes.containsKey("booleanAttr"))
assertNull(resolvedService.attributes["booleanAttr"])
assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
assertEquals(" value ",
resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
assertNull(resolvedService.attributes["nullBinaryDataAttr"])
assertTrue(resolvedService.attributes.containsKey("emptyBinaryDataAttr"))
assertNull(resolvedService.attributes["emptyBinaryDataAttr"])
assertEquals(localPort, resolvedService.port)
// Unregister the service
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
// Expect a callback for service lost
discoveryRecord.expectCallbackEventually<ServiceLost> {
it.serviceInfo.serviceName == serviceName
}
// Register service again to see if NsdManager can discover it
val si2 = NsdServiceInfo()
si2.serviceType = serviceType
si2.serviceName = serviceName
si2.port = localPort
val registrationRecord2 = NsdRegistrationRecord()
nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>(
REGISTRATION_TIMEOUT_MS).serviceInfo
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
// Resolve the service
val resolveRecord2 = NsdResolveRecord()
nsdManager.resolveService(foundInfo2, resolveRecord2)
val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
// Check that the resolved service doesn't have any TXT records
assertEquals(0, resolvedService2.attributes.size)
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
nsdManager.unregisterService(registrationRecord2)
registrationRecord2.expectCallback<ServiceUnregistered>()
}
@Test
fun testNsdManager_DiscoverOnNetwork() {
// This test requires shims supporting T+ APIs (discovering on specific network)
assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = this.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
val registrationRecord = NsdRegistrationRecord()
val registeredInfo = registerService(registrationRecord, si)
tryTest {
val discoveryRecord = NsdDiscoveryRecord()
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
serviceName, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Rewind to ensure the service is not found on the other interface
discoveryRecord.nextEvents.rewind(0)
assertNull(discoveryRecord.nextEvents.poll(timeoutMs = 100L) {
it is ServiceFound &&
it.serviceInfo.serviceName == registeredInfo.serviceName &&
nsdShim.getNetwork(it.serviceInfo) != testNetwork1.network
}, "The service should not be found on this network")
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testNsdManager_DiscoverWithNetworkRequest() {
// This test requires shims supporting T+ APIs (discovering on network request)
assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = this.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
val handler = Handler(handlerThread.looper)
val executor = Executor { handler.post(it) }
val registrationRecord = NsdRegistrationRecord(expectedThreadId = handlerThread.threadId)
val registeredInfo1 = registerService(registrationRecord, si, executor)
val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
tryTest {
val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
NetworkRequest.Builder()
.removeCapability(NET_CAPABILITY_TRUSTED)
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(specifier)
.build(),
executor, discoveryRecord)
val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>()
assertEquals(serviceType, discoveryStarted.serviceType)
val serviceDiscovered = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo1.serviceName, serviceDiscovered.serviceInfo.serviceName)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered.serviceInfo))
// Unregister, then register the service back: it should be lost and found again
nsdManager.unregisterService(registrationRecord)
val serviceLost1 = discoveryRecord.expectCallback<ServiceLost>()
assertEquals(registeredInfo1.serviceName, serviceLost1.serviceInfo.serviceName)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo))
registrationRecord.expectCallback<ServiceUnregistered>()
val registeredInfo2 = registerService(registrationRecord, si, executor)
val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
// Teardown, then bring back up a network on the test interface: the service should
// go away, then come back
testNetwork1.agent.unregister()
val serviceLost = discoveryRecord.expectCallback<ServiceLost>()
assertEquals(registeredInfo2.serviceName, serviceLost.serviceInfo.serviceName)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost.serviceInfo))
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
registerTestNetworkAgent(testNetwork1.iface.interfaceName)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered3.serviceInfo.serviceName)
assertEquals(newNetwork, nsdShim.getNetwork(serviceDiscovered3.serviceInfo))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
// This test requires shims supporting T+ APIs (discovering on network request)
assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = this.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
val handler = Handler(handlerThread.looper)
val executor = Executor { handler.post(it) }
val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
tryTest {
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
NetworkRequest.Builder()
.removeCapability(NET_CAPABILITY_TRUSTED)
.addTransportType(TRANSPORT_TEST)
// Specified network does not have this capability
.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
.setNetworkSpecifier(specifier)
.build(),
executor, discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStarted>()
} cleanup {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
}
}
@Test
fun testNsdManager_ResolveOnNetwork() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = this.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
val registrationRecord = NsdRegistrationRecord()
val registeredInfo = registerService(registrationRecord, si)
tryTest {
val resolveRecord = NsdResolveRecord()
val discoveryRecord = NsdDiscoveryRecord()
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
val foundInfo1 = discoveryRecord.waitForServiceDiscovered(
serviceName, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1))
// Rewind as the service could be found on each interface in any order
discoveryRecord.nextEvents.rewind(0)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
serviceName, testNetwork2.network)
assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
nsdShim.resolveService(nsdManager, foundInfo1, Executor { it.run() }, resolveRecord)
val cb = resolveRecord.expectCallback<ServiceResolved>()
cb.serviceInfo.let {
// Resolved service type has leading dot
assertEquals(".$serviceType", it.serviceType)
assertEquals(registeredInfo.serviceName, it.serviceName)
assertEquals(si.port, it.port)
assertEquals(testNetwork1.network, nsdShim.getNetwork(it))
}
// TODO: check that MDNS packets are sent only on testNetwork1.
} cleanupStep {
nsdManager.unregisterService(registrationRecord)
} cleanup {
registrationRecord.expectCallback<ServiceUnregistered>()
}
}
@Test
fun testNsdManager_RegisterOnNetwork() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = serviceType
si.serviceName = this.serviceName
si.network = testNetwork1.network
si.port = 12345 // Test won't try to connect so port does not matter
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
registerService(registrationRecord, si)
val discoveryRecord = NsdDiscoveryRecord()
val discoveryRecord2 = NsdDiscoveryRecord()
val discoveryRecord3 = NsdDiscoveryRecord()
tryTest {
// Discover service on testNetwork1.
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, discoveryRecord)
// Expect that service is found on testNetwork1
val foundInfo = discoveryRecord.waitForServiceDiscovered(
serviceName, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Discover service on testNetwork2.
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork2.network, Executor { it.run() }, discoveryRecord2)
// Expect that discovery is started then no other callbacks.
discoveryRecord2.expectCallback<DiscoveryStarted>()
discoveryRecord2.assertNoCallback()
// Discover service on all networks (not specify any network).
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
// Expect that service is found on testNetwork1
val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
serviceName, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord2)
discoveryRecord2.expectCallback<DiscoveryStopped>()
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
val si = NsdServiceInfo().apply {
serviceType = this@NsdManagerTest.serviceType
serviceName = serviceNames
port = 12345 // Test won't try to connect so port does not matter
}
// Register the service name which contains non-standard characters.
val registrationRecord = NsdRegistrationRecord()
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
tryTest {
// Discover that service name.
val discoveryRecord = NsdDiscoveryRecord()
nsdManager.discoverServices(
serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
)
val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
// Expect that resolving the service name works properly even service name contains
// non-standard characters.
val resolveRecord = NsdResolveRecord()
nsdManager.resolveService(foundInfo, resolveRecord)
val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
} cleanupStep {
nsdManager.unregisterService(registrationRecord)
} cleanup {
registrationRecord.expectCallback<ServiceUnregistered>()
}
}
@Test @CtsNetTestCasesMaxTargetSdk30("Socket is started with the service up to target SDK 30")
fun testManagerCreatesLegacySocket() {
nsdManager // Ensure the lazy-init member is initialized, so NsdManager is created
val socket = File("/dev/socket/mdnsd")
val timeout = System.currentTimeMillis() + TIMEOUT_MS
while (!socket.exists() && System.currentTimeMillis() < timeout) {
Thread.sleep(10)
}
assertTrue("$socket was not found after $TIMEOUT_MS ms", socket.exists())
}
// The compat change is part of a connectivity module update that applies to T+
@ConnectivityModuleTest @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@Test @CtsNetTestCasesMaxTargetSdk30("Socket is started with the service up to target SDK 30")
fun testManagerCreatesLegacySocket_CompatChange() {
// The socket may have been already created by some other app, or some other test, in which
// case this test cannot verify creation. At least verify that the compat change is
// disabled in a process with max SDK 30; unit tests already verify that start is requested
// when the compat change is disabled.
// Note that before T the compat constant had a different int value.
assertFalse(CompatChanges.isChangeEnabled(
NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
}
@Test
fun testStopServiceResolution() {
// This test requires shims supporting U+ APIs (NsdManager.stopServiceResolution)
assumeTrue(TestUtils.shouldTestUApis())
val si = NsdServiceInfo()
si.serviceType = this@NsdManagerTest.serviceType
si.serviceName = this@NsdManagerTest.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
val resolveRecord = NsdResolveRecord()
// Try to resolve an unknown service then stop it immediately.
// Expected ResolveStopped callback.
nsdShim.resolveService(nsdManager, si, { it.run() }, resolveRecord)
nsdShim.stopServiceResolution(nsdManager, resolveRecord)
val stoppedCb = resolveRecord.expectCallback<ResolveStopped>()
assertEquals(si.serviceName, stoppedCb.serviceInfo.serviceName)
assertEquals(si.serviceType, stoppedCb.serviceInfo.serviceType)
}
@Test
fun testRegisterServiceInfoCallback() {
// This test requires shims supporting U+ APIs (NsdManager.subscribeService)
assumeTrue(TestUtils.shouldTestUApis())
// Ensure Wi-Fi network connected and get addresses
val wifiNetwork = ctsNetUtils.ensureWifiConnected()
val lp = cm.getLinkProperties(wifiNetwork)
assertNotNull(lp)
val addresses = lp.addresses
assertFalse(addresses.isEmpty())
val si = NsdServiceInfo().apply {
serviceType = this@NsdManagerTest.serviceType
serviceName = this@NsdManagerTest.serviceName
network = wifiNetwork
port = 12345 // Test won't try to connect so port does not matter
}
// Register service on Wi-Fi network
val registrationRecord = NsdRegistrationRecord()
registerService(registrationRecord, si)
val discoveryRecord = NsdDiscoveryRecord()
val cbRecord = NsdServiceInfoCallbackRecord()
tryTest {
// Discover service on Wi-Fi network.
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
wifiNetwork, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
serviceName, wifiNetwork)
// Subscribe to service and check the addresses are the same as Wi-Fi addresses
nsdShim.registerServiceInfoCallback(nsdManager, foundInfo, { it.run() }, cbRecord)
for (i in addresses.indices) {
val subscribeCb = cbRecord.expectCallback<ServiceUpdated>()
assertEquals(foundInfo.serviceName, subscribeCb.serviceInfo.serviceName)
val hostAddresses = subscribeCb.serviceInfo.hostAddresses
assertEquals(i + 1, hostAddresses.size)
for (hostAddress in hostAddresses) {
assertTrue(addresses.contains(hostAddress))
}
}
} cleanupStep {
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
discoveryRecord.expectCallback<ServiceLost>()
cbRecord.expectCallback<ServiceUpdatedLost>()
} cleanupStep {
// Cancel subscription and check stop callback received.
nsdShim.unregisterServiceInfoCallback(nsdManager, cbRecord)
cbRecord.expectCallback<UnregisterCallbackSucceeded>()
} cleanup {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
}
}
/**
* Register a service and return its registration record.
*/
private fun registerService(
record: NsdRegistrationRecord,
si: NsdServiceInfo,
executor: Executor = Executor { it.run() }
): NsdServiceInfo {
nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, executor, record)
// We may not always get the name that we tried to register;
// This events tells us the name that was registered.
val cb = record.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
return cb.serviceInfo
}
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdShim.resolveService(nsdManager, discoveredInfo, Executor { it.run() }, record)
val resolvedCb = record.expectCallback<ServiceResolved>()
assertEquals(discoveredInfo.serviceName, resolvedCb.serviceInfo.serviceName)
return resolvedCb.serviceInfo
}
}
private fun ByteArray?.utf8ToString(): String {
if (this == null) return ""
return String(this, StandardCharsets.UTF_8)
}