Merge "Add a method to apply a set of network preferences to a user profile"
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6847c74..bc1d002 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -12,6 +12,7 @@
maze@google.com
nuccachen@google.com
paulhu@google.com
+prohr@google.com
reminv@google.com
satk@google.com
waynema@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 90312a4..06d4416 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -61,6 +61,9 @@
"path": "frameworks/base/core/java/android/net"
},
{
+ "path": "frameworks/opt/net/ethernet"
+ },
+ {
"path": "packages/modules/NetworkStack"
},
{
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 1747450..e4faf85 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -4042,11 +4042,11 @@
config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
INetd.PERMISSION_NONE,
(nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
- getVpnType(nai));
+ getVpnType(nai), /*excludeLocalRoutes=*/ false);
} else {
config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
- VpnManager.TYPE_VPN_NONE);
+ VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
}
mNetd.networkCreate(config);
mDnsResolver.createNetworkCache(nai.network.getNetId());
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f72a458..b684068 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -25,6 +25,9 @@
"cts-tradefed",
"tradefed",
],
+ static_libs: [
+ "modules-utils-build-testing",
+ ],
// Tag this module as a cts test artifact
test_suites: [
"cts",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 63572c3..12e7d33 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -18,12 +18,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- ],
+java_defaults {
+ name: "CtsHostsideNetworkTestsAppDefaults",
platform_apis: true,
static_libs: [
"CtsHostsideNetworkTestsAidl",
@@ -48,3 +44,28 @@
"sts",
],
}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAppDefaults",
+ ],
+ static_libs: [
+ "NetworkStackApiStableShims",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsAppNext",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAppDefaults",
+ "ConnectivityNextEnableDefaults",
+ ],
+ static_libs: [
+ "NetworkStackApiCurrentShims",
+ ],
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index 855abf7..449454e 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -18,17 +18,26 @@
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.IpPrefix;
import android.net.Network;
+import android.net.NetworkUtils;
import android.net.ProxyInfo;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
import java.io.IOException;
import java.net.InetAddress;
-import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
public class MyVpnService extends VpnService {
@@ -67,39 +76,62 @@
(underlyingNetworks != null) ? underlyingNetworks.toArray(new Network[0]) : null);
}
- private void start(String packageName, Intent intent) {
- Builder builder = new Builder();
+ private String parseIpAndMaskListArgument(String packageName, Intent intent, String argName,
+ BiConsumer<InetAddress, Integer> consumer) {
+ final String addresses = intent.getStringExtra(packageName + "." + argName);
- String addresses = intent.getStringExtra(packageName + ".addresses");
- if (addresses != null) {
- String[] addressArray = addresses.split(",");
- for (int i = 0; i < addressArray.length; i++) {
- String[] prefixAndMask = addressArray[i].split("/");
- try {
- InetAddress address = InetAddress.getByName(prefixAndMask[0]);
- int prefixLength = Integer.parseInt(prefixAndMask[1]);
- builder.addAddress(address, prefixLength);
- } catch (UnknownHostException|NumberFormatException|
- ArrayIndexOutOfBoundsException e) {
- continue;
- }
- }
+ if (TextUtils.isEmpty(addresses)) {
+ return null;
}
- String routes = intent.getStringExtra(packageName + ".routes");
- if (routes != null) {
- String[] routeArray = routes.split(",");
- for (int i = 0; i < routeArray.length; i++) {
- String[] prefixAndMask = routeArray[i].split("/");
+ final String[] addressesArray = addresses.split(",");
+ for (String address : addressesArray) {
+ final Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+ consumer.accept(ipAndMask.first, ipAndMask.second);
+ }
+
+ return addresses;
+ }
+
+ private String parseIpPrefixListArgument(String packageName, Intent intent, String argName,
+ Consumer<IpPrefix> consumer) {
+ return parseIpAndMaskListArgument(packageName, intent, argName,
+ (inetAddress, prefixLength) -> consumer.accept(
+ new IpPrefix(inetAddress, prefixLength)));
+ }
+
+ private void start(String packageName, Intent intent) {
+ Builder builder = new Builder();
+ VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+
+ final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
+ builder::addAddress);
+
+ String addedRoutes;
+ if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
+ false)) {
+ addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
try {
- InetAddress address = InetAddress.getByName(prefixAndMask[0]);
- int prefixLength = Integer.parseInt(prefixAndMask[1]);
- builder.addRoute(address, prefixLength);
- } catch (UnknownHostException|NumberFormatException|
- ArrayIndexOutOfBoundsException e) {
- continue;
+ vpnServiceBuilderShim.addRoute(builder, prefix);
+ } catch (UnsupportedApiLevelException e) {
+ throw new RuntimeException(e);
}
- }
+ });
+ } else {
+ addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
+ builder::addRoute);
+ }
+
+ String excludedRoutes = null;
+ if (SdkLevel.isAtLeastT()) {
+ excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
+ (prefix) -> {
+ try {
+ vpnServiceBuilderShim.excludeRoute(builder, prefix);
+ } catch (UnsupportedApiLevelException e) {
+ throw new RuntimeException(e);
+ }
+ });
}
String allowed = intent.getStringExtra(packageName + ".allowedapplications");
@@ -152,7 +184,8 @@
Log.i(TAG, "Establishing VPN,"
+ " addresses=" + addresses
- + " routes=" + routes
+ + " addedRoutes=" + addedRoutes
+ + " excludedRoutes=" + excludedRoutes
+ " allowedApplications=" + allowed
+ " disallowedApplications=" + disallowed);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 65ea4b1..708d600 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -298,28 +298,54 @@
mActivity.startService(intent);
}
- private void establishVpn(String[] addresses, String[] routes, String allowedApplications,
- String disallowedApplications, @Nullable ProxyInfo proxyInfo,
- @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+ private void establishVpn(String[] addresses, String[] routes, String[] excludedRoutes,
+ String allowedApplications, String disallowedApplications,
+ @Nullable ProxyInfo proxyInfo, @Nullable ArrayList<Network> underlyingNetworks,
+ boolean isAlwaysMetered, boolean addRoutesByIpPrefix)
throws Exception {
final Intent intent = new Intent(mActivity, MyVpnService.class)
.putExtra(mPackageName + ".cmd", MyVpnService.CMD_CONNECT)
.putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
.putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+ .putExtra(mPackageName + ".excludedRoutes", TextUtils.join(",", excludedRoutes))
.putExtra(mPackageName + ".allowedapplications", allowedApplications)
.putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
.putExtra(mPackageName + ".httpProxy", proxyInfo)
.putParcelableArrayListExtra(
mPackageName + ".underlyingNetworks", underlyingNetworks)
- .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+ .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered)
+ .putExtra(mPackageName + ".addRoutesByIpPrefix", addRoutesByIpPrefix);
mActivity.startService(intent);
}
// TODO: Consider replacing arguments with a Builder.
private void startVpn(
- String[] addresses, String[] routes, String allowedApplications,
- String disallowedApplications, @Nullable ProxyInfo proxyInfo,
- @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
+ String[] addresses, String[] routes, String allowedApplications,
+ String disallowedApplications, @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+ throws Exception {
+ startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+ disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered);
+ }
+
+ private void startVpn(
+ String[] addresses, String[] routes, String[] excludedRoutes,
+ String allowedApplications, String disallowedApplications,
+ @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+ throws Exception {
+ startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+ disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered,
+ false /* addRoutesByIpPrefix */);
+ }
+
+ private void startVpn(
+ String[] addresses, String[] routes, String[] excludedRoutes,
+ String allowedApplications, String disallowedApplications,
+ @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered,
+ boolean addRoutesByIpPrefix)
+ throws Exception {
prepareVpn();
// Register a callback so we will be notified when our VPN comes up.
@@ -340,8 +366,8 @@
mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
// Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
- establishVpn(addresses, routes, allowedApplications, disallowedApplications, proxyInfo,
- underlyingNetworks, isAlwaysMetered);
+ establishVpn(addresses, routes, excludedRoutes, allowedApplications, disallowedApplications,
+ proxyInfo, underlyingNetworks, isAlwaysMetered, addRoutesByIpPrefix);
synchronized (mLock) {
if (mNetwork == null) {
Log.i(TAG, "bf mLock");
@@ -561,6 +587,12 @@
}
private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+ checkUdpEcho(to, expectedFrom, expectedFrom != null);
+ }
+
+ private void checkUdpEcho(String to, String expectedFrom,
+ boolean expectConnectionOwnerIsVisible)
+ throws IOException {
DatagramSocket s;
InetAddress address = InetAddress.getByName(to);
if (address instanceof Inet6Address) { // http://b/18094870
@@ -584,7 +616,7 @@
try {
if (expectedFrom != null) {
s.send(p);
- checkConnectionOwnerUidUdp(s, true);
+ checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
s.receive(p);
MoreAsserts.assertEquals(data, p.getData());
} else {
@@ -593,7 +625,7 @@
s.receive(p);
fail("Received unexpected reply");
} catch (IOException expected) {
- checkConnectionOwnerUidUdp(s, false);
+ checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
}
}
} finally {
@@ -601,19 +633,38 @@
}
}
+ private void checkTrafficOnVpn(String destination) throws Exception {
+ final InetAddress address = InetAddress.getByName(destination);
+
+ if (address instanceof Inet6Address) {
+ checkUdpEcho(destination, "2001:db8:1:2::ffe");
+ checkTcpReflection(destination, "2001:db8:1:2::ffe");
+ checkPing(destination);
+ } else {
+ checkUdpEcho(destination, "192.0.2.2");
+ checkTcpReflection(destination, "192.0.2.2");
+ }
+
+ }
+
+ private void checkNoTrafficOnVpn(String destination) throws IOException {
+ checkUdpEcho(destination, null);
+ checkTcpReflection(destination, null);
+ }
+
private void checkTrafficOnVpn() throws Exception {
- checkUdpEcho("192.0.2.251", "192.0.2.2");
- checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
- checkPing("2001:db8:dead:beef::f00");
- checkTcpReflection("192.0.2.252", "192.0.2.2");
- checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+ checkTrafficOnVpn("192.0.2.251");
+ checkTrafficOnVpn("2001:db8:dead:beef::f00");
}
private void checkNoTrafficOnVpn() throws Exception {
- checkUdpEcho("192.0.2.251", null);
- checkUdpEcho("2001:db8:dead:beef::f00", null);
- checkTcpReflection("192.0.2.252", null);
- checkTcpReflection("2001:db8:dead:beef::f00", null);
+ checkNoTrafficOnVpn("192.0.2.251");
+ checkNoTrafficOnVpn("2001:db8:dead:beef::f00");
+ }
+
+ private void checkTrafficBypassesVpn(String destination) throws Exception {
+ checkUdpEcho(destination, null, true /* expectVpnOwnedConnection */);
+ checkTcpReflection(destination, null);
}
private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
@@ -974,9 +1025,9 @@
}
Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
- new String[] {"192.0.2.0/24", "2001:db8::/32"},
- "", disallowedApps, null, null /* underlyingNetworks */,
- false /* isAlwaysMetered */);
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "", disallowedApps, null, null /* underlyingNetworks */,
+ false /* isAlwaysMetered */);
assertSocketStillOpen(localFd, TEST_HOST);
assertSocketStillOpen(remoteFd, TEST_HOST);
@@ -989,6 +1040,74 @@
}
@Test
+ public void testExcludedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+ if (!SdkLevel.isAtLeastT()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Excluded routes should bypass VPN.
+ checkTrafficBypassesVpn("192.0.2.1");
+ checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+ // Other routes should go through VPN, since default routes are included.
+ checkTrafficOnVpn("198.51.100.1");
+ checkTrafficOnVpn("2002:db8::1");
+ }
+
+ @Test
+ public void testIncludedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* routes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Included routes should go through VPN.
+ checkTrafficOnVpn("192.0.2.1");
+ checkTrafficOnVpn("2001:db8:dead:beef::f00");
+ // Other routes should bypass VPN, since default routes are not included.
+ checkTrafficBypassesVpn("198.51.100.1");
+ checkTrafficBypassesVpn("2002:db8::1");
+ }
+
+ @Test
+ public void testInterleavedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+ if (!SdkLevel.isAtLeastT()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "192.0.2.0/32", "::/0", "2001:db8::/128"} /* routes */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */,
+ true /* addRoutesByIpPrefix */);
+
+ // Excluded routes should bypass VPN.
+ checkTrafficBypassesVpn("192.0.2.1");
+ checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+
+ // Included routes inside excluded routes should go through VPN, since the longest common
+ // prefix precedes.
+ checkTrafficOnVpn("192.0.2.0");
+ checkTrafficOnVpn("2001:db8::");
+
+ // Other routes should go through VPN, since default routes are included.
+ checkTrafficOnVpn("198.51.100.1");
+ checkTrafficOnVpn("2002:db8::1");
+ }
+
+ @Test
public void testGetConnectionOwnerUidSecurity() throws Exception {
if (!supportedHardware()) return;
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 89c79d3..cc07fd1 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -20,6 +20,7 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.CollectingTestListener;
@@ -42,6 +43,7 @@
protected static final String TAG = "HostsideNetworkTests";
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+ protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
@@ -65,8 +67,12 @@
assertNotNull(mAbi);
assertNotNull(mCtsBuild);
+ DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
+ String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT
+ : TEST_APK;
+
uninstallPackage(TEST_PKG, false);
- installPackage(TEST_APK);
+ installPackage(testApk);
}
@Override
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 180015d..3821f87 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -104,4 +104,16 @@
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
"testDownloadWithDownloadManagerDisallowed");
}
+
+ public void testExcludedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testExcludedRoutes");
+ }
+
+ public void testIncludedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testIncludedRoutes");
+ }
+
+ public void testInterleavedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
deleted file mode 100644
index 2bcfdc3..0000000
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.java
+++ /dev/null
@@ -1,594 +0,0 @@
-/*
- * 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.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.Arrays;
-import java.util.Random;
-import java.util.List;
-import java.util.ArrayList;
-
-@AppModeFull(reason = "Socket cannot bind in instant app mode")
-public class NsdManagerTest extends AndroidTestCase {
-
- private static final String TAG = "NsdManagerTest";
- private static final String SERVICE_TYPE = "_nmt._tcp";
- private static final int TIMEOUT = 2000;
-
- private static final boolean DBG = false;
-
- NsdManager mNsdManager;
-
- NsdManager.RegistrationListener mRegistrationListener;
- NsdManager.DiscoveryListener mDiscoveryListener;
- NsdManager.ResolveListener mResolveListener;
- private NsdServiceInfo mResolvedService;
-
- public NsdManagerTest() {
- initRegistrationListener();
- initDiscoveryListener();
- initResolveListener();
- }
-
- private void initRegistrationListener() {
- mRegistrationListener = new NsdManager.RegistrationListener() {
- @Override
- public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onRegistrationFailed", errorCode);
- }
-
- @Override
- public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onUnregistrationFailed", errorCode);
- }
-
- @Override
- public void onServiceRegistered(NsdServiceInfo serviceInfo) {
- setEvent("onServiceRegistered", serviceInfo);
- }
-
- @Override
- public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
- setEvent("onServiceUnregistered", serviceInfo);
- }
- };
- }
-
- private void initDiscoveryListener() {
- mDiscoveryListener = new NsdManager.DiscoveryListener() {
- @Override
- public void onStartDiscoveryFailed(String serviceType, int errorCode) {
- setEvent("onStartDiscoveryFailed", errorCode);
- }
-
- @Override
- public void onStopDiscoveryFailed(String serviceType, int errorCode) {
- setEvent("onStopDiscoveryFailed", errorCode);
- }
-
- @Override
- public void onDiscoveryStarted(String serviceType) {
- NsdServiceInfo info = new NsdServiceInfo();
- info.setServiceType(serviceType);
- setEvent("onDiscoveryStarted", info);
- }
-
- @Override
- public void onDiscoveryStopped(String serviceType) {
- NsdServiceInfo info = new NsdServiceInfo();
- info.setServiceType(serviceType);
- setEvent("onDiscoveryStopped", info);
- }
-
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {
- setEvent("onServiceFound", serviceInfo);
- }
-
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {
- setEvent("onServiceLost", serviceInfo);
- }
- };
- }
-
- private void initResolveListener() {
- mResolveListener = new NsdManager.ResolveListener() {
- @Override
- public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onResolveFailed", errorCode);
- }
-
- @Override
- public void onServiceResolved(NsdServiceInfo serviceInfo) {
- mResolvedService = serviceInfo;
- setEvent("onServiceResolved", serviceInfo);
- }
- };
- }
-
-
-
- private final class EventData {
- EventData(String callbackName, NsdServiceInfo info) {
- mCallbackName = callbackName;
- mSucceeded = true;
- mErrorCode = 0;
- mInfo = info;
- }
- EventData(String callbackName, int errorCode) {
- mCallbackName = callbackName;
- mSucceeded = false;
- mErrorCode = errorCode;
- mInfo = null;
- }
- private final String mCallbackName;
- private final boolean mSucceeded;
- private final int mErrorCode;
- private final NsdServiceInfo mInfo;
- }
-
- private final List<EventData> mEventCache = new ArrayList<EventData>();
-
- private void setEvent(String callbackName, int errorCode) {
- if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
- EventData eventData = new EventData(callbackName, errorCode);
- synchronized (mEventCache) {
- mEventCache.add(eventData);
- mEventCache.notify();
- }
- }
-
- private void setEvent(String callbackName, NsdServiceInfo info) {
- if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
- EventData eventData = new EventData(callbackName, info);
- synchronized (mEventCache) {
- mEventCache.add(eventData);
- mEventCache.notify();
- }
- }
-
- void clearEventCache() {
- synchronized(mEventCache) {
- mEventCache.clear();
- }
- }
-
- int eventCacheSize() {
- synchronized(mEventCache) {
- return mEventCache.size();
- }
- }
-
- private int mWaitId = 0;
- private EventData waitForCallback(String callbackName) {
-
- synchronized(mEventCache) {
-
- mWaitId ++;
- if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
-
- try {
- long startTime = android.os.SystemClock.uptimeMillis();
- long elapsedTime = 0;
- int index = 0;
- while (elapsedTime < TIMEOUT ) {
- // first check if we've received that event
- for (; index < mEventCache.size(); index++) {
- EventData e = mEventCache.get(index);
- if (e.mCallbackName.equals(callbackName)) {
- if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
- return e;
- }
- }
-
- // Not yet received, just wait
- mEventCache.wait(TIMEOUT - elapsedTime);
- elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
- }
- // we exited the loop because of TIMEOUT; fail the call
- if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
- return null;
- } catch (InterruptedException e) {
- return null; // wait timed out!
- }
- }
- }
-
- private EventData waitForNewEvents() throws InterruptedException {
- if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
-
- long startTime = android.os.SystemClock.uptimeMillis();
- long elapsedTime = 0;
- synchronized (mEventCache) {
- int index = mEventCache.size();
- while (elapsedTime < TIMEOUT ) {
- // first check if we've received that event
- for (; index < mEventCache.size(); index++) {
- EventData e = mEventCache.get(index);
- return e;
- }
-
- // Not yet received, just wait
- mEventCache.wait(TIMEOUT - elapsedTime);
- elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
- }
- }
-
- return null;
- }
-
- private String mServiceName;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- if (DBG) Log.d(TAG, "Setup test ...");
- mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
-
- Random rand = new Random();
- mServiceName = new String("NsdTest");
- for (int i = 0; i < 4; i++) {
- mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
- }
- }
-
- @Override
- public void tearDown() throws Exception {
- if (DBG) Log.d(TAG, "Tear down test ...");
- super.tearDown();
- }
-
- public void testNDSManager() throws Exception {
- EventData lastEvent = null;
-
- if (DBG) Log.d(TAG, "Starting test ...");
-
- NsdServiceInfo si = new NsdServiceInfo();
- si.setServiceType(SERVICE_TYPE);
- si.setServiceName(mServiceName);
-
- byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
- String 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
- try {
- si.setAttribute(null, (String) null);
- fail("Could set null key");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("", (String) null);
- fail("Could set empty key");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(String256, (String) null);
- fail("Could set key with 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("key", String256.substring(3));
- fail("Could set key+value combination with more than 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("key", String256.substring(4));
- fail("Could set key+value combination with 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(new String(new byte[]{0x19}), (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("=", (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(new String(new byte[]{0x7F}), (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- // Allowed attributes
- si.setAttribute("booleanAttr", (String) null);
- si.setAttribute("keyValueAttr", "value");
- si.setAttribute("keyEqualsAttr", "=");
- si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
- si.setAttribute("binaryDataAttr", testByteArray);
- si.setAttribute("nullBinaryDataAttr", (byte[]) null);
- si.setAttribute("emptyBinaryDataAttr", new byte[]{});
- si.setAttribute("longkey", String256.substring(9));
-
- ServerSocket socket;
- int localPort;
-
- try {
- socket = new ServerSocket(0);
- localPort = socket.getLocalPort();
- si.setPort(localPort);
- } catch (IOException e) {
- if (DBG) Log.d(TAG, "Could not open a local socket");
- assertTrue(false);
- return;
- }
-
- if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
-
- clearEventCache();
-
- mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
- lastEvent = waitForCallback("onServiceRegistered"); // id = 1
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(eventCacheSize() == 1);
-
- // We may not always get the name that we tried to register;
- // This events tells us the name that was registered.
- String registeredName = lastEvent.mInfo.getServiceName();
- si.setServiceName(registeredName);
-
- clearEventCache();
-
- mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
- mDiscoveryListener);
-
- // Expect discovery started
- lastEvent = waitForCallback("onDiscoveryStarted"); // id = 2
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- // Remove this event, so accounting becomes easier later
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
-
- // Expect a service record to be discovered (and filter the ones
- // that are unrelated to this test)
- boolean found = false;
- for (int i = 0; i < 32; i++) {
-
- lastEvent = waitForCallback("onServiceFound"); // id = 3
- if (lastEvent == null) {
- // no more onServiceFound events are being reported!
- break;
- }
-
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
- // Save it, as it will get overwritten with new serviceFound events
- si = lastEvent.mInfo;
- found = true;
- }
-
- // Remove this event from the event cache, so it won't be found by subsequent
- // calls to waitForCallback
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
- }
-
- assertTrue(found);
-
- // We've removed all serviceFound events, and we've removed the discoveryStarted
- // event as well, so now the event cache should be empty!
- assertTrue(eventCacheSize() == 0);
-
- // Resolve the service
- clearEventCache();
- mNsdManager.resolveService(si, mResolveListener);
- lastEvent = waitForCallback("onServiceResolved"); // id = 4
-
- assertNotNull(mResolvedService);
-
- // Check Txt attributes
- assertEquals(8, mResolvedService.getAttributes().size());
- assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
- assertNull(mResolvedService.getAttributes().get("booleanAttr"));
- assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
- assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
- assertEquals(" value ", new String(mResolvedService.getAttributes()
- .get(" whiteSpaceKeyValueAttr ")));
- assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
- .get("longkey")));
- assertTrue(Arrays.equals(testByteArray,
- mResolvedService.getAttributes().get("binaryDataAttr")));
- assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
- assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
- assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
- assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
- String.valueOf(lastEvent.mInfo.getPort()));
-
- assertTrue(lastEvent.mInfo.getPort() == localPort);
- assertTrue(eventCacheSize() == 1);
-
- checkForAdditionalEvents();
- clearEventCache();
-
- // Unregister the service
- mNsdManager.unregisterService(mRegistrationListener);
- lastEvent = waitForCallback("onServiceUnregistered"); // id = 5
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- // Expect a callback for service lost
- lastEvent = waitForCallback("onServiceLost"); // id = 6
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
- // Register service again to see if we discover it
- checkForAdditionalEvents();
- clearEventCache();
-
- si = new NsdServiceInfo();
- si.setServiceType(SERVICE_TYPE);
- si.setServiceName(mServiceName);
- si.setPort(localPort);
-
- // Create a new registration listener and register same service again
- initRegistrationListener();
-
- mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-
- lastEvent = waitForCallback("onServiceRegistered"); // id = 7
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- registeredName = lastEvent.mInfo.getServiceName();
-
- // Expect a record to be discovered
- // Expect a service record to be discovered (and filter the ones
- // that are unrelated to this test)
- found = false;
- for (int i = 0; i < 32; i++) {
-
- lastEvent = waitForCallback("onServiceFound"); // id = 8
- if (lastEvent == null) {
- // no more onServiceFound events are being reported!
- break;
- }
-
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
- // Save it, as it will get overwritten with new serviceFound events
- si = lastEvent.mInfo;
- found = true;
- }
-
- // Remove this event from the event cache, so it won't be found by subsequent
- // calls to waitForCallback
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
- }
-
- assertTrue(found);
-
- // Resolve the service
- clearEventCache();
- mNsdManager.resolveService(si, mResolveListener);
- lastEvent = waitForCallback("onServiceResolved"); // id = 9
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
- assertNotNull(mResolvedService);
-
- // Check that we don't have any TXT records
- assertEquals(0, mResolvedService.getAttributes().size());
-
- checkForAdditionalEvents();
- clearEventCache();
-
- mNsdManager.stopServiceDiscovery(mDiscoveryListener);
- lastEvent = waitForCallback("onDiscoveryStopped"); // id = 10
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(checkCacheSize(1));
-
- checkForAdditionalEvents();
- clearEventCache();
-
- mNsdManager.unregisterService(mRegistrationListener);
-
- lastEvent = waitForCallback("onServiceUnregistered"); // id = 11
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(checkCacheSize(1));
- }
-
- boolean checkCacheSize(int size) {
- synchronized (mEventCache) {
- int cacheSize = mEventCache.size();
- if (cacheSize != size) {
- Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
- for (int i = 0; i < cacheSize; i++) {
- EventData e = mEventCache.get(i);
- String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
- Log.d(TAG, "eventName is " + e.mCallbackName + sname);
- }
- }
- return (cacheSize == size);
- }
- }
-
- boolean checkForAdditionalEvents() {
- try {
- EventData e = waitForNewEvents();
- if (e != null) {
- String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
- Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
- }
- return (e == null);
- }
- catch (InterruptedException ex) {
- return false;
- }
- }
-}
-
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
new file mode 100644
index 0000000..9307c27
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -0,0 +1,325 @@
+/*
+ * 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.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.ServiceResolved
+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.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 org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.ServerSocket
+import java.nio.charset.StandardCharsets
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val TAG = "NsdManagerTest"
+private const val SERVICE_TYPE = "_nmt._tcp"
+private const val TIMEOUT_MS = 2000L
+private const val DBG = false
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+@RunWith(AndroidJUnit4::class)
+class NsdManagerTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+ private val serviceName = "NsdTest%04d".format(Random().nextInt(1000))
+
+ private interface NsdEvent
+ private open class NsdRecord<T : NsdEvent> private constructor(
+ private val history: ArrayTrackRecord<T>
+ ) : TrackRecord<T> by history {
+ constructor() : this(ArrayTrackRecord())
+
+ val nextEvents = history.newReadHead()
+
+ inline fun <reified V : NsdEvent> expectCallbackEventually(
+ crossinline predicate: (V) -> Boolean = { true }
+ ): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
+ ?: fail("Callback for ${V::class.java.simpleName} not seen after $TIMEOUT_MS ms")
+
+ inline fun <reified V : NsdEvent> expectCallback(): V {
+ val nextEvent = nextEvents.poll(TIMEOUT_MS)
+ assertNotNull(nextEvent, "No callback received after $TIMEOUT_MS ms")
+ assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
+ nextEvent.javaClass.simpleName)
+ return nextEvent
+ }
+ }
+
+ private class NsdRegistrationRecord : RegistrationListener,
+ NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+ 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 : DiscoveryListener,
+ NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+ 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): NsdServiceInfo {
+ return expectCallbackEventually<ServiceFound> {
+ it.serviceInfo.serviceName == serviceName
+ }.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()
+ }
+
+ override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
+ add(ResolveFailed(si, err))
+ }
+
+ override fun onServiceResolved(si: NsdServiceInfo) {
+ add(ServiceResolved(si))
+ }
+ }
+
+ @Test
+ fun testNsdManager() {
+ val si = NsdServiceInfo()
+ si.serviceType = SERVICE_TYPE
+ 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()
+ val registeredInfo = registerService(registrationRecord, si)
+
+ val discoveryRecord = NsdDiscoveryRecord()
+ nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+
+ // Expect discovery started
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+
+ // Expect a service record to be discovered
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
+
+ val resolvedService = resolveService(foundInfo)
+
+ // 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 = SERVICE_TYPE
+ si2.serviceName = serviceName
+ si2.port = localPort
+ val registrationRecord2 = NsdRegistrationRecord()
+ val registeredInfo2 = registerService(registrationRecord2, si2)
+
+ // 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 resolvedService2 = resolveService(foundInfo2)
+
+ // 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>()
+ }
+
+ /**
+ * Register a service and return its registration record.
+ */
+ private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, 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>()
+ return cb.serviceInfo
+ }
+
+ private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
+ val record = NsdResolveRecord()
+ nsdManager.resolveService(discoveredInfo, 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)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c47604c..8d91552 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3386,12 +3386,12 @@
private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) {
return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
- /*secure=*/ false, VpnManager.TYPE_VPN_NONE);
+ /*secure=*/ false, VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
}
private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) {
return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE,
- secure, vpnType);
+ secure, vpnType, /*excludeLocalRoutes=*/ false);
}
@Test